1use std::collections::{HashMap, HashSet};
66use std::sync::{Arc, RwLock};
67use std::time::{Duration, Instant, SystemTime};
68
69use fastmcp_core::{
70 AccessToken, AuthContext, McpContext, McpError, McpErrorCode, McpResult, SecurityIdentifier,
71 Sha256Digest, draw_security_identifier, sha256_bounded,
72};
73use url::{Host, Url};
74use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
75
76use crate::auth::{AuthRequest, TokenVerifier};
77#[cfg(feature = "builtin-auth-server")]
78use crate::oidc::OidcProvider;
79
80const PKCE_CODE_VERIFIER_MIN_BYTES: usize = 43;
81const PKCE_CODE_VERIFIER_MAX_BYTES: usize = 128;
82const OAUTH_OPAQUE_CREDENTIAL_BYTES: usize = 43;
83const CLIENT_SECRET_VERIFIER_DOMAIN: &[u8] = b"fastmcp:oauth:client-secret:v1\0";
84const AUTHORIZATION_CODE_DIGEST_DOMAIN: &[u8] = b"fastmcp:oauth:authorization-code:v1\0";
85const ACCESS_TOKEN_DIGEST_DOMAIN: &[u8] = b"fastmcp:oauth:access-token:v1\0";
86const REFRESH_TOKEN_DIGEST_DOMAIN: &[u8] = b"fastmcp:oauth:refresh-token:v1\0";
87const AUTHORIZATION_GRANT_ID_DOMAIN: &[u8] = b"fastmcp:oauth:authorization-grant-id:v1\0";
88const DIRECT_GRANT_ID_DOMAIN: &[u8] = b"fastmcp:oauth:direct-grant-id:v1\0";
89const OAUTH_SESSION_OWNER_DOMAIN: &[u8] = b"fastmcp:oauth:session-owner:v1\0";
90const OAUTH_REGISTRATION_EPOCH_BYTES: usize = 32;
91const CLIENT_SECRET_SALT_BYTES: usize = 32;
92const MAX_CLIENT_SECRET_VERIFIER_INPUT_BYTES: usize = CLIENT_SECRET_VERIFIER_DOMAIN.len()
93 + CLIENT_SECRET_SALT_BYTES
94 + MAX_OAUTH_CLIENT_CREDENTIAL_BYTES;
95const MAX_OPAQUE_CREDENTIAL_DIGEST_INPUT_BYTES: usize =
96 AUTHORIZATION_CODE_DIGEST_DOMAIN.len() + OAUTH_OPAQUE_CREDENTIAL_BYTES;
97const MAX_GRANT_ID_DERIVATION_INPUT_BYTES: usize = AUTHORIZATION_GRANT_ID_DOMAIN.len() + 64;
98const DUMMY_CLIENT_SECRET_SALT: [u8; CLIENT_SECRET_SALT_BYTES] = [0x5a; CLIENT_SECRET_SALT_BYTES];
99const DUMMY_CLIENT_SECRET_DIGEST: Sha256Digest = Sha256Digest::from_bytes([0xa5; 32]);
100
101pub const MAX_OAUTH_ISSUER_BYTES: usize = 2_048;
103
104pub const MAX_OAUTH_CLIENT_ID_BYTES: usize = 256;
106pub const MAX_OAUTH_CLIENT_CREDENTIAL_BYTES: usize = 1_024;
108pub const MAX_OAUTH_REDIRECT_URIS_PER_CLIENT: usize = 16;
110pub const MAX_OAUTH_REDIRECT_URI_BYTES: usize = 2_048;
112pub const MAX_OAUTH_SCOPES_PER_CLIENT: usize = 64;
114pub const MAX_OAUTH_SCOPE_BYTES: usize = 256;
116pub const MAX_OAUTH_CLIENT_NAME_BYTES: usize = 256;
118pub const MAX_OAUTH_CLIENT_DESCRIPTION_BYTES: usize = 4_096;
120pub const MAX_OAUTH_SUBJECT_BYTES: usize = 1_024;
122pub const MAX_OAUTH_RESOURCE_BYTES: usize = 2_048;
124const MAX_OAUTH_SESSION_OWNER_INPUT_BYTES: usize = OAUTH_SESSION_OWNER_DOMAIN.len()
125 + 8
126 + MAX_OAUTH_ISSUER_BYTES
127 + 8
128 + MAX_OAUTH_CLIENT_ID_BYTES
129 + OAUTH_REGISTRATION_EPOCH_BYTES
130 + 1
131 + 8
132 + MAX_OAUTH_SUBJECT_BYTES;
133pub const MAX_OAUTH_STATE_BYTES: usize = 4_096;
135
136pub const MAX_OAUTH_AUTHORIZATION_QUERY_BYTES: usize = 16 * 1_024;
141pub const MAX_OAUTH_FORM_BODY_BYTES: usize = 16 * 1_024;
143pub const MAX_OAUTH_PARAMETER_PAIRS: usize = 64;
145pub const MAX_OAUTH_PARAMETER_NAME_BYTES: usize = 256;
147pub const MAX_OAUTH_PARAMETER_VALUE_BYTES: usize = MAX_OAUTH_STATE_BYTES;
149
150const OAUTH_ISSUER_ERROR: &str = "OAuth issuer URL is invalid or outside retained-value bounds";
151const OAUTH_CLIENT_ID_RETENTION_ERROR: &str = "OAuth client_id is outside retained-value bounds";
152const OAUTH_CLIENT_CREDENTIAL_RETENTION_ERROR: &str =
153 "OAuth client credential is outside retained-value bounds";
154const OAUTH_CLIENT_CREDENTIAL_CLASS_ERROR: &str =
155 "OAuth client credential classification is inconsistent";
156const OAUTH_CLIENT_REDIRECT_REQUIRED_ERROR: &str =
157 "OAuth client requires at least one redirect URI";
158const OAUTH_CLIENT_REDIRECT_COUNT_ERROR: &str =
159 "OAuth client redirect URI count exceeds retention bounds";
160const OAUTH_CLIENT_REDIRECT_VALUE_ERROR: &str =
161 "OAuth client redirect URI is outside retained-value bounds";
162const OAUTH_CLIENT_SCOPE_COUNT_ERROR: &str = "OAuth client scope count exceeds retention bounds";
163const OAUTH_CLIENT_SCOPE_VALUE_ERROR: &str = "OAuth client scope is outside retained-value bounds";
164const OAUTH_CLIENT_NAME_RETENTION_ERROR: &str =
165 "OAuth client name is invalid or outside retention bounds";
166const OAUTH_CLIENT_DESCRIPTION_RETENTION_ERROR: &str =
167 "OAuth client description is invalid or outside retention bounds";
168const OAUTH_AUTHORIZATION_SUBJECT_RETENTION_ERROR: &str =
169 "OAuth authorization subject is invalid or outside retention bounds";
170const OAUTH_AUTHORIZATION_STATE_RETENTION_ERROR: &str =
171 "OAuth authorization state is invalid or outside retention bounds";
172const OAUTH_AUTHORIZATION_RESOURCE_RETENTION_ERROR: &str =
173 "OAuth authorization resource is invalid or outside retention bounds";
174const OAUTH_REQUEST_SCOPE_COUNT_ERROR: &str = "OAuth request scope count exceeds retention bounds";
175const OAUTH_REQUEST_SCOPE_VALUE_ERROR: &str = "OAuth request scope is invalid or outside bounds";
176const OAUTH_CLIENT_NOT_FOUND_ERROR: &str = "OAuth client not found";
177const OAUTH_CLIENT_AUTHENTICATION_ERROR: &str = "client authentication failed";
178const OAUTH_GRANT_TYPE_UNSUPPORTED_ERROR: &str = "OAuth grant_type is not supported";
179const OAUTH_INVALID_GRANT_ERROR: &str = "OAuth grant is invalid";
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193pub enum OAuthParameterEndpoint {
194 AuthorizationQuery,
196 TokenForm,
198 RevocationForm,
200 IntrospectionForm,
202}
203
204impl OAuthParameterEndpoint {
205 #[must_use]
207 pub const fn source(self) -> OAuthParameterSource {
208 match self {
209 Self::AuthorizationQuery => OAuthParameterSource::Query,
210 Self::TokenForm | Self::RevocationForm | Self::IntrospectionForm => {
211 OAuthParameterSource::Form
212 }
213 }
214 }
215
216 const fn maximum_input_bytes(self) -> usize {
217 match self {
218 Self::AuthorizationQuery => MAX_OAUTH_AUTHORIZATION_QUERY_BYTES,
219 Self::TokenForm | Self::RevocationForm | Self::IntrospectionForm => {
220 MAX_OAUTH_FORM_BODY_BYTES
221 }
222 }
223 }
224
225 fn defined_parameter(self, name: &str) -> Option<OAuthParameterName> {
226 match self {
227 Self::AuthorizationQuery => match name {
228 "response_type" => Some(OAuthParameterName::ResponseType),
229 "client_id" => Some(OAuthParameterName::ClientId),
230 "redirect_uri" => Some(OAuthParameterName::RedirectUri),
231 "resource" => Some(OAuthParameterName::Resource),
232 "scope" => Some(OAuthParameterName::Scope),
233 "state" => Some(OAuthParameterName::State),
234 "code_challenge" => Some(OAuthParameterName::CodeChallenge),
235 "code_challenge_method" => Some(OAuthParameterName::CodeChallengeMethod),
236 _ => None,
237 },
238 Self::TokenForm => match name {
239 "grant_type" => Some(OAuthParameterName::GrantType),
240 "code" => Some(OAuthParameterName::Code),
241 "redirect_uri" => Some(OAuthParameterName::RedirectUri),
242 "resource" => Some(OAuthParameterName::Resource),
243 "client_id" => Some(OAuthParameterName::ClientId),
244 "client_secret" => Some(OAuthParameterName::ClientSecret),
245 "code_verifier" => Some(OAuthParameterName::CodeVerifier),
246 "refresh_token" => Some(OAuthParameterName::RefreshToken),
247 "scope" => Some(OAuthParameterName::Scope),
248 _ => None,
249 },
250 Self::RevocationForm | Self::IntrospectionForm => match name {
251 "token" => Some(OAuthParameterName::Token),
252 "token_type_hint" => Some(OAuthParameterName::TokenTypeHint),
253 "client_id" => Some(OAuthParameterName::ClientId),
254 "client_secret" => Some(OAuthParameterName::ClientSecret),
255 _ => None,
256 },
257 }
258 }
259}
260
261#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263pub enum OAuthParameterSource {
264 Query,
266 Form,
268}
269
270#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
276pub enum OAuthParameterName {
277 ResponseType,
279 ClientId,
281 RedirectUri,
283 Resource,
285 Scope,
287 State,
289 CodeChallenge,
291 CodeChallengeMethod,
293 GrantType,
295 Code,
297 ClientSecret,
299 CodeVerifier,
301 RefreshToken,
303 Token,
305 TokenTypeHint,
307}
308
309#[derive(Clone)]
315pub struct OAuthHttpRoutes {
316 server: Arc<OAuthServer>,
317 public_endpoint_base: String,
318 authorization_path: String,
319 token_path: String,
320 revocation_path: String,
321 #[cfg(feature = "builtin-auth-server")]
322 oidc: Option<OidcHttpRoutes>,
323}
324
325#[cfg(feature = "builtin-auth-server")]
327#[derive(Clone)]
328pub(crate) struct OidcHttpRoutes {
329 provider: Arc<OidcProvider>,
330 discovery_path: String,
331 jwks_path: String,
332 jwks_uri: String,
333}
334
335#[cfg(feature = "builtin-auth-server")]
336impl OidcHttpRoutes {
337 pub(crate) fn provider(&self) -> &Arc<OidcProvider> {
338 &self.provider
339 }
340
341 pub(crate) fn discovery_path(&self) -> &str {
342 &self.discovery_path
343 }
344
345 pub(crate) fn jwks_path(&self) -> &str {
346 &self.jwks_path
347 }
348
349 pub(crate) fn jwks_uri(&self) -> &str {
350 &self.jwks_uri
351 }
352}
353
354impl std::fmt::Debug for OAuthHttpRoutes {
355 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
356 let mut debug = formatter.debug_struct("OAuthHttpRoutes");
357 debug
358 .field("public_endpoint_base", &self.public_endpoint_base)
359 .field("authorization_path", &self.authorization_path)
360 .field("token_path", &self.token_path)
361 .field("revocation_path", &self.revocation_path);
362 #[cfg(feature = "builtin-auth-server")]
363 debug
364 .field(
365 "oidc_discovery_path",
366 &self.oidc.as_ref().map(|oidc| oidc.discovery_path.as_str()),
367 )
368 .field(
369 "oidc_jwks_path",
370 &self.oidc.as_ref().map(|oidc| oidc.jwks_path.as_str()),
371 );
372 debug.finish_non_exhaustive()
373 }
374}
375
376#[derive(Debug, Clone, PartialEq, Eq)]
378pub enum OAuthHttpRouteConfigurationError {
379 InvalidPublicEndpointBase,
381 IssuerOriginMismatch,
383}
384
385impl std::fmt::Display for OAuthHttpRouteConfigurationError {
386 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
387 match self {
388 Self::InvalidPublicEndpointBase => formatter.write_str(
389 "OAuth public endpoint base must be a canonical HTTPS URL without query or fragment",
390 ),
391 Self::IssuerOriginMismatch => formatter.write_str(
392 "OAuth public endpoint base must share the configured issuer origin",
393 ),
394 }
395 }
396}
397
398impl std::error::Error for OAuthHttpRouteConfigurationError {}
399
400impl OAuthHttpRoutes {
401 pub fn new(
408 server: Arc<OAuthServer>,
409 public_endpoint_base: impl Into<String>,
410 ) -> Result<Self, OAuthHttpRouteConfigurationError> {
411 let public_endpoint_base = public_endpoint_base.into();
412 let Some(base) = parse_secure_endpoint(&public_endpoint_base, MAX_OAUTH_ISSUER_BYTES)
413 else {
414 return Err(OAuthHttpRouteConfigurationError::InvalidPublicEndpointBase);
415 };
416 if base.scheme() != "https" || base.query().is_some() {
417 return Err(OAuthHttpRouteConfigurationError::InvalidPublicEndpointBase);
418 }
419 let Some(issuer) = parse_secure_endpoint(&server.config().issuer, MAX_OAUTH_ISSUER_BYTES)
420 else {
421 return Err(OAuthHttpRouteConfigurationError::IssuerOriginMismatch);
422 };
423 if base.scheme() != issuer.scheme()
424 || base.host_str() != issuer.host_str()
425 || base.port_or_known_default() != issuer.port_or_known_default()
426 {
427 return Err(OAuthHttpRouteConfigurationError::IssuerOriginMismatch);
428 }
429
430 let base_path = base.path().trim_end_matches('/');
431 let route_path = |suffix: &str| {
432 if base_path.is_empty() {
433 format!("/{suffix}")
434 } else {
435 format!("{base_path}/{suffix}")
436 }
437 };
438 Ok(Self {
439 server,
440 public_endpoint_base,
441 authorization_path: route_path("authorize"),
442 token_path: route_path("token"),
443 revocation_path: route_path("revoke"),
444 #[cfg(feature = "builtin-auth-server")]
445 oidc: None,
446 })
447 }
448
449 #[cfg(feature = "builtin-auth-server")]
454 pub fn with_oidc(
455 mut self,
456 provider: Arc<OidcProvider>,
457 ) -> Result<Self, OAuthHttpRouteConfigurationError> {
458 if !Arc::ptr_eq(provider.oauth(), &self.server) {
459 return Err(OAuthHttpRouteConfigurationError::IssuerOriginMismatch);
460 }
461 let jwks_uri = provider
462 .advertised_id_token_jwks_uri()
463 .map_err(|_| OAuthHttpRouteConfigurationError::InvalidPublicEndpointBase)?;
464 let jwks = Url::parse(&jwks_uri)
465 .map_err(|_| OAuthHttpRouteConfigurationError::InvalidPublicEndpointBase)?;
466 let issuer = Url::parse(&provider.config().issuer)
467 .map_err(|_| OAuthHttpRouteConfigurationError::IssuerOriginMismatch)?;
468 if jwks.origin() != issuer.origin() || jwks.path().is_empty() {
469 return Err(OAuthHttpRouteConfigurationError::IssuerOriginMismatch);
470 }
471 let issuer_path = issuer.path().trim_matches('/');
472 let discovery_path = if issuer_path.is_empty() {
473 "/.well-known/openid-configuration".to_string()
474 } else {
475 format!("/.well-known/openid-configuration/{issuer_path}")
476 };
477 let candidate = OidcHttpRoutes {
478 provider,
479 discovery_path,
480 jwks_path: jwks.path().to_string(),
481 jwks_uri,
482 };
483 if [
484 self.authorization_path(),
485 self.token_path(),
486 self.revocation_path(),
487 ]
488 .contains(&candidate.discovery_path.as_str())
489 || [
490 self.authorization_path(),
491 self.token_path(),
492 self.revocation_path(),
493 ]
494 .contains(&candidate.jwks_path.as_str())
495 || candidate.discovery_path == candidate.jwks_path
496 {
497 return Err(OAuthHttpRouteConfigurationError::InvalidPublicEndpointBase);
498 }
499 self.oidc = Some(candidate);
500 Ok(self)
501 }
502
503 #[must_use]
505 pub fn public_endpoint_base(&self) -> &str {
506 &self.public_endpoint_base
507 }
508
509 #[must_use]
511 pub fn authorization_path(&self) -> &str {
512 &self.authorization_path
513 }
514
515 #[must_use]
517 pub fn token_path(&self) -> &str {
518 &self.token_path
519 }
520
521 #[must_use]
523 pub fn revocation_path(&self) -> &str {
524 &self.revocation_path
525 }
526
527 pub(crate) fn server(&self) -> &Arc<OAuthServer> {
528 &self.server
529 }
530
531 pub(crate) fn has_path(&self, path: &str) -> bool {
532 path == self.authorization_path
533 || path == self.token_path
534 || path == self.revocation_path
535 || {
536 #[cfg(feature = "builtin-auth-server")]
537 {
538 self.oidc
539 .as_ref()
540 .is_some_and(|oidc| path == oidc.discovery_path || path == oidc.jwks_path)
541 }
542 #[cfg(not(feature = "builtin-auth-server"))]
543 {
544 false
545 }
546 }
547 }
548
549 #[cfg(feature = "builtin-auth-server")]
550 pub(crate) fn oidc_routes(&self) -> Option<&OidcHttpRoutes> {
551 self.oidc.as_ref()
552 }
553
554 pub(crate) fn validate_non_overlapping_paths<'a>(
555 &self,
556 occupied_paths: impl IntoIterator<Item = &'a str>,
557 ) -> Result<(), OAuthHttpRouteConfigurationError> {
558 if occupied_paths.into_iter().any(|occupied| {
559 [
560 self.authorization_path(),
561 self.token_path(),
562 self.revocation_path(),
563 ]
564 .contains(&occupied)
565 || {
566 #[cfg(feature = "builtin-auth-server")]
567 {
568 self.oidc.as_ref().is_some_and(|oidc| {
569 occupied == oidc.discovery_path || occupied == oidc.jwks_path
570 })
571 }
572 #[cfg(not(feature = "builtin-auth-server"))]
573 {
574 false
575 }
576 }
577 }) {
578 return Err(OAuthHttpRouteConfigurationError::InvalidPublicEndpointBase);
579 }
580 Ok(())
581 }
582}
583
584#[derive(Debug, Clone)]
586pub struct OAuthAdmittedParameter {
587 source: OAuthParameterSource,
588 ordinal: usize,
589 name: String,
590 value_len: usize,
591 defined: bool,
592}
593
594impl OAuthAdmittedParameter {
595 #[must_use]
597 pub const fn source(&self) -> OAuthParameterSource {
598 self.source
599 }
600
601 #[must_use]
603 pub const fn ordinal(&self) -> usize {
604 self.ordinal
605 }
606
607 #[must_use]
609 pub fn name(&self) -> &str {
610 &self.name
611 }
612
613 #[must_use]
620 pub const fn value_len(&self) -> usize {
621 self.value_len
622 }
623
624 #[must_use]
626 pub const fn is_defined(&self) -> bool {
627 self.defined
628 }
629}
630
631pub struct OAuthParameterAdmission {
638 endpoint: OAuthParameterEndpoint,
639 source: OAuthParameterSource,
640 parameters: Vec<OAuthAdmittedParameter>,
641 defined: HashMap<OAuthParameterName, OAuthSensitiveParameterValue>,
642}
643
644pub(crate) struct OAuthSensitiveParameterValue {
651 value: Zeroizing<String>,
652}
653
654impl OAuthSensitiveParameterValue {
655 #[must_use]
657 pub(crate) fn into_string(mut self) -> String {
658 std::mem::take(&mut *self.value)
659 }
660}
661
662impl std::fmt::Debug for OAuthParameterAdmission {
663 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
664 formatter
665 .debug_struct("OAuthParameterAdmission")
666 .field("endpoint", &self.endpoint)
667 .field("source", &self.source)
668 .field("parameter_count", &self.parameters.len())
669 .field("defined_count", &self.defined.len())
670 .finish()
671 }
672}
673
674#[derive(Debug, Clone, PartialEq, Eq)]
677pub enum OAuthParameterAdmissionError {
678 InputTooLarge,
680 TooManyPairs,
682 EmptyName,
684 NameTooLarge,
686 ValueTooLarge,
688 MalformedPercentEncoding,
690 InvalidUtf8,
692 ControlCharacter,
694 DuplicateDefinedParameter {
696 parameter: OAuthParameterName,
698 source: OAuthParameterSource,
700 first_ordinal: usize,
702 duplicate_ordinal: usize,
704 },
705}
706
707impl std::fmt::Display for OAuthParameterAdmissionError {
708 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
709 let message = match self {
710 Self::InputTooLarge => "OAuth parameter input exceeds the endpoint bound",
711 Self::TooManyPairs => "OAuth parameter input contains too many pairs",
712 Self::EmptyName => "OAuth parameter name is empty",
713 Self::NameTooLarge => "OAuth parameter name exceeds the endpoint bound",
714 Self::ValueTooLarge => "OAuth parameter value exceeds the endpoint bound",
715 Self::MalformedPercentEncoding => "OAuth parameter percent encoding is malformed",
716 Self::InvalidUtf8 => "OAuth parameter encoding is not valid UTF-8",
717 Self::ControlCharacter => "OAuth parameter contains a control character",
718 Self::DuplicateDefinedParameter { .. } => {
719 "OAuth parameter input repeats a defined endpoint parameter"
720 }
721 };
722 formatter.write_str(message)
723 }
724}
725
726impl std::error::Error for OAuthParameterAdmissionError {}
727
728impl OAuthParameterAdmission {
729 pub fn admit(
738 endpoint: OAuthParameterEndpoint,
739 input: &[u8],
740 ) -> Result<Self, OAuthParameterAdmissionError> {
741 if input.len() > endpoint.maximum_input_bytes() {
742 return Err(OAuthParameterAdmissionError::InputTooLarge);
743 }
744
745 let source = endpoint.source();
746 let mut parameters = Vec::new();
747 let mut defined = HashMap::new();
748 let mut seen_defined = HashMap::new();
749 if input.is_empty() {
750 return Ok(Self {
751 endpoint,
752 source,
753 parameters,
754 defined,
755 });
756 }
757
758 let mut ordinal = 0;
759 for pair in input.split(|byte| *byte == b'&') {
760 if pair.is_empty() {
764 continue;
765 }
766 if ordinal >= MAX_OAUTH_PARAMETER_PAIRS {
767 return Err(OAuthParameterAdmissionError::TooManyPairs);
768 }
769 let (raw_name, raw_value): (&[u8], &[u8]) =
774 match pair.iter().position(|byte| *byte == b'=') {
775 Some(delimiter) => {
776 let (name, value_with_delimiter) = pair.split_at(delimiter);
777 (name, &value_with_delimiter[1..])
778 }
779 None => (pair, &[]),
780 };
781 let name = Zeroizing::new(
782 decode_oauth_form_component(raw_name, MAX_OAUTH_PARAMETER_NAME_BYTES).map_err(
783 |error| match error {
784 OAuthFormDecodeError::TooLarge => {
785 OAuthParameterAdmissionError::NameTooLarge
786 }
787 error => OAuthParameterAdmissionError::from(error),
788 },
789 )?,
790 );
791 let value = Zeroizing::new(
792 decode_oauth_form_component(raw_value, MAX_OAUTH_PARAMETER_VALUE_BYTES)
793 .map_err(OAuthParameterAdmissionError::from)?,
794 );
795 if name.is_empty() {
796 return Err(OAuthParameterAdmissionError::EmptyName);
797 }
798 if name.chars().any(char::is_control) || value.chars().any(char::is_control) {
799 return Err(OAuthParameterAdmissionError::ControlCharacter);
800 }
801
802 let value_len = value.len();
803 let defined_name = endpoint.defined_parameter(name.as_str());
804 if let Some(defined_name) = defined_name {
805 if let Some(first_ordinal) = seen_defined.insert(defined_name, ordinal) {
806 return Err(OAuthParameterAdmissionError::DuplicateDefinedParameter {
807 parameter: defined_name,
808 source,
809 first_ordinal,
810 duplicate_ordinal: ordinal,
811 });
812 }
813 if !value.is_empty() {
817 defined.insert(defined_name, OAuthSensitiveParameterValue { value });
818 }
819 }
820 parameters.push(OAuthAdmittedParameter {
821 source,
822 ordinal,
823 name: name.to_string(),
826 value_len,
827 defined: defined_name.is_some(),
828 });
829 ordinal += 1;
830 }
831
832 Ok(Self {
833 endpoint,
834 source,
835 parameters,
836 defined,
837 })
838 }
839
840 #[must_use]
842 pub const fn endpoint(&self) -> OAuthParameterEndpoint {
843 self.endpoint
844 }
845
846 #[must_use]
848 pub const fn source(&self) -> OAuthParameterSource {
849 self.source
850 }
851
852 #[must_use]
854 pub fn parameters(&self) -> &[OAuthAdmittedParameter] {
855 &self.parameters
856 }
857
858 pub(crate) fn take_defined_value(
865 &mut self,
866 name: OAuthParameterName,
867 ) -> Option<OAuthSensitiveParameterValue> {
868 self.defined.remove(&name)
869 }
870
871 pub fn unknown_parameters(&self) -> impl Iterator<Item = &OAuthAdmittedParameter> {
876 self.parameters
877 .iter()
878 .filter(|parameter| !parameter.is_defined())
879 }
880}
881
882enum OAuthFormDecodeError {
883 TooLarge,
884 MalformedPercentEncoding,
885 InvalidUtf8,
886}
887
888impl From<OAuthFormDecodeError> for OAuthParameterAdmissionError {
889 fn from(error: OAuthFormDecodeError) -> Self {
890 match error {
891 OAuthFormDecodeError::TooLarge => Self::ValueTooLarge,
892 OAuthFormDecodeError::MalformedPercentEncoding => Self::MalformedPercentEncoding,
893 OAuthFormDecodeError::InvalidUtf8 => Self::InvalidUtf8,
894 }
895 }
896}
897
898fn decode_oauth_form_component(
899 input: &[u8],
900 maximum_output_bytes: usize,
901) -> Result<String, OAuthFormDecodeError> {
902 let mut decoded = Zeroizing::new(Vec::with_capacity(input.len().min(maximum_output_bytes)));
907 let mut index = 0;
908 while index < input.len() {
909 let byte = input[index];
910 match byte {
911 b'+' => {
912 decoded.push(b' ');
913 index += 1;
914 }
915 b'%' => {
916 let Some(high) = input.get(index + 1).copied().and_then(decode_hex_digit) else {
917 return Err(OAuthFormDecodeError::MalformedPercentEncoding);
918 };
919 let Some(low) = input.get(index + 2).copied().and_then(decode_hex_digit) else {
920 return Err(OAuthFormDecodeError::MalformedPercentEncoding);
921 };
922 decoded.push((high << 4) | low);
923 index += 3;
924 }
925 _ => {
926 decoded.push(byte);
927 index += 1;
928 }
929 }
930 if decoded.len() > maximum_output_bytes {
931 return Err(OAuthFormDecodeError::TooLarge);
932 }
933 }
934 std::str::from_utf8(decoded.as_slice())
939 .map(str::to_owned)
940 .map_err(|_| OAuthFormDecodeError::InvalidUtf8)
941}
942
943const fn decode_hex_digit(byte: u8) -> Option<u8> {
944 match byte {
945 b'0'..=b'9' => Some(byte - b'0'),
946 b'a'..=b'f' => Some(byte - b'a' + 10),
947 b'A'..=b'F' => Some(byte - b'A' + 10),
948 _ => None,
949 }
950}
951
952const MAX_ACCESS_TOKEN_LIFETIME: Duration = Duration::from_hours(24);
957const MAX_REFRESH_TOKEN_LIFETIME: Duration = Duration::from_hours(8_760);
958const MAX_AUTHORIZATION_CODE_LIFETIME: Duration = Duration::from_mins(10);
959const MIN_OAUTH_CREDENTIAL_LIFETIME: Duration = Duration::from_secs(1);
960
961pub const DEFAULT_MAX_OAUTH_CLIENTS: usize = 1_024;
963pub const DEFAULT_MAX_AUTHORIZATION_CODES: usize = 16 * 1_024;
965pub const DEFAULT_MAX_AUTHORIZATION_CODES_PER_CLIENT: usize = 64;
967pub const DEFAULT_MAX_ACCESS_TOKENS: usize = 64 * 1_024;
969pub const DEFAULT_MAX_ACCESS_TOKENS_PER_CLIENT: usize = 256;
971pub const DEFAULT_MAX_REFRESH_TOKENS: usize = 16 * 1_024;
973pub const DEFAULT_MAX_REFRESH_TOKENS_PER_CLIENT: usize = 64;
975pub const DEFAULT_MAX_REVOCATION_TOMBSTONES: usize = 64 * 1_024;
977pub const DEFAULT_MAX_REVOCATION_TOMBSTONES_PER_CLIENT: usize = 256;
979
980pub const HARD_MAX_OAUTH_CLIENTS: usize = 4 * DEFAULT_MAX_OAUTH_CLIENTS;
982pub const HARD_MAX_AUTHORIZATION_CODES: usize = 2 * DEFAULT_MAX_AUTHORIZATION_CODES;
984pub const HARD_MAX_AUTHORIZATION_CODES_PER_CLIENT: usize =
986 4 * DEFAULT_MAX_AUTHORIZATION_CODES_PER_CLIENT;
987pub const HARD_MAX_ACCESS_TOKENS: usize = 2 * DEFAULT_MAX_ACCESS_TOKENS;
989pub const HARD_MAX_ACCESS_TOKENS_PER_CLIENT: usize = 4 * DEFAULT_MAX_ACCESS_TOKENS_PER_CLIENT;
991pub const HARD_MAX_REFRESH_TOKENS: usize = 2 * DEFAULT_MAX_REFRESH_TOKENS;
993pub const HARD_MAX_REFRESH_TOKENS_PER_CLIENT: usize = 4 * DEFAULT_MAX_REFRESH_TOKENS_PER_CLIENT;
995pub const HARD_MAX_REVOCATION_TOMBSTONES: usize = 2 * DEFAULT_MAX_REVOCATION_TOMBSTONES;
997pub const HARD_MAX_REVOCATION_TOMBSTONES_PER_CLIENT: usize =
999 4 * DEFAULT_MAX_REVOCATION_TOMBSTONES_PER_CLIENT;
1000pub const HARD_MAX_OAUTH_RETAINED_ENTRIES: usize = 256 * 1_024;
1006
1007#[derive(Debug, Clone)]
1013pub struct OAuthServerConfig {
1014 pub issuer: String,
1016 pub access_token_lifetime: Duration,
1018 pub refresh_token_lifetime: Duration,
1020 pub authorization_code_lifetime: Duration,
1022 pub allow_public_clients: bool,
1024 pub min_code_verifier_length: usize,
1026 pub max_code_verifier_length: usize,
1028 pub max_clients: usize,
1031 pub max_authorization_codes: usize,
1034 pub max_authorization_codes_per_client: usize,
1037 pub max_access_tokens: usize,
1040 pub max_access_tokens_per_client: usize,
1043 pub max_refresh_tokens: usize,
1046 pub max_refresh_tokens_per_client: usize,
1049 pub max_revocation_tombstones: usize,
1052 pub max_revocation_tombstones_per_client: usize,
1055}
1056
1057impl Default for OAuthServerConfig {
1058 fn default() -> Self {
1059 Self {
1060 issuer: "https://fastmcp.invalid/".to_string(),
1064 access_token_lifetime: Duration::from_mins(15),
1065 refresh_token_lifetime: Duration::from_hours(720),
1066 authorization_code_lifetime: Duration::from_mins(5),
1067 allow_public_clients: true,
1068 min_code_verifier_length: PKCE_CODE_VERIFIER_MIN_BYTES,
1069 max_code_verifier_length: PKCE_CODE_VERIFIER_MAX_BYTES,
1070 max_clients: DEFAULT_MAX_OAUTH_CLIENTS,
1071 max_authorization_codes: DEFAULT_MAX_AUTHORIZATION_CODES,
1072 max_authorization_codes_per_client: DEFAULT_MAX_AUTHORIZATION_CODES_PER_CLIENT,
1073 max_access_tokens: DEFAULT_MAX_ACCESS_TOKENS,
1074 max_access_tokens_per_client: DEFAULT_MAX_ACCESS_TOKENS_PER_CLIENT,
1075 max_refresh_tokens: DEFAULT_MAX_REFRESH_TOKENS,
1076 max_refresh_tokens_per_client: DEFAULT_MAX_REFRESH_TOKENS_PER_CLIENT,
1077 max_revocation_tombstones: DEFAULT_MAX_REVOCATION_TOMBSTONES,
1078 max_revocation_tombstones_per_client: DEFAULT_MAX_REVOCATION_TOMBSTONES_PER_CLIENT,
1079 }
1080 }
1081}
1082
1083impl OAuthServerConfig {
1084 pub fn validate(&self) -> Result<(), OAuthError> {
1092 validate_oauth_issuer(&self.issuer)?;
1093
1094 if !(PKCE_CODE_VERIFIER_MIN_BYTES..=PKCE_CODE_VERIFIER_MAX_BYTES)
1095 .contains(&self.min_code_verifier_length)
1096 || !(PKCE_CODE_VERIFIER_MIN_BYTES..=PKCE_CODE_VERIFIER_MAX_BYTES)
1097 .contains(&self.max_code_verifier_length)
1098 || self.min_code_verifier_length > self.max_code_verifier_length
1099 {
1100 return Err(OAuthError::ServerError(format!(
1101 "OAuth configuration PKCE verifier bounds must satisfy \
1102 {PKCE_CODE_VERIFIER_MIN_BYTES} <= min_code_verifier_length <= \
1103 max_code_verifier_length <= {PKCE_CODE_VERIFIER_MAX_BYTES}"
1104 )));
1105 }
1106
1107 for (field, limit, hard_limit) in [
1108 ("max_clients", self.max_clients, HARD_MAX_OAUTH_CLIENTS),
1109 (
1110 "max_authorization_codes",
1111 self.max_authorization_codes,
1112 HARD_MAX_AUTHORIZATION_CODES,
1113 ),
1114 (
1115 "max_authorization_codes_per_client",
1116 self.max_authorization_codes_per_client,
1117 HARD_MAX_AUTHORIZATION_CODES_PER_CLIENT,
1118 ),
1119 (
1120 "max_access_tokens",
1121 self.max_access_tokens,
1122 HARD_MAX_ACCESS_TOKENS,
1123 ),
1124 (
1125 "max_access_tokens_per_client",
1126 self.max_access_tokens_per_client,
1127 HARD_MAX_ACCESS_TOKENS_PER_CLIENT,
1128 ),
1129 (
1130 "max_refresh_tokens",
1131 self.max_refresh_tokens,
1132 HARD_MAX_REFRESH_TOKENS,
1133 ),
1134 (
1135 "max_refresh_tokens_per_client",
1136 self.max_refresh_tokens_per_client,
1137 HARD_MAX_REFRESH_TOKENS_PER_CLIENT,
1138 ),
1139 (
1140 "max_revocation_tombstones",
1141 self.max_revocation_tombstones,
1142 HARD_MAX_REVOCATION_TOMBSTONES,
1143 ),
1144 (
1145 "max_revocation_tombstones_per_client",
1146 self.max_revocation_tombstones_per_client,
1147 HARD_MAX_REVOCATION_TOMBSTONES_PER_CLIENT,
1148 ),
1149 ] {
1150 if !(1..=hard_limit).contains(&limit) {
1151 return Err(OAuthError::ServerError(format!(
1152 "OAuth configuration limit `{field}` must be between 1 and its hard ceiling \
1153 of {hard_limit}"
1154 )));
1155 }
1156 }
1157
1158 let retained_entries = self.checked_global_retention_limit()?;
1159 if retained_entries > HARD_MAX_OAUTH_RETAINED_ENTRIES {
1160 return Err(OAuthError::ServerError(format!(
1161 "OAuth aggregate retained-state limit {retained_entries} exceeds hard ceiling \
1162 {HARD_MAX_OAUTH_RETAINED_ENTRIES}"
1163 )));
1164 }
1165
1166 for (global_field, global_limit, per_client_field, per_client_limit) in [
1167 (
1168 "max_authorization_codes",
1169 self.max_authorization_codes,
1170 "max_authorization_codes_per_client",
1171 self.max_authorization_codes_per_client,
1172 ),
1173 (
1174 "max_access_tokens",
1175 self.max_access_tokens,
1176 "max_access_tokens_per_client",
1177 self.max_access_tokens_per_client,
1178 ),
1179 (
1180 "max_refresh_tokens",
1181 self.max_refresh_tokens,
1182 "max_refresh_tokens_per_client",
1183 self.max_refresh_tokens_per_client,
1184 ),
1185 (
1186 "max_revocation_tombstones",
1187 self.max_revocation_tombstones,
1188 "max_revocation_tombstones_per_client",
1189 self.max_revocation_tombstones_per_client,
1190 ),
1191 ] {
1192 if per_client_limit > global_limit {
1193 return Err(OAuthError::ServerError(format!(
1194 "OAuth configuration limit `{per_client_field}` must not exceed \
1195 `{global_field}`"
1196 )));
1197 }
1198 }
1199
1200 validate_lifetime(
1201 self.access_token_lifetime,
1202 MIN_OAUTH_CREDENTIAL_LIFETIME,
1203 MAX_ACCESS_TOKEN_LIFETIME,
1204 "access_token_lifetime",
1205 )?;
1206 validate_lifetime(
1207 self.refresh_token_lifetime,
1208 MIN_OAUTH_CREDENTIAL_LIFETIME,
1209 MAX_REFRESH_TOKEN_LIFETIME,
1210 "refresh_token_lifetime",
1211 )?;
1212 validate_lifetime(
1213 self.authorization_code_lifetime,
1214 MIN_OAUTH_CREDENTIAL_LIFETIME,
1215 MAX_AUTHORIZATION_CODE_LIFETIME,
1216 "authorization_code_lifetime",
1217 )?;
1218 if self.refresh_token_lifetime < self.access_token_lifetime {
1219 return Err(OAuthError::ServerError(
1220 "OAuth configuration `refresh_token_lifetime` must not be shorter than \
1221 `access_token_lifetime`"
1222 .to_string(),
1223 ));
1224 }
1225
1226 let now = Instant::now();
1227 checked_deadline(now, self.access_token_lifetime, "access_token_lifetime")?;
1228 checked_deadline(now, self.refresh_token_lifetime, "refresh_token_lifetime")?;
1229 checked_deadline(
1230 now,
1231 self.authorization_code_lifetime,
1232 "authorization_code_lifetime",
1233 )?;
1234 Ok(())
1235 }
1236
1237 fn checked_global_retention_limit(&self) -> Result<usize, OAuthError> {
1238 [
1239 self.max_clients,
1240 self.max_authorization_codes,
1241 self.max_access_tokens,
1242 self.max_refresh_tokens,
1243 self.max_revocation_tombstones,
1244 ]
1245 .into_iter()
1246 .try_fold(0_usize, |total, limit| {
1247 total.checked_add(limit).ok_or_else(|| {
1248 OAuthError::ServerError(
1249 "OAuth aggregate retained-state limit is not representable".to_string(),
1250 )
1251 })
1252 })
1253 }
1254}
1255
1256#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1262pub enum ClientType {
1263 Confidential,
1265 Public,
1267}
1268
1269#[derive(Zeroize, ZeroizeOnDrop)]
1274struct ClientSecret {
1275 bytes: Vec<u8>,
1276}
1277
1278impl ClientSecret {
1279 fn new(value: String) -> Self {
1280 Self {
1281 bytes: value.into_bytes(),
1282 }
1283 }
1284
1285 fn as_bytes(&self) -> &[u8] {
1286 &self.bytes
1287 }
1288}
1289
1290impl std::fmt::Debug for ClientSecret {
1291 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1292 f.write_str("ClientSecret([redacted])")
1293 }
1294}
1295
1296#[derive(Clone, Copy)]
1302struct ClientSecretVerifier {
1303 salt: [u8; CLIENT_SECRET_SALT_BYTES],
1304 digest: Sha256Digest,
1305}
1306
1307impl ClientSecretVerifier {
1308 fn create(secret: &ClientSecret) -> Result<Self, OAuthError> {
1309 let salt = draw_security_identifier()
1310 .map_err(|error| OAuthError::ServerError(error.to_string()))?;
1311 Self::create_with_salt(secret.as_bytes(), *salt.as_bytes())
1312 }
1313
1314 fn create_with_salt(
1315 secret: &[u8],
1316 salt: [u8; CLIENT_SECRET_SALT_BYTES],
1317 ) -> Result<Self, OAuthError> {
1318 let digest = client_secret_digest(&salt, secret)?;
1319 Ok(Self { salt, digest })
1320 }
1321
1322 fn dummy() -> Self {
1323 Self {
1324 salt: DUMMY_CLIENT_SECRET_SALT,
1325 digest: DUMMY_CLIENT_SECRET_DIGEST,
1326 }
1327 }
1328
1329 fn verify(&self, provided: &[u8]) -> bool {
1330 client_secret_digest(&self.salt, provided).is_ok_and(|provided_digest| {
1331 constant_time_digest_eq(self.digest.as_bytes(), provided_digest.as_bytes())
1332 })
1333 }
1334}
1335
1336impl std::fmt::Debug for ClientSecretVerifier {
1337 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1338 f.write_str("ClientSecretVerifier([redacted])")
1339 }
1340}
1341
1342pub struct OAuthClient {
1344 pub client_id: String,
1346 client_secret: Option<ClientSecret>,
1351 pub client_type: ClientType,
1353 pub redirect_uris: Vec<String>,
1355 pub allowed_scopes: HashSet<String>,
1357 pub name: Option<String>,
1359 pub description: Option<String>,
1361 pub registered_at: SystemTime,
1363}
1364
1365struct RegisteredOAuthClient {
1366 metadata: OAuthClientMetadata,
1367 secret_verifier: Option<ClientSecretVerifier>,
1368 registration_epoch: OAuthRegistrationEpoch,
1369}
1370
1371impl RegisteredOAuthClient {
1372 fn from_registration(
1373 client: OAuthClient,
1374 registration_epoch: OAuthRegistrationEpoch,
1375 ) -> Result<Self, OAuthError> {
1376 client.validate_for_retention()?;
1377 let secret_verifier = client
1378 .client_secret
1379 .as_ref()
1380 .map(ClientSecretVerifier::create)
1381 .transpose()?;
1382 let metadata = OAuthClientMetadata::from(&client);
1383 Ok(Self {
1384 metadata,
1385 secret_verifier,
1386 registration_epoch,
1387 })
1388 }
1389
1390 fn validate_redirect_uri(&self, uri: &str) -> bool {
1391 self.metadata.validate_redirect_uri(uri)
1392 }
1393
1394 fn validate_scopes(&self, scopes: &[String]) -> bool {
1395 self.metadata.validate_scopes(scopes)
1396 }
1397
1398 fn authenticate(&self, provided: Option<&str>) -> bool {
1399 match (self.secret_verifier.as_ref(), provided) {
1400 (Some(verifier), Some(secret)) => verifier.verify(secret.as_bytes()),
1401 (Some(verifier), None) => {
1402 std::hint::black_box(verifier.verify(std::hint::black_box(&[])));
1403 false
1404 }
1405 (None, None) => {
1406 perform_dummy_client_secret_verification(&[]);
1407 self.metadata.client_type == ClientType::Public
1408 }
1409 (None, Some(secret)) => {
1410 perform_dummy_client_secret_verification(secret.as_bytes());
1411 false
1412 }
1413 }
1414 }
1415}
1416
1417#[repr(transparent)]
1423#[derive(Clone, Copy, PartialEq, Eq)]
1424struct OAuthRegistrationEpoch([u8; OAUTH_REGISTRATION_EPOCH_BYTES]);
1425
1426impl OAuthRegistrationEpoch {
1427 fn draw() -> Result<Self, OAuthError> {
1428 let identifier = draw_security_identifier()
1429 .map_err(|error| OAuthError::ServerError(error.to_string()))?;
1430 Ok(Self(*identifier.as_bytes()))
1431 }
1432
1433 const fn as_bytes(&self) -> &[u8; OAUTH_REGISTRATION_EPOCH_BYTES] {
1434 &self.0
1435 }
1436}
1437
1438impl std::fmt::Debug for OAuthRegistrationEpoch {
1439 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1440 f.write_str("OAuthRegistrationEpoch([opaque; 32 bytes])")
1441 }
1442}
1443
1444#[derive(Clone, PartialEq, Eq)]
1451pub struct OAuthClientMetadata {
1452 pub client_id: String,
1454 pub client_type: ClientType,
1456 pub redirect_uris: Vec<String>,
1458 pub allowed_scopes: HashSet<String>,
1460 pub name: Option<String>,
1462 pub description: Option<String>,
1464 pub registered_at: SystemTime,
1466}
1467
1468impl std::fmt::Debug for OAuthClientMetadata {
1469 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1470 f.debug_struct("OAuthClientMetadata")
1471 .field("client_id_len", &self.client_id.len())
1472 .field("client_type", &self.client_type)
1473 .field("redirect_uri_count", &self.redirect_uris.len())
1474 .field("allowed_scope_count", &self.allowed_scopes.len())
1475 .field("name_present", &self.name.is_some())
1476 .field("description_present", &self.description.is_some())
1477 .finish_non_exhaustive()
1478 }
1479}
1480
1481impl From<&OAuthClient> for OAuthClientMetadata {
1482 fn from(client: &OAuthClient) -> Self {
1483 Self {
1484 client_id: client.client_id.clone(),
1485 client_type: client.client_type,
1486 redirect_uris: client.redirect_uris.clone(),
1487 allowed_scopes: client.allowed_scopes.clone(),
1488 name: client.name.clone(),
1489 description: client.description.clone(),
1490 registered_at: client.registered_at,
1491 }
1492 }
1493}
1494
1495impl From<&RegisteredOAuthClient> for OAuthClientMetadata {
1496 fn from(client: &RegisteredOAuthClient) -> Self {
1497 client.metadata.clone()
1498 }
1499}
1500
1501impl OAuthClientMetadata {
1502 #[must_use]
1504 pub fn validate_redirect_uri(&self, uri: &str) -> bool {
1505 validate_registered_redirect_uri(&self.redirect_uris, uri)
1506 }
1507
1508 #[must_use]
1510 pub fn validate_scopes(&self, scopes: &[String]) -> bool {
1511 validate_registered_scopes(&self.allowed_scopes, scopes)
1512 }
1513}
1514
1515impl std::fmt::Debug for OAuthClient {
1516 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1517 f.debug_struct("OAuthClient")
1518 .field("client_id_len", &self.client_id.len())
1519 .field("client_secret_present", &self.client_secret.is_some())
1520 .field("redirect_uri_count", &self.redirect_uris.len())
1521 .field("allowed_scope_count", &self.allowed_scopes.len())
1522 .field("name_present", &self.name.is_some())
1523 .field("description_present", &self.description.is_some())
1524 .finish_non_exhaustive()
1525 }
1526}
1527
1528impl OAuthClient {
1529 #[must_use]
1531 pub fn builder(client_id: impl Into<String>) -> OAuthClientBuilder {
1532 OAuthClientBuilder::new(client_id)
1533 }
1534
1535 fn validate_for_retention(&self) -> Result<(), OAuthError> {
1536 if self.client_id.is_empty()
1537 || self.client_id.len() > MAX_OAUTH_CLIENT_ID_BYTES
1538 || self.client_id.chars().any(char::is_control)
1539 {
1540 return Err(OAuthError::InvalidRequest(
1541 OAUTH_CLIENT_ID_RETENTION_ERROR.to_string(),
1542 ));
1543 }
1544
1545 if self.client_secret.as_ref().is_some_and(|credential| {
1546 credential.as_bytes().is_empty()
1547 || credential.as_bytes().len() > MAX_OAUTH_CLIENT_CREDENTIAL_BYTES
1548 }) {
1549 return Err(OAuthError::InvalidRequest(
1550 OAUTH_CLIENT_CREDENTIAL_RETENTION_ERROR.to_string(),
1551 ));
1552 }
1553
1554 let credential_class_is_consistent = matches!(
1555 (self.client_type, self.client_secret.is_some()),
1556 (ClientType::Public, false) | (ClientType::Confidential, true)
1557 );
1558 if !credential_class_is_consistent {
1559 return Err(OAuthError::InvalidRequest(
1560 OAUTH_CLIENT_CREDENTIAL_CLASS_ERROR.to_string(),
1561 ));
1562 }
1563
1564 if self.redirect_uris.is_empty() {
1565 return Err(OAuthError::InvalidRequest(
1566 OAUTH_CLIENT_REDIRECT_REQUIRED_ERROR.to_string(),
1567 ));
1568 }
1569 if self.redirect_uris.len() > MAX_OAUTH_REDIRECT_URIS_PER_CLIENT {
1570 return Err(OAuthError::InvalidRequest(
1571 OAUTH_CLIENT_REDIRECT_COUNT_ERROR.to_string(),
1572 ));
1573 }
1574 if self
1575 .redirect_uris
1576 .iter()
1577 .any(|uri| parse_redirect_uri(uri).is_none())
1578 {
1579 return Err(OAuthError::InvalidRequest(
1580 OAUTH_CLIENT_REDIRECT_VALUE_ERROR.to_string(),
1581 ));
1582 }
1583
1584 if self.allowed_scopes.len() > MAX_OAUTH_SCOPES_PER_CLIENT {
1585 return Err(OAuthError::InvalidRequest(
1586 OAUTH_CLIENT_SCOPE_COUNT_ERROR.to_string(),
1587 ));
1588 }
1589 if self
1590 .allowed_scopes
1591 .iter()
1592 .any(|scope| !is_valid_oauth_scope_token(scope))
1593 {
1594 return Err(OAuthError::InvalidRequest(
1595 OAUTH_CLIENT_SCOPE_VALUE_ERROR.to_string(),
1596 ));
1597 }
1598
1599 if self.name.as_ref().is_some_and(|name| {
1600 name.len() > MAX_OAUTH_CLIENT_NAME_BYTES || contains_unsafe_display_character(name)
1601 }) {
1602 return Err(OAuthError::InvalidRequest(
1603 OAUTH_CLIENT_NAME_RETENTION_ERROR.to_string(),
1604 ));
1605 }
1606 if self.description.as_ref().is_some_and(|description| {
1607 description.len() > MAX_OAUTH_CLIENT_DESCRIPTION_BYTES
1608 || contains_unsafe_display_character(description)
1609 }) {
1610 return Err(OAuthError::InvalidRequest(
1611 OAUTH_CLIENT_DESCRIPTION_RETENTION_ERROR.to_string(),
1612 ));
1613 }
1614
1615 Ok(())
1616 }
1617
1618 #[must_use]
1620 pub fn validate_redirect_uri(&self, uri: &str) -> bool {
1621 validate_registered_redirect_uri(&self.redirect_uris, uri)
1622 }
1623
1624 #[must_use]
1626 pub fn validate_scopes(&self, scopes: &[String]) -> bool {
1627 validate_registered_scopes(&self.allowed_scopes, scopes)
1628 }
1629
1630 #[must_use]
1632 pub fn authenticate(&self, secret: Option<&str>) -> bool {
1633 match (&self.client_secret, secret) {
1634 (Some(expected), Some(provided)) => {
1635 authenticate_client_secret(expected.as_bytes(), provided.as_bytes())
1636 }
1637 (None, None) => self.client_type == ClientType::Public,
1638 _ => false,
1639 }
1640 }
1641
1642 #[must_use]
1645 pub fn has_client_secret(&self) -> bool {
1646 self.client_secret.is_some()
1647 }
1648}
1649
1650pub struct OAuthClientBuilder {
1652 client_id: String,
1653 client_credential: Option<ClientSecret>,
1654 redirect_uris: Vec<String>,
1655 allowed_scopes: HashSet<String>,
1656 name: Option<String>,
1657 description: Option<String>,
1658}
1659
1660impl std::fmt::Debug for OAuthClientBuilder {
1661 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1662 f.debug_struct("OAuthClientBuilder")
1663 .field("client_id_len", &self.client_id.len())
1664 .field(
1665 "client_credential_present",
1666 &self.client_credential.is_some(),
1667 )
1668 .field("redirect_uri_count", &self.redirect_uris.len())
1669 .field("allowed_scope_count", &self.allowed_scopes.len())
1670 .field("name_present", &self.name.is_some())
1671 .field("description_present", &self.description.is_some())
1672 .finish()
1673 }
1674}
1675
1676impl OAuthClientBuilder {
1677 fn new(client_id: impl Into<String>) -> Self {
1679 Self {
1680 client_id: client_id.into(),
1681 client_credential: None,
1682 redirect_uris: Vec::new(),
1683 allowed_scopes: HashSet::new(),
1684 name: None,
1685 description: None,
1686 }
1687 }
1688
1689 #[must_use]
1691 pub fn secret(mut self, credential: impl Into<String>) -> Self {
1692 self.client_credential = Some(ClientSecret::new(credential.into()));
1693 self
1694 }
1695
1696 #[must_use]
1698 pub fn redirect_uri(mut self, uri: impl Into<String>) -> Self {
1699 self.redirect_uris.push(uri.into());
1700 self
1701 }
1702
1703 #[must_use]
1705 pub fn redirect_uris<I, S>(mut self, uris: I) -> Self
1706 where
1707 I: IntoIterator<Item = S>,
1708 S: Into<String>,
1709 {
1710 self.redirect_uris.extend(uris.into_iter().map(Into::into));
1711 self
1712 }
1713
1714 #[must_use]
1716 pub fn scope(mut self, scope: impl Into<String>) -> Self {
1717 self.allowed_scopes.insert(scope.into());
1718 self
1719 }
1720
1721 #[must_use]
1723 pub fn scopes<I, S>(mut self, scopes: I) -> Self
1724 where
1725 I: IntoIterator<Item = S>,
1726 S: Into<String>,
1727 {
1728 self.allowed_scopes
1729 .extend(scopes.into_iter().map(Into::into));
1730 self
1731 }
1732
1733 #[must_use]
1735 pub fn name(mut self, name: impl Into<String>) -> Self {
1736 self.name = Some(name.into());
1737 self
1738 }
1739
1740 #[must_use]
1742 pub fn description(mut self, description: impl Into<String>) -> Self {
1743 self.description = Some(description.into());
1744 self
1745 }
1746
1747 pub fn build(self) -> Result<OAuthClient, OAuthError> {
1754 let client_type = if self.client_credential.is_some() {
1755 ClientType::Confidential
1756 } else {
1757 ClientType::Public
1758 };
1759
1760 let client = OAuthClient {
1761 client_id: self.client_id,
1762 client_secret: self.client_credential,
1763 client_type,
1764 redirect_uris: self.redirect_uris,
1765 allowed_scopes: self.allowed_scopes,
1766 name: self.name,
1767 description: self.description,
1768 registered_at: SystemTime::now(),
1769 };
1770 client.validate_for_retention()?;
1771 Ok(client)
1772 }
1773}
1774
1775#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1781pub enum CodeChallengeMethod {
1782 Plain,
1787 S256,
1789}
1790
1791impl CodeChallengeMethod {
1792 #[must_use]
1794 pub fn parse(s: &str) -> Option<Self> {
1795 match s {
1796 "plain" => Some(Self::Plain),
1797 "S256" => Some(Self::S256),
1798 _ => None,
1799 }
1800 }
1801
1802 #[must_use]
1804 pub fn as_str(&self) -> &'static str {
1805 match self {
1806 Self::Plain => "plain",
1807 Self::S256 => "S256",
1808 }
1809 }
1810}
1811
1812#[derive(Clone)]
1818pub struct AuthorizationCode {
1819 pub client_id: String,
1821 pub redirect_uri: String,
1823 pub scopes: Vec<String>,
1825 pub resource: Option<String>,
1827 pub code_challenge: String,
1829 pub code_challenge_method: CodeChallengeMethod,
1831 pub issued_at: Instant,
1833 pub expires_at: Instant,
1835 pub subject: Option<String>,
1837 pub state: Option<String>,
1839 registration_epoch: OAuthRegistrationEpoch,
1841}
1842
1843impl std::fmt::Debug for AuthorizationCode {
1844 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1845 f.debug_struct("AuthorizationCode")
1846 .field("client_id_len", &self.client_id.len())
1847 .field("redirect_uri_len", &self.redirect_uri.len())
1848 .field("scope_count", &self.scopes.len())
1849 .field("resource_present", &self.resource.is_some())
1850 .field("code_challenge_len", &self.code_challenge.len())
1851 .field("subject_present", &self.subject.is_some())
1852 .field("state_present", &self.state.is_some())
1853 .finish_non_exhaustive()
1854 }
1855}
1856
1857impl AuthorizationCode {
1858 #[must_use]
1860 pub fn is_expired(&self) -> bool {
1861 Instant::now() >= self.expires_at
1862 }
1863
1864 #[must_use]
1866 pub fn validate_code_verifier(&self, verifier: &str) -> bool {
1867 if validate_pkce_code_verifier(verifier).is_err() {
1868 return false;
1869 }
1870
1871 match self.code_challenge_method {
1872 CodeChallengeMethod::Plain => false,
1873 CodeChallengeMethod::S256 => compute_s256_challenge(verifier)
1874 .is_ok_and(|computed| constant_time_eq(&self.code_challenge, &computed)),
1875 }
1876 }
1877}
1878
1879#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1885pub enum TokenType {
1886 Bearer,
1888}
1889
1890impl TokenType {
1891 #[must_use]
1893 pub fn as_str(&self) -> &'static str {
1894 match self {
1895 Self::Bearer => "bearer",
1896 }
1897 }
1898}
1899
1900#[repr(transparent)]
1906#[derive(Clone, Copy, PartialEq, Eq, Hash)]
1907pub struct OAuthGrantId([u8; 32]);
1908
1909impl OAuthGrantId {
1910 #[must_use]
1913 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
1914 Self(bytes)
1915 }
1916
1917 #[must_use]
1919 pub const fn as_bytes(&self) -> &[u8; 32] {
1920 &self.0
1921 }
1922}
1923
1924impl std::fmt::Debug for OAuthGrantId {
1925 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1926 f.write_str("OAuthGrantId([opaque; 32 bytes])")
1927 }
1928}
1929
1930#[derive(Clone)]
1936pub struct OAuthToken {
1937 pub token: String,
1943 pub token_type: TokenType,
1945 pub client_id: String,
1947 pub scopes: Vec<String>,
1949 pub resource: Option<String>,
1951 pub issued_at: Instant,
1953 pub expires_at: Instant,
1955 pub subject: Option<String>,
1957 pub is_refresh_token: bool,
1959}
1960
1961impl std::fmt::Debug for OAuthToken {
1962 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1963 f.debug_struct("OAuthToken")
1964 .field("client_id_len", &self.client_id.len())
1965 .field("scope_count", &self.scopes.len())
1966 .field("resource_present", &self.resource.is_some())
1967 .field("subject_present", &self.subject.is_some())
1968 .finish_non_exhaustive()
1969 }
1970}
1971
1972impl OAuthToken {
1973 #[must_use]
1975 pub fn is_expired(&self) -> bool {
1976 Instant::now() >= self.expires_at
1977 }
1978
1979 #[must_use]
1981 pub fn expires_in_secs(&self) -> u64 {
1982 self.expires_at
1983 .saturating_duration_since(Instant::now())
1984 .as_secs()
1985 }
1986}
1987
1988#[derive(Clone)]
1993pub(crate) struct StoredOAuthToken {
1994 metadata: OAuthToken,
1995 grant_id: OAuthGrantId,
1996 registration_epoch: OAuthRegistrationEpoch,
1997 family_expires_at: Instant,
1999}
2000
2001impl std::ops::Deref for StoredOAuthToken {
2002 type Target = OAuthToken;
2003
2004 fn deref(&self) -> &Self::Target {
2005 &self.metadata
2006 }
2007}
2008
2009#[derive(serde::Serialize)]
2014pub struct TokenResponse {
2015 pub access_token: String,
2017 pub token_type: String,
2019 pub expires_in: u64,
2021 #[serde(skip_serializing_if = "Option::is_none")]
2023 pub refresh_token: Option<String>,
2024 #[serde(skip_serializing_if = "Option::is_none")]
2026 pub scope: Option<String>,
2027}
2028
2029impl std::fmt::Debug for TokenResponse {
2030 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2031 f.debug_struct("TokenResponse")
2032 .field("access_token_len", &self.access_token.len())
2033 .field("token_type_len", &self.token_type.len())
2034 .field("refresh_token_present", &self.refresh_token.is_some())
2035 .field(
2036 "scope_count",
2037 &self
2038 .scope
2039 .as_deref()
2040 .map_or(0, |scope| scope.split_ascii_whitespace().count()),
2041 )
2042 .finish()
2043 }
2044}
2045
2046#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2057pub struct AuthorizationApprovalGeneration([u8; 32]);
2058
2059impl AuthorizationApprovalGeneration {
2060 #[must_use]
2062 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
2063 Self(bytes)
2064 }
2065}
2066
2067#[derive(Clone, PartialEq, Eq)]
2068struct AuthorizationApprovalBinding {
2069 client_id: String,
2070 redirect_uri: String,
2071 scopes: Vec<String>,
2072 resource: Option<String>,
2073 state: Option<String>,
2074 code_challenge: String,
2075 code_challenge_method: CodeChallengeMethod,
2076 registration_epoch: OAuthRegistrationEpoch,
2077}
2078
2079pub struct AuthorizationApprovalRequest {
2086 binding: AuthorizationApprovalBinding,
2087}
2088
2089impl AuthorizationApprovalRequest {
2090 #[must_use]
2092 pub fn client_id(&self) -> &str {
2093 &self.binding.client_id
2094 }
2095
2096 #[must_use]
2098 pub fn redirect_uri(&self) -> &str {
2099 &self.binding.redirect_uri
2100 }
2101
2102 #[must_use]
2104 pub fn scopes(&self) -> &[String] {
2105 &self.binding.scopes
2106 }
2107
2108 #[must_use]
2110 pub fn resource(&self) -> Option<&str> {
2111 self.binding.resource.as_deref()
2112 }
2113
2114 #[must_use]
2116 pub fn state(&self) -> Option<&str> {
2117 self.binding.state.as_deref()
2118 }
2119
2120 #[must_use]
2122 pub fn code_challenge(&self) -> &str {
2123 &self.binding.code_challenge
2124 }
2125
2126 #[must_use]
2128 pub const fn code_challenge_method(&self) -> CodeChallengeMethod {
2129 self.binding.code_challenge_method
2130 }
2131
2132 pub fn approve(
2139 &self,
2140 subject: String,
2141 approved_scopes: Vec<String>,
2142 approved_resource: Option<String>,
2143 generation: AuthorizationApprovalGeneration,
2144 ) -> Result<AuthorizationApprovalDecision, OAuthError> {
2145 validate_authorization_subject(&subject)?;
2146 Ok(AuthorizationApprovalDecision {
2147 binding: self.binding.clone(),
2148 subject,
2149 approved_scopes,
2150 approved_resource,
2151 generation,
2152 })
2153 }
2154}
2155
2156impl std::fmt::Debug for AuthorizationApprovalRequest {
2157 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2158 f.debug_struct("AuthorizationApprovalRequest")
2159 .field("client_id_len", &self.binding.client_id.len())
2160 .field("redirect_uri_len", &self.binding.redirect_uri.len())
2161 .field("scope_count", &self.binding.scopes.len())
2162 .field("resource_present", &self.binding.resource.is_some())
2163 .field("state_present", &self.binding.state.is_some())
2164 .field("code_challenge_len", &self.binding.code_challenge.len())
2165 .finish_non_exhaustive()
2166 }
2167}
2168
2169pub struct AuthorizationApprovalDecision {
2176 binding: AuthorizationApprovalBinding,
2177 subject: String,
2178 approved_scopes: Vec<String>,
2179 approved_resource: Option<String>,
2180 generation: AuthorizationApprovalGeneration,
2181}
2182
2183impl std::fmt::Debug for AuthorizationApprovalDecision {
2184 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2185 f.debug_struct("AuthorizationApprovalDecision")
2186 .field("subject_len", &self.subject.len())
2187 .field("scope_count", &self.approved_scopes.len())
2188 .field("resource_present", &self.approved_resource.is_some())
2189 .finish_non_exhaustive()
2190 }
2191}
2192
2193#[allow(
2195 clippy::large_enum_variant,
2196 reason = "the decision is a deliberate one-shot, non-cloneable approval capability payload"
2197)]
2198pub enum AuthorizationApprovalDisposition {
2199 Approved(AuthorizationApprovalDecision),
2201 Denied,
2203 Error,
2205 Cancelled,
2207}
2208
2209impl std::fmt::Debug for AuthorizationApprovalDisposition {
2210 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2211 match self {
2212 Self::Approved(_) => f.write_str("AuthorizationApprovalDisposition::Approved(..)"),
2213 Self::Denied => f.write_str("AuthorizationApprovalDisposition::Denied"),
2214 Self::Error => f.write_str("AuthorizationApprovalDisposition::Error"),
2215 Self::Cancelled => f.write_str("AuthorizationApprovalDisposition::Cancelled"),
2216 }
2217 }
2218}
2219
2220pub trait AuthorizationApprovalBackend: Send + Sync {
2226 fn generation(&self) -> AuthorizationApprovalGeneration;
2228
2229 fn approve(&self, request: &AuthorizationApprovalRequest) -> AuthorizationApprovalDisposition;
2231}
2232
2233struct DenyAllAuthorizationApprovalBackend;
2234
2235impl AuthorizationApprovalBackend for DenyAllAuthorizationApprovalBackend {
2236 fn generation(&self) -> AuthorizationApprovalGeneration {
2237 AuthorizationApprovalGeneration::from_bytes([0; 32])
2238 }
2239
2240 fn approve(&self, _request: &AuthorizationApprovalRequest) -> AuthorizationApprovalDisposition {
2241 AuthorizationApprovalDisposition::Denied
2242 }
2243}
2244
2245#[cfg(test)]
2246struct TestDefaultAuthorizationApprovalBackend;
2247
2248#[cfg(test)]
2249impl AuthorizationApprovalBackend for TestDefaultAuthorizationApprovalBackend {
2250 fn generation(&self) -> AuthorizationApprovalGeneration {
2251 AuthorizationApprovalGeneration::from_bytes([0x54; 32])
2252 }
2253
2254 fn approve(&self, request: &AuthorizationApprovalRequest) -> AuthorizationApprovalDisposition {
2255 AuthorizationApprovalDisposition::Approved(
2256 request
2257 .approve(
2258 "oauth-test-subject".to_string(),
2259 request.scopes().to_vec(),
2260 request.resource().map(str::to_string),
2261 self.generation(),
2262 )
2263 .expect("validated test approval request must produce a decision"),
2264 )
2265 }
2266}
2267
2268#[derive(Clone)]
2270pub struct AuthorizationRequest {
2271 pub response_type: String,
2273 pub client_id: String,
2275 pub redirect_uri: String,
2277 pub scopes: Vec<String>,
2279 pub resource: Option<String>,
2281 pub state: Option<String>,
2283 pub code_challenge: String,
2285 pub code_challenge_method: CodeChallengeMethod,
2287}
2288
2289impl std::fmt::Debug for AuthorizationRequest {
2290 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2291 f.debug_struct("AuthorizationRequest")
2292 .field("response_type_len", &self.response_type.len())
2293 .field("client_id_len", &self.client_id.len())
2294 .field("redirect_uri_len", &self.redirect_uri.len())
2295 .field("scope_count", &self.scopes.len())
2296 .field("resource_present", &self.resource.is_some())
2297 .field("state_present", &self.state.is_some())
2298 .field("code_challenge_len", &self.code_challenge.len())
2299 .finish()
2300 }
2301}
2302
2303pub struct TokenRequest {
2305 pub grant_type: String,
2307 pub code: Option<String>,
2309 pub redirect_uri: Option<String>,
2311 pub client_id: String,
2313 pub client_secret: Option<String>,
2315 pub code_verifier: Option<String>,
2317 pub refresh_token: Option<String>,
2319 pub scopes: Option<Vec<String>>,
2321 pub resource: Option<String>,
2324}
2325
2326impl std::fmt::Debug for TokenRequest {
2327 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2328 f.debug_struct("TokenRequest")
2329 .field("grant_type_len", &self.grant_type.len())
2330 .field("code_present", &self.code.is_some())
2331 .field("redirect_uri_present", &self.redirect_uri.is_some())
2332 .field("client_id_len", &self.client_id.len())
2333 .field("client_secret_present", &self.client_secret.is_some())
2334 .field("code_verifier_present", &self.code_verifier.is_some())
2335 .field("refresh_token_present", &self.refresh_token.is_some())
2336 .field("scope_count", &self.scopes.as_ref().map_or(0, Vec::len))
2337 .field("resource_present", &self.resource.is_some())
2338 .finish()
2339 }
2340}
2341
2342#[derive(Clone)]
2348pub enum OAuthError {
2349 InvalidRequest(String),
2351 InvalidClient(String),
2353 InvalidGrant(String),
2355 UnauthorizedClient(String),
2357 UnsupportedGrantType(String),
2359 InvalidScope(String),
2361 ServerError(String),
2363 TemporarilyUnavailable(String),
2365 AccessDenied(String),
2367 UnsupportedResponseType(String),
2369}
2370
2371impl std::fmt::Debug for OAuthError {
2372 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2373 let (variant, description) = match self {
2374 Self::InvalidRequest(description) => ("InvalidRequest", description),
2375 Self::InvalidClient(description) => ("InvalidClient", description),
2376 Self::InvalidGrant(description) => ("InvalidGrant", description),
2377 Self::UnauthorizedClient(description) => ("UnauthorizedClient", description),
2378 Self::UnsupportedGrantType(description) => ("UnsupportedGrantType", description),
2379 Self::InvalidScope(description) => ("InvalidScope", description),
2380 Self::ServerError(description) => ("ServerError", description),
2381 Self::TemporarilyUnavailable(description) => ("TemporarilyUnavailable", description),
2382 Self::AccessDenied(description) => ("AccessDenied", description),
2383 Self::UnsupportedResponseType(description) => ("UnsupportedResponseType", description),
2384 };
2385
2386 f.debug_struct(variant)
2387 .field("description_len", &description.len())
2388 .finish()
2389 }
2390}
2391
2392impl OAuthError {
2393 #[must_use]
2395 pub fn error_code(&self) -> &'static str {
2396 match self {
2397 Self::InvalidRequest(_) => "invalid_request",
2398 Self::InvalidClient(_) => "invalid_client",
2399 Self::InvalidGrant(_) => "invalid_grant",
2400 Self::UnauthorizedClient(_) => "unauthorized_client",
2401 Self::UnsupportedGrantType(_) => "unsupported_grant_type",
2402 Self::InvalidScope(_) => "invalid_scope",
2403 Self::ServerError(_) => "server_error",
2404 Self::TemporarilyUnavailable(_) => "temporarily_unavailable",
2405 Self::AccessDenied(_) => "access_denied",
2406 Self::UnsupportedResponseType(_) => "unsupported_response_type",
2407 }
2408 }
2409
2410 #[must_use]
2412 pub fn description(&self) -> &str {
2413 match self {
2414 Self::InvalidRequest(s)
2415 | Self::InvalidClient(s)
2416 | Self::InvalidGrant(s)
2417 | Self::UnauthorizedClient(s)
2418 | Self::UnsupportedGrantType(s)
2419 | Self::InvalidScope(s)
2420 | Self::ServerError(s)
2421 | Self::TemporarilyUnavailable(s)
2422 | Self::AccessDenied(s)
2423 | Self::UnsupportedResponseType(s) => s,
2424 }
2425 }
2426}
2427
2428impl std::fmt::Display for OAuthError {
2429 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2430 write!(f, "{}: {}", self.error_code(), self.description())
2431 }
2432}
2433
2434impl std::error::Error for OAuthError {}
2435
2436impl From<OAuthError> for McpError {
2437 fn from(err: OAuthError) -> Self {
2438 match &err {
2439 OAuthError::InvalidClient(_) | OAuthError::UnauthorizedClient(_) => {
2440 McpError::new(McpErrorCode::ResourceForbidden, err.to_string())
2441 }
2442 OAuthError::AccessDenied(_) => {
2443 McpError::new(McpErrorCode::ResourceForbidden, err.to_string())
2444 }
2445 _ => McpError::new(McpErrorCode::InvalidRequest, err.to_string()),
2446 }
2447 }
2448}
2449
2450#[derive(Clone, Copy, PartialEq, Eq, Hash)]
2455pub(crate) struct CredentialDigest(Sha256Digest);
2456
2457impl std::fmt::Debug for CredentialDigest {
2458 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2459 f.write_str("CredentialDigest([redacted; 32 bytes])")
2460 }
2461}
2462
2463#[derive(Clone, Copy)]
2464enum CredentialKind {
2465 AuthorizationCode,
2466 AccessToken,
2467 RefreshToken,
2468}
2469
2470impl CredentialKind {
2471 fn domain(self) -> &'static [u8] {
2472 match self {
2473 Self::AuthorizationCode => AUTHORIZATION_CODE_DIGEST_DOMAIN,
2474 Self::AccessToken => ACCESS_TOKEN_DIGEST_DOMAIN,
2475 Self::RefreshToken => REFRESH_TOKEN_DIGEST_DOMAIN,
2476 }
2477 }
2478}
2479
2480#[derive(Debug, Clone)]
2482pub(crate) struct RevocationTombstone {
2483 pub(crate) client_id: String,
2485 pub(crate) grant_id: OAuthGrantId,
2487 pub(crate) replay_guard: bool,
2490 pub(crate) expires_at: Instant,
2493}
2494
2495pub(crate) struct OAuthServerState {
2497 clients: HashMap<String, RegisteredOAuthClient>,
2499 pub(crate) authorization_codes: HashMap<CredentialDigest, AuthorizationCode>,
2501 pub(crate) access_tokens: HashMap<CredentialDigest, StoredOAuthToken>,
2503 pub(crate) refresh_tokens: HashMap<CredentialDigest, StoredOAuthToken>,
2505 pub(crate) revoked_tokens: HashMap<CredentialDigest, RevocationTombstone>,
2508}
2509
2510impl OAuthServerState {
2511 fn new() -> Self {
2512 Self {
2513 clients: HashMap::new(),
2514 authorization_codes: HashMap::new(),
2515 access_tokens: HashMap::new(),
2516 refresh_tokens: HashMap::new(),
2517 revoked_tokens: HashMap::new(),
2518 }
2519 }
2520
2521 fn cleanup_expired_at(&mut self, now: Instant) {
2522 self.authorization_codes
2523 .retain(|_, code| code.expires_at > now);
2524 self.access_tokens.retain(|_, token| token.expires_at > now);
2525 self.refresh_tokens
2526 .retain(|_, token| token.expires_at > now);
2527 self.revoked_tokens
2528 .retain(|_, tombstone| tombstone.expires_at > now);
2529 }
2530
2531 fn authorization_code_count_for_client(&self, client_id: &str) -> usize {
2532 self.authorization_codes
2533 .values()
2534 .filter(|code| code.client_id == client_id)
2535 .count()
2536 }
2537
2538 fn access_token_count_for_client(&self, client_id: &str) -> usize {
2539 self.access_tokens
2540 .values()
2541 .filter(|token| token.client_id == client_id)
2542 .count()
2543 }
2544
2545 fn refresh_token_count_for_client(&self, client_id: &str) -> usize {
2546 self.refresh_tokens
2547 .values()
2548 .filter(|token| token.client_id == client_id)
2549 .count()
2550 }
2551
2552 fn revocation_tombstone_count_for_client(&self, client_id: &str) -> usize {
2553 self.revoked_tokens
2554 .values()
2555 .filter(|tombstone| tombstone.client_id == client_id)
2556 .count()
2557 }
2558
2559 fn credential_value_in_use(&self, value: &str) -> bool {
2560 let authorization_code = digest_credential(CredentialKind::AuthorizationCode, value);
2561 let access_token = digest_credential(CredentialKind::AccessToken, value);
2562 let refresh_token = digest_credential(CredentialKind::RefreshToken, value);
2563
2564 authorization_code.is_ok_and(|digest| self.authorization_codes.contains_key(&digest))
2565 || access_token.is_ok_and(|digest| {
2566 self.access_tokens.contains_key(&digest)
2567 || self.revoked_tokens.contains_key(&digest)
2568 })
2569 || refresh_token.is_ok_and(|digest| {
2570 self.refresh_tokens.contains_key(&digest)
2571 || self.revoked_tokens.contains_key(&digest)
2572 })
2573 }
2574
2575 fn insert_tombstone_bounded(
2576 &mut self,
2577 token: CredentialDigest,
2578 tombstone: RevocationTombstone,
2579 config: &OAuthServerConfig,
2580 now: Instant,
2581 ) {
2582 if tombstone.expires_at <= now
2583 || config.max_revocation_tombstones == 0
2584 || config.max_revocation_tombstones_per_client == 0
2585 {
2586 return;
2587 }
2588
2589 if let Some(existing) = self.revoked_tokens.get_mut(&token) {
2590 existing.replay_guard |= tombstone.replay_guard;
2591 if tombstone.expires_at > existing.expires_at {
2592 existing.expires_at = tombstone.expires_at;
2593 }
2594 return;
2595 }
2596
2597 if self.revocation_tombstone_count_for_client(&tombstone.client_id)
2598 >= config.max_revocation_tombstones_per_client
2599 {
2600 let oldest_for_client = self
2601 .revoked_tokens
2602 .iter()
2603 .filter(|(_, entry)| entry.client_id == tombstone.client_id && !entry.replay_guard)
2604 .min_by_key(|(_, entry)| entry.expires_at)
2605 .map(|(value, _)| *value);
2606 if let Some(oldest) = oldest_for_client {
2607 self.revoked_tokens.remove(&oldest);
2608 } else {
2609 return;
2610 }
2611 }
2612
2613 if self.revoked_tokens.len() >= config.max_revocation_tombstones {
2614 let oldest = self
2615 .revoked_tokens
2616 .iter()
2617 .filter(|(_, entry)| !entry.replay_guard)
2618 .min_by_key(|(_, entry)| entry.expires_at)
2619 .map(|(value, _)| *value);
2620 if let Some(oldest) = oldest {
2621 self.revoked_tokens.remove(&oldest);
2622 } else {
2623 return;
2624 }
2625 }
2626
2627 self.revoked_tokens.insert(token, tombstone);
2628 }
2629
2630 fn ensure_replay_guard_capacity(
2631 &self,
2632 client_id: &str,
2633 config: &OAuthServerConfig,
2634 ) -> Result<(), OAuthError> {
2635 let client_count = self.revocation_tombstone_count_for_client(client_id);
2636 if client_count >= config.max_revocation_tombstones_per_client
2637 && !self
2638 .revoked_tokens
2639 .values()
2640 .any(|entry| entry.client_id == client_id && !entry.replay_guard)
2641 {
2642 return Err(capacity_error("refresh-token replay guards"));
2643 }
2644 if self.revoked_tokens.len() >= config.max_revocation_tombstones
2645 && !self
2646 .revoked_tokens
2647 .values()
2648 .any(|entry| !entry.replay_guard)
2649 {
2650 return Err(capacity_error("refresh-token replay guards"));
2651 }
2652 Ok(())
2653 }
2654
2655 fn align_replay_guards_with_family_deadline(
2656 &mut self,
2657 grant_id: OAuthGrantId,
2658 client_id: &str,
2659 family_expires_at: Instant,
2660 ) {
2661 for tombstone in self.revoked_tokens.values_mut().filter(|entry| {
2662 entry.replay_guard && entry.client_id == client_id && entry.grant_id == grant_id
2663 }) {
2664 tombstone.expires_at = family_expires_at;
2665 }
2666 }
2667
2668 fn revoke_grant_family(
2671 &mut self,
2672 grant_id: OAuthGrantId,
2673 client_id: &str,
2674 config: &OAuthServerConfig,
2675 now: Instant,
2676 ) {
2677 let access_tokens: Vec<_> = self
2678 .access_tokens
2679 .iter()
2680 .filter(|(_, token)| token.client_id == client_id && token.grant_id == grant_id)
2681 .map(|(digest, token)| (*digest, token.expires_at))
2682 .collect();
2683 let refresh_tokens: Vec<_> = self
2684 .refresh_tokens
2685 .iter()
2686 .filter(|(_, token)| token.client_id == client_id && token.grant_id == grant_id)
2687 .map(|(digest, token)| (*digest, token.expires_at))
2688 .collect();
2689
2690 self.revoked_tokens.retain(|_, tombstone| {
2694 !(tombstone.replay_guard
2695 && tombstone.client_id == client_id
2696 && tombstone.grant_id == grant_id)
2697 });
2698
2699 for (digest, expires_at) in access_tokens {
2700 self.access_tokens.remove(&digest);
2701 self.insert_tombstone_bounded(
2702 digest,
2703 RevocationTombstone {
2704 client_id: client_id.to_string(),
2705 grant_id,
2706 replay_guard: false,
2707 expires_at,
2708 },
2709 config,
2710 now,
2711 );
2712 }
2713 for (digest, expires_at) in refresh_tokens {
2714 self.refresh_tokens.remove(&digest);
2715 self.insert_tombstone_bounded(
2716 digest,
2717 RevocationTombstone {
2718 client_id: client_id.to_string(),
2719 grant_id,
2720 replay_guard: false,
2721 expires_at,
2722 },
2723 config,
2724 now,
2725 );
2726 }
2727 }
2728}
2729
2730struct PreparedTokenPair {
2731 access_value: String,
2732 access_digest: CredentialDigest,
2733 access_metadata: StoredOAuthToken,
2734 refresh_value: String,
2735 refresh_digest: CredentialDigest,
2736 refresh_metadata: StoredOAuthToken,
2737 access_lifetime_secs: u64,
2738}
2739
2740impl PreparedTokenPair {
2741 fn issued_at(&self) -> Instant {
2742 self.access_metadata.issued_at
2743 }
2744
2745 fn into_response_and_records(
2746 self,
2747 ) -> (
2748 TokenResponse,
2749 (CredentialDigest, StoredOAuthToken),
2750 (CredentialDigest, StoredOAuthToken),
2751 ) {
2752 let response = TokenResponse {
2753 access_token: self.access_value,
2754 token_type: self.access_metadata.token_type.as_str().to_string(),
2755 expires_in: self.access_lifetime_secs,
2756 refresh_token: Some(self.refresh_value),
2757 scope: if self.access_metadata.scopes.is_empty() {
2758 None
2759 } else {
2760 Some(self.access_metadata.scopes.join(" "))
2761 },
2762 };
2763 (
2764 response,
2765 (self.access_digest, self.access_metadata),
2766 (self.refresh_digest, self.refresh_metadata),
2767 )
2768 }
2769}
2770
2771pub struct OAuthServer {
2776 config: OAuthServerConfig,
2777 approval_backend: Arc<dyn AuthorizationApprovalBackend>,
2778 approval_generation: AuthorizationApprovalGeneration,
2779 pub(crate) state: RwLock<OAuthServerState>,
2780}
2781
2782impl OAuthServer {
2783 #[must_use]
2789 pub fn new(config: OAuthServerConfig) -> Self {
2790 Self::with_approval_backend(config, Arc::new(DenyAllAuthorizationApprovalBackend))
2791 }
2792
2793 #[must_use]
2799 pub fn with_approval_backend(
2800 config: OAuthServerConfig,
2801 approval_backend: Arc<dyn AuthorizationApprovalBackend>,
2802 ) -> Self {
2803 let approval_generation = approval_backend.generation();
2804 Self {
2805 config,
2806 approval_backend,
2807 approval_generation,
2808 state: RwLock::new(OAuthServerState::new()),
2809 }
2810 }
2811
2812 pub fn try_new(config: OAuthServerConfig) -> Result<Self, OAuthError> {
2820 config.validate()?;
2821 Ok(Self::new(config))
2822 }
2823
2824 pub fn try_with_approval_backend(
2826 config: OAuthServerConfig,
2827 approval_backend: Arc<dyn AuthorizationApprovalBackend>,
2828 ) -> Result<Self, OAuthError> {
2829 config.validate()?;
2830 Ok(Self::with_approval_backend(config, approval_backend))
2831 }
2832
2833 #[must_use]
2835 pub fn with_defaults() -> Self {
2836 #[cfg(test)]
2837 {
2838 Self::with_approval_backend(
2839 OAuthServerConfig::default(),
2840 Arc::new(TestDefaultAuthorizationApprovalBackend),
2841 )
2842 }
2843 #[cfg(not(test))]
2844 {
2845 Self::new(OAuthServerConfig::default())
2846 }
2847 }
2848
2849 #[must_use]
2851 pub fn config(&self) -> &OAuthServerConfig {
2852 &self.config
2853 }
2854
2855 fn state_for_mutation(
2856 &self,
2857 ) -> Result<(std::sync::RwLockWriteGuard<'_, OAuthServerState>, Instant), OAuthError> {
2858 self.config.validate()?;
2859 let mut state = self
2860 .state
2861 .write()
2862 .map_err(|_| OAuthError::ServerError("failed to acquire write lock".to_string()))?;
2863 let now = Instant::now();
2866 state.cleanup_expired_at(now);
2867 Ok((state, now))
2868 }
2869
2870 fn state_for_resource_checked_mutation<F>(
2875 &self,
2876 resource_matches: F,
2877 ) -> Result<(std::sync::RwLockWriteGuard<'_, OAuthServerState>, Instant), OAuthError>
2878 where
2879 F: FnOnce(&OAuthServerState) -> bool,
2880 {
2881 self.config.validate()?;
2882 let mut state = self
2883 .state
2884 .write()
2885 .map_err(|_| OAuthError::ServerError("failed to acquire write lock".to_string()))?;
2886 if !resource_matches(&state) {
2887 return Err(invalid_grant_error());
2888 }
2889 let now = Instant::now();
2892 state.cleanup_expired_at(now);
2893 Ok((state, now))
2894 }
2895
2896 pub fn register_client(&self, client: OAuthClient) -> Result<(), OAuthError> {
2911 client.validate_for_retention()?;
2915
2916 if client.client_type == ClientType::Public && !self.config.allow_public_clients {
2917 return Err(OAuthError::InvalidClient(
2918 "public clients are not allowed".to_string(),
2919 ));
2920 }
2921
2922 let registration_epoch = OAuthRegistrationEpoch::draw()?;
2923 let client = RegisteredOAuthClient::from_registration(client, registration_epoch)?;
2924 let client_id = client.metadata.client_id.clone();
2925
2926 let (mut state, _) = self.state_for_mutation()?;
2927
2928 if state.clients.contains_key(&client_id) {
2929 return Err(OAuthError::InvalidClient(
2930 "OAuth client_id is already registered".to_string(),
2931 ));
2932 }
2933
2934 if state.clients.len() >= self.config.max_clients {
2935 return Err(capacity_error("registered clients"));
2936 }
2937
2938 state.clients.insert(client_id, client);
2939 Ok(())
2940 }
2941
2942 pub fn unregister_client(&self, client_id: &str) -> Result<(), OAuthError> {
2946 validate_client_id_admission(client_id)?;
2947 let (mut state, _) = self.state_for_mutation()?;
2948
2949 if !state.clients.contains_key(client_id) {
2950 return Err(OAuthError::InvalidClient(
2951 OAUTH_CLIENT_NOT_FOUND_ERROR.to_string(),
2952 ));
2953 }
2954
2955 state.clients.remove(client_id);
2956 state
2957 .authorization_codes
2958 .retain(|_, code| code.client_id != client_id);
2959 state
2960 .access_tokens
2961 .retain(|_, token| token.client_id != client_id);
2962 state
2963 .refresh_tokens
2964 .retain(|_, token| token.client_id != client_id);
2965 state
2969 .revoked_tokens
2970 .retain(|_, tombstone| tombstone.client_id != client_id);
2971
2972 Ok(())
2973 }
2974
2975 #[must_use]
2980 pub fn get_client(&self, client_id: &str) -> Option<OAuthClientMetadata> {
2981 if validate_client_id_admission(client_id).is_err() {
2982 return None;
2983 }
2984 self.state
2985 .read()
2986 .ok()
2987 .and_then(|s| s.clients.get(client_id).map(OAuthClientMetadata::from))
2988 }
2989
2990 #[must_use]
2995 pub fn list_clients(&self) -> Vec<OAuthClientMetadata> {
2996 let mut clients: Vec<OAuthClientMetadata> = self
2997 .state
2998 .read()
2999 .map(|s| s.clients.values().map(OAuthClientMetadata::from).collect())
3000 .unwrap_or_default();
3001 clients.sort_unstable_by(|left, right| left.client_id.cmp(&right.client_id));
3002 clients
3003 }
3004
3005 pub fn authorize(
3016 &self,
3017 request: &AuthorizationRequest,
3018 ) -> Result<(String, String), OAuthError> {
3019 self.authorize_with_token_draw(request, draw_security_identifier)
3020 }
3021
3022 pub(crate) fn authorization_error_redirect(
3030 &self,
3031 request: &AuthorizationRequest,
3032 error: &OAuthError,
3033 ) -> Option<String> {
3034 if matches!(error, OAuthError::InvalidClient(_))
3035 || validate_client_id_admission(&request.client_id).is_err()
3036 || parse_redirect_uri(&request.redirect_uri).is_none()
3037 || validate_optional_authorization_value(
3038 request.state.as_deref(),
3039 MAX_OAUTH_STATE_BYTES,
3040 OAUTH_AUTHORIZATION_STATE_RETENTION_ERROR,
3041 )
3042 .is_err()
3043 {
3044 return None;
3045 }
3046
3047 let state = self.state.read().ok()?;
3048 let client = state.clients.get(&request.client_id)?;
3049 if !client.validate_redirect_uri(&request.redirect_uri) {
3050 return None;
3051 }
3052 drop(state);
3053
3054 let mut redirect = request.redirect_uri.clone();
3055 let separator = if redirect.contains('?') { '&' } else { '?' };
3056 redirect.push(separator);
3057 redirect.push_str("error=");
3058 redirect.push_str(error.error_code());
3059 if let Some(state) = &request.state {
3060 redirect.push_str("&state=");
3061 redirect.push_str(&url_encode(state));
3062 }
3063 redirect.push_str("&iss=");
3066 redirect.push_str(&url_encode(&self.config.issuer));
3067 Some(redirect)
3068 }
3069
3070 fn authorize_with_token_draw<F, E>(
3071 &self,
3072 request: &AuthorizationRequest,
3073 draw: F,
3074 ) -> Result<(String, String), OAuthError>
3075 where
3076 F: FnOnce() -> Result<SecurityIdentifier, E>,
3077 E: std::fmt::Display,
3078 {
3079 validate_client_id_admission(&request.client_id)?;
3080 if parse_redirect_uri(&request.redirect_uri).is_none() {
3081 return Err(OAuthError::InvalidRequest(
3082 "invalid redirect_uri".to_string(),
3083 ));
3084 }
3085 validate_optional_authorization_value(
3086 request.state.as_deref(),
3087 MAX_OAUTH_STATE_BYTES,
3088 OAUTH_AUTHORIZATION_STATE_RETENTION_ERROR,
3089 )?;
3090 validate_optional_authorization_resource(request.resource.as_deref())?;
3091 let canonical_scopes = canonicalize_request_scopes(&request.scopes)?;
3092
3093 if request.response_type != "code" {
3095 return Err(OAuthError::UnsupportedResponseType(
3096 "only 'code' response_type is supported".to_string(),
3097 ));
3098 }
3099
3100 let (client, approved_registration_epoch) = {
3103 let state = self
3104 .state
3105 .read()
3106 .map_err(|_| OAuthError::ServerError("failed to acquire read lock".to_string()))?;
3107 let registered = state.clients.get(&request.client_id).ok_or_else(|| {
3108 OAuthError::InvalidClient(OAUTH_CLIENT_NOT_FOUND_ERROR.to_string())
3109 })?;
3110 (
3111 OAuthClientMetadata::from(registered),
3112 registered.registration_epoch,
3113 )
3114 };
3115
3116 if !client.validate_redirect_uri(&request.redirect_uri) {
3118 return Err(OAuthError::InvalidRequest(
3119 "invalid redirect_uri".to_string(),
3120 ));
3121 }
3122
3123 if !client.validate_scopes(&canonical_scopes) {
3125 return Err(OAuthError::InvalidScope(
3126 "requested scope not allowed".to_string(),
3127 ));
3128 }
3129
3130 if request.code_challenge_method != CodeChallengeMethod::S256 {
3134 return Err(OAuthError::InvalidRequest(
3135 "code_challenge_method must be S256".to_string(),
3136 ));
3137 }
3138 validate_s256_code_challenge(&request.code_challenge)?;
3139 self.config.validate()?;
3140
3141 let approval_request = AuthorizationApprovalRequest {
3142 binding: AuthorizationApprovalBinding {
3143 client_id: request.client_id.clone(),
3144 redirect_uri: request.redirect_uri.clone(),
3145 scopes: canonical_scopes.clone(),
3146 resource: request.resource.clone(),
3147 state: request.state.clone(),
3148 code_challenge: request.code_challenge.clone(),
3149 code_challenge_method: request.code_challenge_method,
3150 registration_epoch: approved_registration_epoch,
3151 },
3152 };
3153 let approval = match self.approval_backend.approve(&approval_request) {
3154 AuthorizationApprovalDisposition::Approved(approval) => approval,
3155 AuthorizationApprovalDisposition::Denied => {
3156 return Err(OAuthError::AccessDenied(
3157 "authorization approval was denied".to_string(),
3158 ));
3159 }
3160 AuthorizationApprovalDisposition::Error => {
3161 return Err(OAuthError::TemporarilyUnavailable(
3162 "authorization approval backend failed".to_string(),
3163 ));
3164 }
3165 AuthorizationApprovalDisposition::Cancelled => {
3166 return Err(OAuthError::AccessDenied(
3167 "authorization approval was cancelled".to_string(),
3168 ));
3169 }
3170 };
3171 if approval.binding != approval_request.binding
3172 || approval.generation != self.approval_generation
3173 || approval.approved_scopes != canonical_scopes
3174 || approval.approved_resource != request.resource
3175 {
3176 return Err(OAuthError::AccessDenied(
3177 "authorization approval did not bind the admitted request".to_string(),
3178 ));
3179 }
3180
3181 let subject = approval.subject;
3185
3186 let code_value = generate_token_with_draw(draw)?;
3188 let code_digest = digest_credential(CredentialKind::AuthorizationCode, &code_value)?;
3189 {
3191 let (mut state, now) = self.state_for_mutation()?;
3192 let current_client = state.clients.get(&request.client_id).ok_or_else(|| {
3193 OAuthError::InvalidClient(OAUTH_CLIENT_NOT_FOUND_ERROR.to_string())
3194 })?;
3195 if current_client.registration_epoch != approved_registration_epoch {
3196 return Err(OAuthError::InvalidClient(
3197 "OAuth client registration changed during authorization".to_string(),
3198 ));
3199 }
3200 if !current_client.validate_redirect_uri(&request.redirect_uri) {
3201 return Err(OAuthError::InvalidRequest(
3202 "invalid redirect_uri".to_string(),
3203 ));
3204 }
3205 if !current_client.validate_scopes(&canonical_scopes) {
3206 return Err(OAuthError::InvalidScope(
3207 "requested scope not allowed".to_string(),
3208 ));
3209 }
3210 ensure_capacity(
3211 state.authorization_codes.len(),
3212 state.authorization_code_count_for_client(&request.client_id),
3213 self.config.max_authorization_codes,
3214 self.config.max_authorization_codes_per_client,
3215 "authorization codes",
3216 )?;
3217 if state.credential_value_in_use(&code_value) {
3218 return Err(OAuthError::ServerError(
3219 "generated OAuth credential collided with retained state".to_string(),
3220 ));
3221 }
3222 let expires_at = checked_deadline(
3223 now,
3224 self.config.authorization_code_lifetime,
3225 "authorization_code_lifetime",
3226 )?;
3227 let code = AuthorizationCode {
3228 client_id: request.client_id.clone(),
3229 redirect_uri: request.redirect_uri.clone(),
3230 scopes: canonical_scopes.clone(),
3231 resource: request.resource.clone(),
3232 code_challenge: request.code_challenge.clone(),
3233 code_challenge_method: request.code_challenge_method,
3234 issued_at: now,
3235 expires_at,
3236 subject: Some(subject),
3237 state: request.state.clone(),
3238 registration_epoch: approved_registration_epoch,
3239 };
3240 state.authorization_codes.insert(code_digest, code);
3241 }
3242
3243 let mut redirect = request.redirect_uri.clone();
3245 let separator = if redirect.contains('?') { '&' } else { '?' };
3246 redirect.push(separator);
3247 redirect.push_str("code=");
3248 redirect.push_str(&url_encode(&code_value));
3249 if let Some(state) = &request.state {
3250 redirect.push_str("&state=");
3251 redirect.push_str(&url_encode(state));
3252 }
3253 redirect.push_str("&iss=");
3257 redirect.push_str(&url_encode(&self.config.issuer));
3258
3259 Ok((code_value, redirect))
3260 }
3261
3262 pub fn token(&self, request: &TokenRequest) -> Result<TokenResponse, OAuthError> {
3268 match request.grant_type.as_str() {
3269 "authorization_code" => self.token_authorization_code(request),
3270 "refresh_token" => self.token_refresh_token(request),
3271 _ => Err(OAuthError::UnsupportedGrantType(
3272 OAUTH_GRANT_TYPE_UNSUPPORTED_ERROR.to_string(),
3273 )),
3274 }
3275 }
3276
3277 fn token_authorization_code(
3278 &self,
3279 request: &TokenRequest,
3280 ) -> Result<TokenResponse, OAuthError> {
3281 self.token_authorization_code_with_draw(request, draw_security_identifier)
3282 }
3283
3284 fn token_authorization_code_with_draw<F, E>(
3285 &self,
3286 request: &TokenRequest,
3287 mut draw: F,
3288 ) -> Result<TokenResponse, OAuthError>
3289 where
3290 F: FnMut() -> Result<SecurityIdentifier, E>,
3291 E: std::fmt::Display,
3292 {
3293 let code_value = request
3295 .code
3296 .as_ref()
3297 .ok_or_else(|| OAuthError::InvalidRequest("code is required".to_string()))?;
3298 let redirect_uri = request
3299 .redirect_uri
3300 .as_ref()
3301 .ok_or_else(|| OAuthError::InvalidRequest("redirect_uri is required".to_string()))?;
3302 let code_verifier = request.code_verifier.as_ref().ok_or_else(|| {
3303 OAuthError::InvalidRequest("code_verifier is required (PKCE)".to_string())
3304 })?;
3305
3306 validate_client_authentication_admission(
3307 &request.client_id,
3308 request.client_secret.as_deref(),
3309 )?;
3310 let code_digest = validate_and_digest_opaque_credential(
3311 CredentialKind::AuthorizationCode,
3312 code_value,
3313 OAUTH_INVALID_GRANT_ERROR,
3314 )?;
3315 if parse_redirect_uri(redirect_uri).is_none() {
3316 return Err(invalid_grant_error());
3317 }
3318 validate_optional_authorization_resource(request.resource.as_deref())
3319 .map_err(|_| invalid_grant_error())?;
3320
3321 validate_pkce_code_verifier(code_verifier).map_err(|_| invalid_grant_error())?;
3324 if code_verifier.len() < self.config.min_code_verifier_length
3325 || code_verifier.len() > self.config.max_code_verifier_length
3326 {
3327 return Err(invalid_grant_error());
3328 }
3329
3330 {
3335 let state = self
3336 .state
3337 .read()
3338 .map_err(|_| OAuthError::ServerError("failed to acquire read lock".to_string()))?;
3339 if state
3340 .authorization_codes
3341 .get(&code_digest)
3342 .is_some_and(|code| request.resource != code.resource)
3343 {
3344 return Err(invalid_grant_error());
3345 }
3346 }
3347
3348 let (mut state, now) = self.state_for_resource_checked_mutation(|state| {
3353 state
3354 .authorization_codes
3355 .get(&code_digest)
3356 .is_none_or(|code| request.resource == code.resource)
3357 })?;
3358 let current_registration_epoch = authenticate_client_or_dummy(
3359 &state,
3360 &request.client_id,
3361 request.client_secret.as_deref(),
3362 )?;
3363 let auth_code = state
3364 .authorization_codes
3365 .get(&code_digest)
3366 .cloned()
3367 .ok_or_else(invalid_grant_error)?;
3368
3369 if auth_code.expires_at <= now {
3370 return Err(invalid_grant_error());
3371 }
3372 if auth_code.client_id != request.client_id {
3373 return Err(invalid_grant_error());
3374 }
3375 if auth_code.registration_epoch != current_registration_epoch {
3376 return Err(invalid_grant_error());
3377 }
3378 if auth_code.redirect_uri != *redirect_uri {
3379 return Err(invalid_grant_error());
3380 }
3381 if request.resource != auth_code.resource {
3382 return Err(invalid_grant_error());
3383 }
3384 if auth_code.code_challenge_method != CodeChallengeMethod::S256
3385 || !auth_code.validate_code_verifier(code_verifier)
3386 {
3387 return Err(invalid_grant_error());
3388 }
3389
3390 ensure_capacity(
3391 state.access_tokens.len(),
3392 state.access_token_count_for_client(&auth_code.client_id),
3393 self.config.max_access_tokens,
3394 self.config.max_access_tokens_per_client,
3395 "access tokens",
3396 )?;
3397 ensure_capacity(
3398 state.refresh_tokens.len(),
3399 state.refresh_token_count_for_client(&auth_code.client_id),
3400 self.config.max_refresh_tokens,
3401 self.config.max_refresh_tokens_per_client,
3402 "refresh tokens",
3403 )?;
3404
3405 let prepared = self.prepare_token_pair_with_draw(
3406 &auth_code.client_id,
3407 &auth_code.scopes,
3408 auth_code.resource.as_deref(),
3409 auth_code.subject.as_deref(),
3410 current_registration_epoch,
3411 Some(derive_authorization_grant_id(code_digest)?),
3412 None,
3413 &mut draw,
3414 )?;
3415 if auth_code.expires_at <= prepared.issued_at() {
3416 return Err(invalid_grant_error());
3417 }
3418 ensure_fresh_token_pair(&state, &prepared)?;
3419 let (response, access, refresh) = prepared.into_response_and_records();
3420
3421 state
3422 .authorization_codes
3423 .remove(&code_digest)
3424 .ok_or_else(invalid_grant_error)?;
3425 state.access_tokens.insert(access.0, access.1);
3426 state.refresh_tokens.insert(refresh.0, refresh.1);
3427
3428 Ok(response)
3429 }
3430
3431 fn token_refresh_token(&self, request: &TokenRequest) -> Result<TokenResponse, OAuthError> {
3432 self.token_refresh_token_with_draw(request, draw_security_identifier)
3433 }
3434
3435 fn token_refresh_token_with_draw<F, E>(
3436 &self,
3437 request: &TokenRequest,
3438 mut draw: F,
3439 ) -> Result<TokenResponse, OAuthError>
3440 where
3441 F: FnMut() -> Result<SecurityIdentifier, E>,
3442 E: std::fmt::Display,
3443 {
3444 let refresh_value = request
3445 .refresh_token
3446 .as_ref()
3447 .ok_or_else(|| OAuthError::InvalidRequest("refresh_token is required".to_string()))?;
3448 validate_client_authentication_admission(
3449 &request.client_id,
3450 request.client_secret.as_deref(),
3451 )?;
3452 let refresh_digest = validate_and_digest_opaque_credential(
3453 CredentialKind::RefreshToken,
3454 refresh_value,
3455 OAUTH_INVALID_GRANT_ERROR,
3456 )?;
3457 validate_optional_authorization_resource(request.resource.as_deref())
3458 .map_err(|_| invalid_grant_error())?;
3459 {
3464 let state = self
3465 .state
3466 .read()
3467 .map_err(|_| OAuthError::ServerError("failed to acquire read lock".to_string()))?;
3468 if state
3469 .refresh_tokens
3470 .get(&refresh_digest)
3471 .is_some_and(|token| {
3472 request.resource.is_some() && request.resource != token.resource
3473 })
3474 {
3475 return Err(invalid_grant_error());
3476 }
3477 }
3478 let (mut state, now) = self.state_for_resource_checked_mutation(|state| {
3483 state
3484 .refresh_tokens
3485 .get(&refresh_digest)
3486 .is_none_or(|token| {
3487 request.resource.is_none() || request.resource == token.resource
3488 })
3489 })?;
3490 let current_registration_epoch = authenticate_client_or_dummy(
3491 &state,
3492 &request.client_id,
3493 request.client_secret.as_deref(),
3494 )?;
3495 if let Some(revoked) = state.revoked_tokens.get(&refresh_digest).cloned() {
3496 if revoked.client_id == request.client_id {
3497 state.revoke_grant_family(revoked.grant_id, &request.client_id, &self.config, now);
3498 }
3499 return Err(invalid_grant_error());
3500 }
3501
3502 let stored_refresh = state
3503 .refresh_tokens
3504 .get(&refresh_digest)
3505 .cloned()
3506 .ok_or_else(invalid_grant_error)?;
3507 if stored_refresh.client_id != request.client_id
3508 || stored_refresh.registration_epoch != current_registration_epoch
3509 || stored_refresh.expires_at <= now
3510 || stored_refresh.family_expires_at <= now
3511 {
3512 return Err(invalid_grant_error());
3513 }
3514 if request.resource.is_some() && request.resource != stored_refresh.resource {
3515 return Err(invalid_grant_error());
3516 }
3517
3518 let canonical_requested_scopes = request
3519 .scopes
3520 .as_deref()
3521 .map(canonicalize_request_scopes)
3522 .transpose()?;
3523
3524 let scopes = if let Some(requested) = canonical_requested_scopes {
3526 for scope in &requested {
3528 if !stored_refresh.scopes.contains(scope) {
3529 return Err(OAuthError::InvalidScope(
3530 "requested scope was not in original grant".to_string(),
3531 ));
3532 }
3533 }
3534 requested
3535 } else {
3536 stored_refresh.scopes.clone()
3537 };
3538
3539 ensure_capacity(
3540 state.access_tokens.len(),
3541 state.access_token_count_for_client(&request.client_id),
3542 self.config.max_access_tokens,
3543 self.config.max_access_tokens_per_client,
3544 "access tokens",
3545 )?;
3546 state.ensure_replay_guard_capacity(&request.client_id, &self.config)?;
3547
3548 let prepared = self.prepare_token_pair_with_draw(
3549 &request.client_id,
3550 &scopes,
3551 stored_refresh.resource.as_deref(),
3552 stored_refresh.subject.as_deref(),
3553 current_registration_epoch,
3554 Some(stored_refresh.grant_id),
3555 Some(stored_refresh.family_expires_at),
3556 &mut draw,
3557 )?;
3558 ensure_fresh_token_pair(&state, &prepared)?;
3559 let replacement_refresh_expiry = prepared.refresh_metadata.expires_at;
3560 let (response, access, refresh) = prepared.into_response_and_records();
3561
3562 let consumed = state
3563 .refresh_tokens
3564 .remove(&refresh_digest)
3565 .ok_or_else(invalid_grant_error)?;
3566 state.align_replay_guards_with_family_deadline(
3567 consumed.grant_id,
3568 &request.client_id,
3569 replacement_refresh_expiry,
3570 );
3571 state.insert_tombstone_bounded(
3572 refresh_digest,
3573 RevocationTombstone {
3574 client_id: consumed.client_id.clone(),
3575 grant_id: consumed.grant_id,
3576 replay_guard: true,
3577 expires_at: replacement_refresh_expiry,
3578 },
3579 &self.config,
3580 now,
3581 );
3582 state.access_tokens.insert(access.0, access.1);
3583 state.refresh_tokens.insert(refresh.0, refresh.1);
3584
3585 Ok(response)
3586 }
3587
3588 fn issue_tokens(
3589 &self,
3590 client_id: &str,
3591 scopes: &[String],
3592 subject: Option<&str>,
3593 ) -> Result<TokenResponse, OAuthError> {
3594 self.issue_tokens_with_draw(client_id, scopes, subject, draw_security_identifier)
3595 }
3596
3597 fn prepare_token_pair_with_draw<F, E>(
3598 &self,
3599 client_id: &str,
3600 scopes: &[String],
3601 resource: Option<&str>,
3602 subject: Option<&str>,
3603 registration_epoch: OAuthRegistrationEpoch,
3604 grant_id: Option<OAuthGrantId>,
3605 family_expires_at: Option<Instant>,
3606 mut draw: F,
3607 ) -> Result<PreparedTokenPair, OAuthError>
3608 where
3609 F: FnMut() -> Result<SecurityIdentifier, E>,
3610 E: std::fmt::Display,
3611 {
3612 let access_value = generate_token_with_draw(&mut draw)?;
3613 let refresh_value = generate_token_with_draw(&mut draw)?;
3614 let access_digest = digest_credential(CredentialKind::AccessToken, &access_value)?;
3615 let refresh_digest = digest_credential(CredentialKind::RefreshToken, &refresh_value)?;
3616 let issued_at = Instant::now();
3619 let grant_id = match grant_id {
3620 Some(grant_id) => grant_id,
3621 None => derive_direct_grant_id(access_digest, refresh_digest)?,
3622 };
3623 let family_expires_at = match family_expires_at {
3624 Some(expires_at) => expires_at,
3625 None => checked_deadline(
3626 issued_at,
3627 self.config.refresh_token_lifetime,
3628 "refresh_token_lifetime",
3629 )?,
3630 };
3631 if family_expires_at
3632 .saturating_duration_since(issued_at)
3633 .as_secs()
3634 == 0
3635 {
3636 return Err(invalid_grant_error());
3637 }
3638 let access_expires_at = checked_deadline(
3639 issued_at,
3640 self.config.access_token_lifetime,
3641 "access_token_lifetime",
3642 )?
3643 .min(family_expires_at);
3644 let access_lifetime_secs = access_expires_at
3645 .saturating_duration_since(issued_at)
3646 .as_secs();
3647 if access_lifetime_secs == 0 {
3648 return Err(invalid_grant_error());
3649 }
3650
3651 Ok(PreparedTokenPair {
3652 access_value,
3653 access_digest,
3654 access_metadata: StoredOAuthToken {
3655 metadata: OAuthToken {
3656 token: String::new(),
3657 token_type: TokenType::Bearer,
3658 client_id: client_id.to_string(),
3659 scopes: scopes.to_vec(),
3660 resource: resource.map(String::from),
3661 issued_at,
3662 expires_at: access_expires_at,
3663 subject: subject.map(String::from),
3664 is_refresh_token: false,
3665 },
3666 grant_id,
3667 registration_epoch,
3668 family_expires_at,
3669 },
3670 refresh_value,
3671 refresh_digest,
3672 refresh_metadata: StoredOAuthToken {
3673 metadata: OAuthToken {
3674 token: String::new(),
3675 token_type: TokenType::Bearer,
3676 client_id: client_id.to_string(),
3677 scopes: scopes.to_vec(),
3678 resource: resource.map(String::from),
3679 issued_at,
3680 expires_at: family_expires_at,
3681 subject: subject.map(String::from),
3682 is_refresh_token: true,
3683 },
3684 grant_id,
3685 registration_epoch,
3686 family_expires_at,
3687 },
3688 access_lifetime_secs,
3689 })
3690 }
3691
3692 fn issue_tokens_with_draw<F, E>(
3693 &self,
3694 client_id: &str,
3695 scopes: &[String],
3696 subject: Option<&str>,
3697 mut draw: F,
3698 ) -> Result<TokenResponse, OAuthError>
3699 where
3700 F: FnMut() -> Result<SecurityIdentifier, E>,
3701 E: std::fmt::Display,
3702 {
3703 validate_optional_authorization_subject(subject)?;
3704 let scopes = canonicalize_request_scopes(scopes)?;
3705 let (mut state, _) = self.state_for_mutation()?;
3706 let registration_epoch = state
3707 .clients
3708 .get(client_id)
3709 .map(|client| client.registration_epoch)
3710 .ok_or_else(|| OAuthError::InvalidClient(OAUTH_CLIENT_NOT_FOUND_ERROR.to_string()))?;
3711 ensure_capacity(
3712 state.access_tokens.len(),
3713 state.access_token_count_for_client(client_id),
3714 self.config.max_access_tokens,
3715 self.config.max_access_tokens_per_client,
3716 "access tokens",
3717 )?;
3718 ensure_capacity(
3719 state.refresh_tokens.len(),
3720 state.refresh_token_count_for_client(client_id),
3721 self.config.max_refresh_tokens,
3722 self.config.max_refresh_tokens_per_client,
3723 "refresh tokens",
3724 )?;
3725
3726 let prepared = self.prepare_token_pair_with_draw(
3727 client_id,
3728 &scopes,
3729 None,
3730 subject,
3731 registration_epoch,
3732 None,
3733 None,
3734 &mut draw,
3735 )?;
3736 ensure_fresh_token_pair(&state, &prepared)?;
3737 let (response, access, refresh) = prepared.into_response_and_records();
3738 state.access_tokens.insert(access.0, access.1);
3739 state.refresh_tokens.insert(refresh.0, refresh.1);
3740 Ok(response)
3741 }
3742
3743 pub fn revoke(
3751 &self,
3752 token: &str,
3753 client_id: &str,
3754 client_secret: Option<&str>,
3755 ) -> Result<(), OAuthError> {
3756 validate_client_authentication_admission(client_id, client_secret)?;
3757 if token.len() > OAUTH_OPAQUE_CREDENTIAL_BYTES || token.chars().any(char::is_control) {
3758 return Err(OAuthError::InvalidRequest(
3759 "revocation token is outside admitted bounds".to_string(),
3760 ));
3761 }
3762 let admitted_token = validate_opaque_credential(token, "revocation token is invalid")
3763 .is_ok()
3764 .then(|| {
3765 Ok::<_, OAuthError>((
3766 digest_credential(CredentialKind::AccessToken, token)?,
3767 digest_credential(CredentialKind::RefreshToken, token)?,
3768 ))
3769 })
3770 .transpose()?;
3771 let (mut state, now) = self.state_for_mutation()?;
3772
3773 authenticate_client_or_dummy(&state, client_id, client_secret)?;
3777 let Some((access_digest, refresh_digest)) = admitted_token else {
3778 return Ok(());
3779 };
3780
3781 let access = state.access_tokens.get(&access_digest).cloned();
3782 let refresh = state.refresh_tokens.get(&refresh_digest).cloned();
3783 let refresh_tombstone = state.revoked_tokens.get(&refresh_digest).cloned();
3784 let access_owner = access.as_ref().map(|entry| &entry.client_id);
3785 let refresh_owner = refresh.as_ref().map(|entry| &entry.client_id);
3786 let refresh_tombstone_owner = refresh_tombstone.as_ref().map(|entry| &entry.client_id);
3787 if access_owner.is_some_and(|owner| owner != client_id)
3788 || refresh_owner.is_some_and(|owner| owner != client_id)
3789 || refresh_tombstone_owner.is_some_and(|owner| owner != client_id)
3790 {
3791 return Ok(());
3794 }
3795
3796 if let Some(access) = state.access_tokens.remove(&access_digest) {
3797 state.insert_tombstone_bounded(
3798 access_digest,
3799 RevocationTombstone {
3800 client_id: client_id.to_string(),
3801 grant_id: access.grant_id,
3802 replay_guard: false,
3803 expires_at: access.expires_at,
3804 },
3805 &self.config,
3806 now,
3807 );
3808 }
3809 let refresh_grant_id = refresh
3810 .as_ref()
3811 .map(|entry| entry.grant_id)
3812 .or_else(|| refresh_tombstone.map(|entry| entry.grant_id));
3813 if let Some(grant_id) = refresh_grant_id {
3814 state.revoke_grant_family(grant_id, client_id, &self.config, now);
3815 }
3816
3817 Ok(())
3818 }
3819
3820 pub fn validate_access_token(&self, token: &str) -> Option<OAuthToken> {
3828 self.validate_stored_access_token(token)
3829 .map(|stored| stored.metadata)
3830 }
3831
3832 fn validate_stored_access_token(&self, token: &str) -> Option<StoredOAuthToken> {
3833 let token_digest = validate_and_digest_opaque_credential(
3834 CredentialKind::AccessToken,
3835 token,
3836 "access token is invalid",
3837 )
3838 .ok()?;
3839 let state = self.state.read().ok()?;
3840
3841 if state.revoked_tokens.contains_key(&token_digest) {
3843 return None;
3844 }
3845
3846 let token_info = state.access_tokens.get(&token_digest)?;
3847
3848 if token_info.is_expired() || token_info.family_expires_at <= Instant::now() {
3849 return None;
3850 }
3851
3852 let current_client = state.clients.get(&token_info.client_id)?;
3853 if current_client.registration_epoch != token_info.registration_epoch {
3854 return None;
3855 }
3856
3857 Some(token_info.clone())
3858 }
3859
3860 #[must_use]
3866 pub fn token_verifier(self: &Arc<Self>) -> OAuthTokenVerifier {
3867 OAuthTokenVerifier {
3868 server: Arc::clone(self),
3869 }
3870 }
3871
3872 pub fn cleanup_expired(&self) {
3881 let Ok(mut state) = self.state.write() else {
3882 return;
3883 };
3884
3885 state.cleanup_expired_at(Instant::now());
3886 }
3887
3888 #[must_use]
3890 pub fn stats(&self) -> OAuthServerStats {
3891 let state = match self.state.read() {
3892 Ok(guard) => guard,
3893 Err(poisoned) => poisoned.into_inner(),
3895 };
3896 OAuthServerStats {
3897 clients: state.clients.len(),
3898 authorization_codes: state.authorization_codes.len(),
3899 access_tokens: state.access_tokens.len(),
3900 refresh_tokens: state.refresh_tokens.len(),
3901 revoked_tokens: state.revoked_tokens.len(),
3902 }
3903 }
3904}
3905
3906#[derive(Debug, Clone, Default)]
3908pub struct OAuthServerStats {
3909 pub clients: usize,
3911 pub authorization_codes: usize,
3913 pub access_tokens: usize,
3915 pub refresh_tokens: usize,
3917 pub revoked_tokens: usize,
3919}
3920
3921pub struct OAuthTokenVerifier {
3930 server: Arc<OAuthServer>,
3931}
3932
3933impl TokenVerifier for OAuthTokenVerifier {
3934 fn verify(
3935 &self,
3936 _ctx: &McpContext,
3937 _request: AuthRequest<'_>,
3938 token: &AccessToken,
3939 ) -> McpResult<AuthContext> {
3940 if !token.scheme.eq_ignore_ascii_case("Bearer") {
3942 return Err(McpError::new(
3943 McpErrorCode::ResourceForbidden,
3944 "unsupported auth scheme",
3945 ));
3946 }
3947
3948 let stored_token = self
3950 .server
3951 .validate_stored_access_token(&token.token)
3952 .ok_or_else(|| {
3953 McpError::new(McpErrorCode::ResourceForbidden, "invalid or expired token")
3954 })?;
3955 let registration_epoch = stored_token.registration_epoch;
3956 let token_info = stored_token.metadata;
3957 let OAuthToken {
3958 client_id,
3959 scopes,
3960 resource,
3961 subject,
3962 ..
3963 } = token_info;
3964 let session_owner = oauth_session_owner(
3965 &self.server.config.issuer,
3966 &client_id,
3967 registration_epoch,
3968 subject.as_deref(),
3969 )?;
3970 let display_subject = subject
3971 .clone()
3972 .filter(|subject| !subject.is_empty())
3973 .unwrap_or_else(|| client_id.clone());
3974
3975 let mut auth = AuthContext::with_subject(display_subject);
3976 auth.scopes = scopes;
3977 auth.claims = Some(serde_json::json!({
3978 "client_id": client_id,
3979 "grant_subject": subject,
3980 "resource": resource,
3981 "iss": self.server.config.issuer,
3982 }));
3983 Ok(auth.with_session_owner(session_owner))
3984 }
3985}
3986
3987fn oauth_session_owner(
3992 issuer: &str,
3993 client_id: &str,
3994 registration_epoch: OAuthRegistrationEpoch,
3995 subject: Option<&str>,
3996) -> McpResult<Sha256Digest> {
3997 if issuer.len() > MAX_OAUTH_ISSUER_BYTES
3998 || client_id.is_empty()
3999 || client_id.len() > MAX_OAUTH_CLIENT_ID_BYTES
4000 || subject
4001 .is_some_and(|subject| subject.is_empty() || subject.len() > MAX_OAUTH_SUBJECT_BYTES)
4002 {
4003 return Err(McpError::internal_error(
4004 "OAuth session owner facts are outside admitted bounds",
4005 ));
4006 }
4007
4008 let subject_bytes = subject.map_or(0, str::len);
4009 let capacity = OAUTH_SESSION_OWNER_DOMAIN
4010 .len()
4011 .checked_add(8)
4012 .and_then(|size| size.checked_add(issuer.len()))
4013 .and_then(|size| size.checked_add(8))
4014 .and_then(|size| size.checked_add(client_id.len()))
4015 .and_then(|size| size.checked_add(OAUTH_REGISTRATION_EPOCH_BYTES))
4016 .and_then(|size| size.checked_add(1))
4017 .and_then(|size| size.checked_add(8))
4018 .and_then(|size| size.checked_add(subject_bytes))
4019 .filter(|size| *size <= MAX_OAUTH_SESSION_OWNER_INPUT_BYTES)
4020 .ok_or_else(|| McpError::internal_error("OAuth session owner framing overflow"))?;
4021 let mut framed = Vec::new();
4022 framed
4023 .try_reserve_exact(capacity)
4024 .map_err(|_| McpError::internal_error("OAuth session owner allocation failed"))?;
4025 framed.extend_from_slice(OAUTH_SESSION_OWNER_DOMAIN);
4026 framed.extend_from_slice(
4027 &u64::try_from(issuer.len())
4028 .map_err(|_| McpError::internal_error("OAuth issuer length overflow"))?
4029 .to_be_bytes(),
4030 );
4031 framed.extend_from_slice(issuer.as_bytes());
4032 framed.extend_from_slice(
4033 &u64::try_from(client_id.len())
4034 .map_err(|_| McpError::internal_error("OAuth client ID length overflow"))?
4035 .to_be_bytes(),
4036 );
4037 framed.extend_from_slice(client_id.as_bytes());
4038 framed.extend_from_slice(registration_epoch.as_bytes());
4039 match subject {
4040 None => framed.push(0),
4041 Some(subject) => {
4042 framed.push(1);
4043 framed.extend_from_slice(
4044 &u64::try_from(subject.len())
4045 .map_err(|_| McpError::internal_error("OAuth subject length overflow"))?
4046 .to_be_bytes(),
4047 );
4048 framed.extend_from_slice(subject.as_bytes());
4049 }
4050 }
4051
4052 sha256_bounded(&framed, MAX_OAUTH_SESSION_OWNER_INPUT_BYTES)
4053 .map_err(|_| McpError::internal_error("OAuth session owner derivation failed"))
4054}
4055
4056fn checked_deadline(
4057 now: Instant,
4058 lifetime: Duration,
4059 field: &'static str,
4060) -> Result<Instant, OAuthError> {
4061 now.checked_add(lifetime).ok_or_else(|| {
4062 OAuthError::ServerError(format!(
4063 "OAuth configuration lifetime `{field}` exceeds monotonic-clock range"
4064 ))
4065 })
4066}
4067
4068fn validate_lifetime(
4069 lifetime: Duration,
4070 minimum: Duration,
4071 maximum: Duration,
4072 field: &'static str,
4073) -> Result<(), OAuthError> {
4074 if lifetime < minimum || lifetime > maximum {
4075 return Err(OAuthError::ServerError(format!(
4076 "OAuth configuration lifetime `{field}` must be at least {} seconds and no greater \
4077 than {} seconds",
4078 minimum.as_secs(),
4079 maximum.as_secs()
4080 )));
4081 }
4082 Ok(())
4083}
4084
4085fn capacity_error(resource: &'static str) -> OAuthError {
4086 OAuthError::TemporarilyUnavailable(format!("OAuth {resource} capacity has been reached"))
4087}
4088
4089fn invalid_grant_error() -> OAuthError {
4090 OAuthError::InvalidGrant(OAUTH_INVALID_GRANT_ERROR.to_string())
4091}
4092
4093fn ensure_capacity(
4094 global_count: usize,
4095 client_count: usize,
4096 global_limit: usize,
4097 client_limit: usize,
4098 resource: &'static str,
4099) -> Result<(), OAuthError> {
4100 if global_count >= global_limit || client_count >= client_limit {
4101 return Err(capacity_error(resource));
4102 }
4103 Ok(())
4104}
4105
4106fn validate_optional_authorization_value(
4107 value: Option<&str>,
4108 max_bytes: usize,
4109 error: &'static str,
4110) -> Result<(), OAuthError> {
4111 if value.is_some_and(|value| value.len() > max_bytes || value.chars().any(char::is_control)) {
4112 return Err(OAuthError::InvalidRequest(error.to_string()));
4113 }
4114 Ok(())
4115}
4116
4117fn validate_optional_authorization_subject(subject: Option<&str>) -> Result<(), OAuthError> {
4118 if subject.is_some_and(str::is_empty) {
4119 return Err(OAuthError::InvalidRequest(
4120 OAUTH_AUTHORIZATION_SUBJECT_RETENTION_ERROR.to_string(),
4121 ));
4122 }
4123 validate_optional_authorization_value(
4124 subject,
4125 MAX_OAUTH_SUBJECT_BYTES,
4126 OAUTH_AUTHORIZATION_SUBJECT_RETENTION_ERROR,
4127 )
4128}
4129
4130fn validate_authorization_subject(subject: &str) -> Result<(), OAuthError> {
4131 if subject.is_empty() {
4132 return Err(OAuthError::InvalidRequest(
4133 OAUTH_AUTHORIZATION_SUBJECT_RETENTION_ERROR.to_string(),
4134 ));
4135 }
4136 validate_optional_authorization_value(
4137 Some(subject),
4138 MAX_OAUTH_SUBJECT_BYTES,
4139 OAUTH_AUTHORIZATION_SUBJECT_RETENTION_ERROR,
4140 )
4141}
4142
4143fn validate_optional_authorization_resource(resource: Option<&str>) -> Result<(), OAuthError> {
4144 validate_optional_authorization_value(
4145 resource,
4146 MAX_OAUTH_RESOURCE_BYTES,
4147 OAUTH_AUTHORIZATION_RESOURCE_RETENTION_ERROR,
4148 )?;
4149 if let Some(resource) = resource {
4150 let url = Url::parse(resource).map_err(|_| {
4151 OAuthError::InvalidRequest(OAUTH_AUTHORIZATION_RESOURCE_RETENTION_ERROR.to_string())
4152 })?;
4153 if url.cannot_be_a_base() || url.has_authority() && url.host().is_none() {
4154 return Err(OAuthError::InvalidRequest(
4155 OAUTH_AUTHORIZATION_RESOURCE_RETENTION_ERROR.to_string(),
4156 ));
4157 }
4158 }
4159 Ok(())
4160}
4161
4162fn is_valid_oauth_scope_token(scope: &str) -> bool {
4163 !scope.is_empty()
4164 && scope.len() <= MAX_OAUTH_SCOPE_BYTES
4165 && scope
4166 .bytes()
4167 .all(|byte| matches!(byte, 0x21 | 0x23..=0x5B | 0x5D..=0x7E))
4168}
4169
4170fn canonicalize_request_scopes(scopes: &[String]) -> Result<Vec<String>, OAuthError> {
4171 if scopes.len() > MAX_OAUTH_SCOPES_PER_CLIENT {
4172 return Err(OAuthError::InvalidScope(
4173 OAUTH_REQUEST_SCOPE_COUNT_ERROR.to_string(),
4174 ));
4175 }
4176 if scopes
4177 .iter()
4178 .any(|scope| !is_valid_oauth_scope_token(scope))
4179 {
4180 return Err(OAuthError::InvalidScope(
4181 OAUTH_REQUEST_SCOPE_VALUE_ERROR.to_string(),
4182 ));
4183 }
4184
4185 let mut seen = HashSet::with_capacity(scopes.len());
4186 let mut canonical = Vec::with_capacity(scopes.len());
4187 for scope in scopes {
4188 if seen.insert(scope.as_str()) {
4189 canonical.push(scope.clone());
4190 }
4191 }
4192 Ok(canonical)
4193}
4194
4195fn ensure_fresh_token_pair(
4196 state: &OAuthServerState,
4197 pair: &PreparedTokenPair,
4198) -> Result<(), OAuthError> {
4199 if constant_time_eq(&pair.access_value, &pair.refresh_value)
4200 || state.credential_value_in_use(&pair.access_value)
4201 || state.credential_value_in_use(&pair.refresh_value)
4202 {
4203 return Err(OAuthError::ServerError(
4204 "generated OAuth credential collided with retained state".to_string(),
4205 ));
4206 }
4207 Ok(())
4208}
4209
4210fn validate_client_id_admission(client_id: &str) -> Result<(), OAuthError> {
4211 if client_id.is_empty()
4212 || client_id.len() > MAX_OAUTH_CLIENT_ID_BYTES
4213 || client_id.chars().any(char::is_control)
4214 {
4215 return Err(OAuthError::InvalidClient(
4216 OAUTH_CLIENT_AUTHENTICATION_ERROR.to_string(),
4217 ));
4218 }
4219 Ok(())
4220}
4221
4222fn validate_client_authentication_admission(
4223 client_id: &str,
4224 client_secret: Option<&str>,
4225) -> Result<(), OAuthError> {
4226 validate_client_id_admission(client_id)?;
4227 if client_secret.is_some_and(|secret| secret.len() > MAX_OAUTH_CLIENT_CREDENTIAL_BYTES) {
4228 return Err(OAuthError::InvalidClient(
4229 OAUTH_CLIENT_AUTHENTICATION_ERROR.to_string(),
4230 ));
4231 }
4232 Ok(())
4233}
4234
4235fn authenticate_client_or_dummy(
4236 state: &OAuthServerState,
4237 client_id: &str,
4238 client_secret: Option<&str>,
4239) -> Result<OAuthRegistrationEpoch, OAuthError> {
4240 let client = state.clients.get(client_id);
4241 let authenticated = client.map_or_else(
4242 || {
4243 let provided = client_secret.unwrap_or_default();
4244 perform_dummy_client_secret_verification(provided.as_bytes());
4245 false
4246 },
4247 |client| client.authenticate(client_secret),
4248 );
4249 if !authenticated {
4250 return Err(OAuthError::InvalidClient(
4251 OAUTH_CLIENT_AUTHENTICATION_ERROR.to_string(),
4252 ));
4253 }
4254 client
4255 .map(|client| client.registration_epoch)
4256 .ok_or_else(|| OAuthError::InvalidClient(OAUTH_CLIENT_AUTHENTICATION_ERROR.to_string()))
4257}
4258
4259fn perform_dummy_client_secret_verification(provided: &[u8]) {
4260 let verified = ClientSecretVerifier::dummy().verify(std::hint::black_box(provided));
4261 std::hint::black_box(verified);
4262}
4263
4264fn validate_opaque_credential(value: &str, error: &'static str) -> Result<(), OAuthError> {
4265 use base64::Engine;
4266 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
4267
4268 if value.len() != OAUTH_OPAQUE_CREDENTIAL_BYTES {
4269 return Err(OAuthError::InvalidGrant(error.to_string()));
4270 }
4271 let decoded = Zeroizing::new(
4272 URL_SAFE_NO_PAD
4273 .decode(value)
4274 .map_err(|_| OAuthError::InvalidGrant(error.to_string()))?,
4275 );
4276 let canonical = Zeroizing::new(base64url_encode(&decoded));
4277 if decoded.len() != 32 || canonical.as_str() != value {
4278 return Err(OAuthError::InvalidGrant(error.to_string()));
4279 }
4280 Ok(())
4281}
4282
4283fn validate_and_digest_opaque_credential(
4284 kind: CredentialKind,
4285 value: &str,
4286 error: &'static str,
4287) -> Result<CredentialDigest, OAuthError> {
4288 validate_opaque_credential(value, error)?;
4289 digest_credential(kind, value)
4290}
4291
4292fn digest_credential(kind: CredentialKind, value: &str) -> Result<CredentialDigest, OAuthError> {
4293 if value.len() != OAUTH_OPAQUE_CREDENTIAL_BYTES {
4294 return Err(OAuthError::InvalidGrant(
4295 "OAuth credential is outside admitted bounds".to_string(),
4296 ));
4297 }
4298 let domain = kind.domain();
4299 let mut framed = Zeroizing::new(Vec::with_capacity(domain.len() + value.len()));
4300 framed.extend_from_slice(domain);
4301 framed.extend_from_slice(value.as_bytes());
4302 let digest = sha256_bounded(&framed, MAX_OPAQUE_CREDENTIAL_DIGEST_INPUT_BYTES)
4303 .map_err(|error| OAuthError::ServerError(error.to_string()))?;
4304 Ok(CredentialDigest(digest))
4305}
4306
4307fn derive_authorization_grant_id(
4308 authorization_code: CredentialDigest,
4309) -> Result<OAuthGrantId, OAuthError> {
4310 derive_grant_id(AUTHORIZATION_GRANT_ID_DOMAIN, &[authorization_code])
4311}
4312
4313fn derive_direct_grant_id(
4314 access_token: CredentialDigest,
4315 refresh_token: CredentialDigest,
4316) -> Result<OAuthGrantId, OAuthError> {
4317 derive_grant_id(DIRECT_GRANT_ID_DOMAIN, &[access_token, refresh_token])
4318}
4319
4320fn derive_grant_id(
4321 domain: &'static [u8],
4322 credentials: &[CredentialDigest],
4323) -> Result<OAuthGrantId, OAuthError> {
4324 let mut framed = Zeroizing::new(Vec::with_capacity(domain.len() + credentials.len() * 32));
4325 framed.extend_from_slice(domain);
4326 for credential in credentials {
4327 framed.extend_from_slice(credential.0.as_bytes());
4328 }
4329 let digest = sha256_bounded(&framed, MAX_GRANT_ID_DERIVATION_INPUT_BYTES)
4330 .map_err(|error| OAuthError::ServerError(error.to_string()))?;
4331 Ok(OAuthGrantId::from_bytes(digest.into_bytes()))
4332}
4333
4334fn client_secret_digest(
4335 salt: &[u8; CLIENT_SECRET_SALT_BYTES],
4336 secret: &[u8],
4337) -> Result<Sha256Digest, OAuthError> {
4338 if secret.len() > MAX_OAUTH_CLIENT_CREDENTIAL_BYTES {
4339 return Err(OAuthError::InvalidClient(
4340 OAUTH_CLIENT_AUTHENTICATION_ERROR.to_string(),
4341 ));
4342 }
4343 let mut framed = Zeroizing::new(Vec::with_capacity(
4344 CLIENT_SECRET_VERIFIER_DOMAIN.len() + salt.len() + secret.len(),
4345 ));
4346 framed.extend_from_slice(CLIENT_SECRET_VERIFIER_DOMAIN);
4347 framed.extend_from_slice(salt);
4348 framed.extend_from_slice(secret);
4349 sha256_bounded(&framed, MAX_CLIENT_SECRET_VERIFIER_INPUT_BYTES)
4350 .map_err(|error| OAuthError::ServerError(error.to_string()))
4351}
4352
4353fn generate_token() -> Result<String, OAuthError> {
4355 generate_token_with_draw(draw_security_identifier)
4356}
4357
4358fn generate_token_with_draw<F, E>(draw: F) -> Result<String, OAuthError>
4359where
4360 F: FnOnce() -> Result<SecurityIdentifier, E>,
4361 E: std::fmt::Display,
4362{
4363 let identifier = draw().map_err(|error| OAuthError::ServerError(error.to_string()))?;
4364 Ok(base64url_encode(identifier.as_bytes()))
4366}
4367
4368fn base64url_encode(data: &[u8]) -> String {
4370 use base64::Engine;
4371 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
4372 URL_SAFE_NO_PAD.encode(data)
4373}
4374
4375fn validate_pkce_code_verifier(verifier: &str) -> Result<(), OAuthError> {
4377 let verifier_bytes = verifier.as_bytes();
4378 if !(PKCE_CODE_VERIFIER_MIN_BYTES..=PKCE_CODE_VERIFIER_MAX_BYTES)
4379 .contains(&verifier_bytes.len())
4380 {
4381 return Err(OAuthError::InvalidRequest(format!(
4382 "code_verifier must be {PKCE_CODE_VERIFIER_MIN_BYTES} to \
4383 {PKCE_CODE_VERIFIER_MAX_BYTES} bytes of RFC 7636 unreserved ASCII"
4384 )));
4385 }
4386 if !verifier_bytes
4387 .iter()
4388 .copied()
4389 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~'))
4390 {
4391 return Err(OAuthError::InvalidRequest(format!(
4392 "code_verifier must be {PKCE_CODE_VERIFIER_MIN_BYTES} to \
4393 {PKCE_CODE_VERIFIER_MAX_BYTES} bytes of RFC 7636 unreserved ASCII"
4394 )));
4395 }
4396
4397 Ok(())
4398}
4399
4400fn compute_s256_challenge(verifier: &str) -> Result<String, OAuthError> {
4402 validate_pkce_code_verifier(verifier)?;
4403 let digest = sha256_bounded(verifier.as_bytes(), PKCE_CODE_VERIFIER_MAX_BYTES)
4404 .map_err(|error| OAuthError::InvalidRequest(error.to_string()))?;
4405 Ok(base64url_encode(digest.as_bytes()))
4406}
4407
4408fn validate_s256_code_challenge(challenge: &str) -> Result<(), OAuthError> {
4410 use base64::Engine;
4411 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
4412
4413 if challenge.len() != 43 {
4416 return Err(OAuthError::InvalidRequest(
4417 "code_challenge must be a canonical S256 challenge".to_string(),
4418 ));
4419 }
4420
4421 let decoded = URL_SAFE_NO_PAD.decode(challenge).map_err(|_| {
4422 OAuthError::InvalidRequest("code_challenge must be a canonical S256 challenge".to_string())
4423 })?;
4424 if decoded.len() != 32 || base64url_encode(&decoded) != challenge {
4425 return Err(OAuthError::InvalidRequest(
4426 "code_challenge must be a canonical S256 challenge".to_string(),
4427 ));
4428 }
4429
4430 Ok(())
4431}
4432
4433fn url_encode(s: &str) -> String {
4435 let mut result = String::with_capacity(s.len() * 3);
4436 for byte in s.bytes() {
4437 match byte {
4438 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
4439 result.push(byte as char);
4440 }
4441 _ => {
4442 result.push('%');
4443 result.push_str(&format!("{:02X}", byte));
4444 }
4445 }
4446 }
4447 result
4448}
4449
4450fn validate_registered_redirect_uri(redirect_uris: &[String], uri: &str) -> bool {
4451 let Some(candidate) = parse_redirect_uri(uri) else {
4452 return false;
4453 };
4454
4455 for allowed in redirect_uris {
4456 let Some(registered) = parse_redirect_uri(allowed) else {
4457 continue;
4458 };
4459
4460 if allowed == uri {
4464 return true;
4465 }
4466
4467 if registered.scheme() == "http"
4471 && candidate.scheme() == "http"
4472 && loopback_redirect_match(allowed, uri)
4473 {
4474 return true;
4475 }
4476 }
4477
4478 false
4479}
4480
4481fn validate_registered_scopes(allowed_scopes: &HashSet<String>, scopes: &[String]) -> bool {
4482 scopes.iter().all(|scope| allowed_scopes.contains(scope))
4483}
4484
4485fn authenticate_client_secret(expected: &[u8], provided: &[u8]) -> bool {
4486 let Ok(expected_digest) = sha256_bounded(expected, MAX_OAUTH_CLIENT_CREDENTIAL_BYTES) else {
4487 return false;
4488 };
4489 let Ok(provided_digest) = sha256_bounded(provided, MAX_OAUTH_CLIENT_CREDENTIAL_BYTES) else {
4490 return false;
4491 };
4492
4493 constant_time_digest_eq(expected_digest.as_bytes(), provided_digest.as_bytes())
4494}
4495
4496fn constant_time_digest_eq(expected: &[u8; 32], provided: &[u8; 32]) -> bool {
4497 let difference = expected
4498 .iter()
4499 .zip(provided)
4500 .fold(0_u8, |difference, (expected, provided)| {
4501 difference | (*expected ^ *provided)
4502 });
4503 difference == 0
4504}
4505
4506fn constant_time_eq(a: &str, b: &str) -> bool {
4508 if a.len() != b.len() {
4509 return false;
4510 }
4511
4512 let mut result = 0u8;
4513 for (x, y) in a.bytes().zip(b.bytes()) {
4514 result |= x ^ y;
4515 }
4516 result == 0
4517}
4518
4519fn contains_unsafe_display_character(value: &str) -> bool {
4520 value.chars().any(|character| {
4521 character.is_control()
4522 || matches!(
4523 character,
4524 '\u{061c}'
4525 | '\u{200e}'
4526 | '\u{200f}'
4527 | '\u{2028}'..='\u{202e}'
4528 | '\u{2066}'..='\u{206f}'
4529 )
4530 })
4531}
4532
4533fn parsed_url_has_credentials(url: &Url) -> bool {
4534 !url.username().is_empty() || url.password().is_some()
4535}
4536
4537fn raw_url_authority(value: &str) -> Option<&str> {
4538 let (_, after_scheme) = value.split_once("://")?;
4539 let authority_end = after_scheme
4540 .find(['/', '?', '#'])
4541 .unwrap_or(after_scheme.len());
4542 Some(&after_scheme[..authority_end])
4543}
4544
4545fn raw_url_authority_has_userinfo(value: &str) -> bool {
4546 raw_url_authority(value).is_some_and(|authority| authority.contains('@'))
4547}
4548
4549fn parse_canonical_loopback_authority(authority: &str) -> Option<&str> {
4550 for host in ["127.0.0.1", "[::1]"] {
4551 if authority == host {
4552 return Some(host);
4553 }
4554 if let Some(port) = authority
4555 .strip_prefix(host)
4556 .and_then(|rest| rest.strip_prefix(':'))
4557 {
4558 if port.is_empty()
4559 || !port.bytes().all(|byte| byte.is_ascii_digit())
4560 || (port.len() > 1 && port.starts_with('0'))
4561 || port.parse::<u16>().is_err()
4562 {
4563 return None;
4564 }
4565 return Some(host);
4566 }
4567 }
4568 None
4569}
4570
4571fn raw_loopback_redirect_parts(value: &str) -> Option<(&str, &str)> {
4572 let after_scheme = value.strip_prefix("http://")?;
4573 let authority_end = after_scheme
4574 .find(['/', '?', '#'])
4575 .unwrap_or(after_scheme.len());
4576 let authority = &after_scheme[..authority_end];
4577 let host = parse_canonical_loopback_authority(authority)?;
4578 Some((host, &after_scheme[authority_end..]))
4579}
4580
4581fn is_literal_loopback_host(url: &Url) -> bool {
4582 match url.host() {
4583 Some(Host::Ipv4(address)) => address == std::net::Ipv4Addr::LOCALHOST,
4584 Some(Host::Ipv6(address)) => address == std::net::Ipv6Addr::LOCALHOST,
4585 Some(Host::Domain(_)) | None => false,
4586 }
4587}
4588
4589fn parse_secure_endpoint(value: &str, max_bytes: usize) -> Option<Url> {
4590 if value.is_empty()
4591 || value.len() > max_bytes
4592 || value.chars().any(char::is_control)
4593 || raw_url_authority_has_userinfo(value)
4594 {
4595 return None;
4596 }
4597
4598 let url = Url::parse(value).ok()?;
4599 if url.cannot_be_a_base()
4600 || url.host().is_none()
4601 || parsed_url_has_credentials(&url)
4602 || url.fragment().is_some()
4603 || url.as_str() != value
4604 {
4605 return None;
4606 }
4607
4608 match url.scheme() {
4609 "https" => Some(url),
4610 "http"
4611 if is_literal_loopback_host(&url)
4612 && raw_url_authority(value)
4613 .and_then(parse_canonical_loopback_authority)
4614 .is_some() =>
4615 {
4616 Some(url)
4617 }
4618 _ => None,
4619 }
4620}
4621
4622pub(crate) fn validate_oauth_issuer(issuer: &str) -> Result<(), OAuthError> {
4623 let valid = parse_secure_endpoint(issuer, MAX_OAUTH_ISSUER_BYTES)
4624 .is_some_and(|url| url.scheme() == "https" && url.query().is_none());
4625 if !valid {
4626 return Err(OAuthError::ServerError(OAUTH_ISSUER_ERROR.to_string()));
4627 }
4628 Ok(())
4629}
4630
4631fn parse_redirect_uri(uri: &str) -> Option<Url> {
4632 let url = parse_secure_endpoint(uri, MAX_OAUTH_REDIRECT_URI_BYTES)?;
4633 let has_reserved_response_parameter = url.query().is_some_and(|query| {
4634 query.split(['&', ';']).any(|field| {
4635 let raw_name = field.split_once('=').map_or(field, |(name, _)| name);
4636 url::form_urlencoded::parse(raw_name.as_bytes())
4637 .next()
4638 .is_some_and(|(name, _)| {
4639 matches!(
4640 name.as_ref(),
4641 "code" | "state" | "error" | "error_description" | "error_uri" | "iss"
4642 )
4643 })
4644 })
4645 });
4646 (!has_reserved_response_parameter).then_some(url)
4647}
4648
4649fn loopback_redirect_match(registered: &str, candidate: &str) -> bool {
4650 match (
4651 raw_loopback_redirect_parts(registered),
4652 raw_loopback_redirect_parts(candidate),
4653 ) {
4654 (Some((registered_host, registered_tail)), Some((candidate_host, candidate_tail))) => {
4655 registered_host == candidate_host && registered_tail == candidate_tail
4656 }
4657 _ => false,
4658 }
4659}
4660
4661#[cfg(test)]
4662fn is_loopback_redirect(uri: &str) -> bool {
4663 parse_redirect_uri(uri).is_some_and(|url| url.scheme() == "http")
4664}
4665
4666#[cfg(test)]
4667fn loopback_match(a: &str, b: &str) -> bool {
4668 parse_redirect_uri(a).is_some()
4669 && parse_redirect_uri(b).is_some()
4670 && loopback_redirect_match(a, b)
4671}
4672
4673#[cfg(test)]
4678mod tests {
4679 use super::*;
4680 use std::sync::atomic::{AtomicUsize, Ordering};
4681
4682 #[derive(Clone, Copy)]
4683 enum ApprovalTestMode {
4684 Exact,
4685 WrongBinding,
4686 WrongGeneration,
4687 WrongScopes,
4688 WrongResource,
4689 Denied,
4690 Error,
4691 Cancelled,
4692 }
4693
4694 struct CountingApprovalBackend {
4695 generation: AuthorizationApprovalGeneration,
4696 mode: ApprovalTestMode,
4697 calls: AtomicUsize,
4698 observed_debug: std::sync::Mutex<Vec<String>>,
4699 }
4700
4701 impl CountingApprovalBackend {
4702 fn new(mode: ApprovalTestMode) -> Self {
4703 Self {
4704 generation: AuthorizationApprovalGeneration::from_bytes([0x07; 32]),
4705 mode,
4706 calls: AtomicUsize::new(0),
4707 observed_debug: std::sync::Mutex::new(Vec::new()),
4708 }
4709 }
4710 }
4711
4712 impl AuthorizationApprovalBackend for CountingApprovalBackend {
4713 fn generation(&self) -> AuthorizationApprovalGeneration {
4714 self.generation
4715 }
4716
4717 fn approve(
4718 &self,
4719 request: &AuthorizationApprovalRequest,
4720 ) -> AuthorizationApprovalDisposition {
4721 self.calls.fetch_add(1, Ordering::SeqCst);
4722 self.observed_debug
4723 .lock()
4724 .expect("approval observation lock")
4725 .push(format!("{request:?}"));
4726 match self.mode {
4727 ApprovalTestMode::Denied => AuthorizationApprovalDisposition::Denied,
4728 ApprovalTestMode::Error => AuthorizationApprovalDisposition::Error,
4729 ApprovalTestMode::Cancelled => AuthorizationApprovalDisposition::Cancelled,
4730 mode => {
4731 let mut decision = request
4732 .approve(
4733 "approved-subject".to_string(),
4734 request.scopes().to_vec(),
4735 request.resource().map(str::to_string),
4736 if matches!(mode, ApprovalTestMode::WrongGeneration) {
4737 AuthorizationApprovalGeneration::from_bytes([0x08; 32])
4738 } else {
4739 self.generation
4740 },
4741 )
4742 .expect("validated request must construct test decision");
4743 match mode {
4744 ApprovalTestMode::WrongBinding => {
4745 decision.binding.state = Some("wrong-state".to_string());
4746 }
4747 ApprovalTestMode::WrongScopes => {
4748 decision.approved_scopes.push("wrong-scope".to_string());
4749 }
4750 ApprovalTestMode::WrongResource => {
4751 decision.approved_resource = Some("https://wrong.example/".to_string());
4752 }
4753 ApprovalTestMode::Exact
4754 | ApprovalTestMode::WrongGeneration
4755 | ApprovalTestMode::Denied
4756 | ApprovalTestMode::Error
4757 | ApprovalTestMode::Cancelled => {}
4758 }
4759 AuthorizationApprovalDisposition::Approved(decision)
4760 }
4761 }
4762 }
4763 }
4764
4765 fn server_with_counting_approval(backend: Arc<CountingApprovalBackend>) -> OAuthServer {
4766 OAuthServer::with_approval_backend(OAuthServerConfig::default(), backend)
4767 }
4768
4769 fn configured_approved_test_server(config: OAuthServerConfig) -> OAuthServer {
4770 OAuthServer::with_approval_backend(
4771 config,
4772 Arc::new(TestDefaultAuthorizationApprovalBackend),
4773 )
4774 }
4775
4776 fn take_parameter_value(
4777 admission: &mut OAuthParameterAdmission,
4778 name: OAuthParameterName,
4779 ) -> Option<String> {
4780 admission
4781 .take_defined_value(name)
4782 .map(OAuthSensitiveParameterValue::into_string)
4783 }
4784
4785 fn unknown_form_with_value_lengths(value_lengths: &[usize]) -> Vec<u8> {
4786 assert!(!value_lengths.is_empty());
4787 let mut input = Vec::new();
4788 for (index, value_len) in value_lengths.iter().copied().enumerate() {
4789 if index != 0 {
4790 input.push(b'&');
4791 }
4792 input.extend_from_slice(b"unknown=");
4793 input.extend(std::iter::repeat_n(b'x', value_len));
4794 }
4795 input
4796 }
4797
4798 fn assert_oauth_stats_unchanged(before: &OAuthServerStats, after: &OAuthServerStats) {
4799 assert_eq!(after.clients, before.clients);
4800 assert_eq!(after.authorization_codes, before.authorization_codes);
4801 assert_eq!(after.access_tokens, before.access_tokens);
4802 assert_eq!(after.refresh_tokens, before.refresh_tokens);
4803 assert_eq!(after.revoked_tokens, before.revoked_tokens);
4804 }
4805
4806 #[test]
4807 fn authorization_parameter_admission_preserves_order_and_unknowns_without_typed_effect() {
4808 let mut admission = OAuthParameterAdmission::admit(
4809 OAuthParameterEndpoint::AuthorizationQuery,
4810 b"response_type=code&client_id=demo&scope=read+write&state=&unknown=one&unknown=two",
4811 )
4812 .expect("bounded authorization query must be admitted");
4813
4814 assert_eq!(admission.source(), OAuthParameterSource::Query);
4815 assert_eq!(admission.parameters().len(), 6);
4816 assert_eq!(admission.parameters()[0].ordinal(), 0);
4817 assert_eq!(
4818 admission.parameters()[0].source(),
4819 OAuthParameterSource::Query
4820 );
4821 assert_eq!(admission.parameters()[0].name(), "response_type");
4822 assert_eq!(admission.parameters()[2].value_len(), "read write".len());
4823 assert!(admission.parameters()[3].is_defined());
4824 assert_eq!(
4825 take_parameter_value(&mut admission, OAuthParameterName::ResponseType),
4826 Some("code".to_string())
4827 );
4828 assert_eq!(
4829 take_parameter_value(&mut admission, OAuthParameterName::ClientId),
4830 Some("demo".to_string())
4831 );
4832 assert_eq!(
4833 take_parameter_value(&mut admission, OAuthParameterName::State),
4834 None
4835 );
4836 assert_eq!(
4837 admission
4838 .unknown_parameters()
4839 .map(|parameter| (parameter.ordinal(), parameter.name(), parameter.value_len()))
4840 .collect::<Vec<_>>(),
4841 vec![(4, "unknown", 3), (5, "unknown", 3)]
4842 );
4843 }
4844
4845 #[test]
4846 fn authorization_parameter_admission_rejects_only_a_repeated_defined_name() {
4847 let error = OAuthParameterAdmission::admit(
4851 OAuthParameterEndpoint::AuthorizationQuery,
4852 b"response_type=code&client_id=demo&scope=read+write&state=&client_id=other&unknown=two",
4853 )
4854 .expect_err("a repeated defined parameter must be rejected");
4855
4856 assert_eq!(
4857 error,
4858 OAuthParameterAdmissionError::DuplicateDefinedParameter {
4859 parameter: OAuthParameterName::ClientId,
4860 source: OAuthParameterSource::Query,
4861 first_ordinal: 1,
4862 duplicate_ordinal: 4,
4863 }
4864 );
4865 }
4866
4867 #[test]
4868 fn form_parameter_admission_decodes_percent_and_plus_and_omits_empty_defined_values() {
4869 let mut admission = OAuthParameterAdmission::admit(
4870 OAuthParameterEndpoint::TokenForm,
4871 b"grant_type=authorization_code&client_id=demo%2Bclient&client_secret=&code_verifier=one+two%2Bthree&unknown=x&unknown=y",
4872 )
4873 .expect("strictly encoded token form must be admitted");
4874
4875 assert_eq!(admission.source(), OAuthParameterSource::Form);
4876 assert_eq!(
4877 take_parameter_value(&mut admission, OAuthParameterName::ClientId),
4878 Some("demo+client".to_string())
4879 );
4880 assert_eq!(
4881 take_parameter_value(&mut admission, OAuthParameterName::CodeVerifier),
4882 Some("one two+three".to_string())
4883 );
4884 assert_eq!(
4885 take_parameter_value(&mut admission, OAuthParameterName::ClientSecret),
4886 None
4887 );
4888 assert_eq!(admission.unknown_parameters().count(), 2);
4889 }
4890
4891 #[test]
4892 fn form_parameter_admission_rejects_an_empty_then_nonempty_defined_duplicate() {
4893 let error = OAuthParameterAdmission::admit(
4897 OAuthParameterEndpoint::TokenForm,
4898 b"grant_type=authorization_code&client_id=demo%2Bclient&client_secret=&client_secret=secret&code_verifier=one+two%2Bthree&unknown=x&unknown=y",
4899 )
4900 .expect_err("empty defined values must not evade duplicate rejection");
4901
4902 assert_eq!(
4903 error,
4904 OAuthParameterAdmissionError::DuplicateDefinedParameter {
4905 parameter: OAuthParameterName::ClientSecret,
4906 source: OAuthParameterSource::Form,
4907 first_ordinal: 2,
4908 duplicate_ordinal: 3,
4909 }
4910 );
4911 }
4912
4913 #[test]
4914 fn resource_is_a_defined_singleton_for_authorization_and_token_profiles() {
4915 let resource = "https%3A%2F%2Fresource.example%2Fapi";
4916 let mut authorization = OAuthParameterAdmission::admit(
4917 OAuthParameterEndpoint::AuthorizationQuery,
4918 format!("client_id=demo&resource={resource}").as_bytes(),
4919 )
4920 .expect("authorization resource must be admitted");
4921 assert_eq!(
4922 take_parameter_value(&mut authorization, OAuthParameterName::Resource),
4923 Some("https://resource.example/api".to_string())
4924 );
4925
4926 let mut token = OAuthParameterAdmission::admit(
4927 OAuthParameterEndpoint::TokenForm,
4928 format!("grant_type=refresh_token&resource={resource}").as_bytes(),
4929 )
4930 .expect("token resource must be admitted");
4931 assert_eq!(
4932 take_parameter_value(&mut token, OAuthParameterName::Resource),
4933 Some("https://resource.example/api".to_string())
4934 );
4935 }
4936
4937 #[test]
4938 fn token_resource_duplicate_is_rejected_before_grant_processing() {
4939 let error = OAuthParameterAdmission::admit(
4942 OAuthParameterEndpoint::TokenForm,
4943 b"grant_type=refresh_token&resource=https%3A%2F%2Fresource.example%2Fa&resource=https%3A%2F%2Fresource.example%2Fb",
4944 )
4945 .expect_err("repeated RFC 8707 resource must be rejected");
4946 assert_eq!(
4947 error,
4948 OAuthParameterAdmissionError::DuplicateDefinedParameter {
4949 parameter: OAuthParameterName::Resource,
4950 source: OAuthParameterSource::Form,
4951 first_ordinal: 1,
4952 duplicate_ordinal: 2,
4953 }
4954 );
4955 }
4956
4957 #[test]
4958 fn parameter_admission_ignores_empty_segments_and_treats_bare_names_as_empty() {
4959 let mut admission = OAuthParameterAdmission::admit(
4960 OAuthParameterEndpoint::AuthorizationQuery,
4961 b"&&state&&client_id=demo&scope&unknown&unknown=&&",
4962 )
4963 .expect("empty segments and bare names are standard form syntax");
4964
4965 assert_eq!(admission.parameters().len(), 5);
4966 assert_eq!(admission.parameters()[0].ordinal(), 0);
4967 assert_eq!(admission.parameters()[0].name(), "state");
4968 assert_eq!(admission.parameters()[0].value_len(), 0);
4969 assert_eq!(admission.parameters()[2].name(), "scope");
4970 assert_eq!(admission.parameters()[2].value_len(), 0);
4971 assert_eq!(
4972 take_parameter_value(&mut admission, OAuthParameterName::State),
4973 None
4974 );
4975 assert_eq!(
4976 take_parameter_value(&mut admission, OAuthParameterName::Scope),
4977 None
4978 );
4979 assert_eq!(
4980 take_parameter_value(&mut admission, OAuthParameterName::ClientId),
4981 Some("demo".to_string())
4982 );
4983 assert_eq!(
4984 admission
4985 .unknown_parameters()
4986 .map(|parameter| (parameter.ordinal(), parameter.name(), parameter.value_len()))
4987 .collect::<Vec<_>>(),
4988 vec![(3, "unknown", 0), (4, "unknown", 0)]
4989 );
4990 }
4991
4992 #[test]
4993 fn bare_and_equals_empty_defined_names_still_trigger_duplicate_rejection() {
4994 let error = OAuthParameterAdmission::admit(
4997 OAuthParameterEndpoint::AuthorizationQuery,
4998 b"&&state&state=&&client_id=demo&scope&unknown&unknown=&&",
4999 )
5000 .expect_err("empty spellings cannot evade duplicate defined-name rejection");
5001 assert_eq!(
5002 error,
5003 OAuthParameterAdmissionError::DuplicateDefinedParameter {
5004 parameter: OAuthParameterName::State,
5005 source: OAuthParameterSource::Query,
5006 first_ordinal: 0,
5007 duplicate_ordinal: 1,
5008 }
5009 );
5010 }
5011
5012 #[test]
5013 fn parameter_admission_is_pure_and_not_an_http_or_authorization_gate() {
5014 let server = OAuthServer::with_defaults();
5018 let before = server.stats();
5019 let mut admission = OAuthParameterAdmission::admit(
5020 OAuthParameterEndpoint::TokenForm,
5021 b"grant_type=refresh_token&refresh_token=credential&unknown=discard-me",
5022 )
5023 .expect("pure parameter admission must succeed independently of OAuth state");
5024 assert_eq!(
5025 take_parameter_value(&mut admission, OAuthParameterName::RefreshToken),
5026 Some("credential".to_string())
5027 );
5028 let after = server.stats();
5029 assert_oauth_stats_unchanged(&before, &after);
5030 }
5031
5032 #[test]
5033 fn admission_diagnostics_are_redacted_for_defined_and_unknown_values() {
5034 let admission = OAuthParameterAdmission::admit(
5035 OAuthParameterEndpoint::TokenForm,
5036 b"client_secret=defined-never-prints&unknown=unknown-never-prints",
5037 )
5038 .expect("bounded input must be admitted for redaction inspection");
5039
5040 let admission_debug = format!("{admission:?}");
5041 let parameter_debug = format!("{:?}", admission.parameters());
5042 for secret in ["defined-never-prints", "unknown-never-prints"] {
5043 assert!(!admission_debug.contains(secret));
5044 assert!(!parameter_debug.contains(secret));
5045 }
5046 assert!(parameter_debug.contains("value_len"));
5047 }
5048
5049 #[test]
5050 fn parameter_admission_profiles_keep_defined_names_endpoint_specific() {
5051 let mut token_form = OAuthParameterAdmission::admit(
5052 OAuthParameterEndpoint::TokenForm,
5053 b"grant_type=refresh_token&token=opaque-value&unknown=first&unknown=second",
5054 )
5055 .expect("token form must admit bounded unknown parameters");
5056 assert_eq!(
5057 take_parameter_value(&mut token_form, OAuthParameterName::GrantType),
5058 Some("refresh_token".to_string())
5059 );
5060 assert_eq!(
5061 take_parameter_value(&mut token_form, OAuthParameterName::Token),
5062 None
5063 );
5064 assert_eq!(
5065 token_form
5066 .unknown_parameters()
5067 .map(OAuthAdmittedParameter::name)
5068 .collect::<Vec<_>>(),
5069 vec!["token", "unknown", "unknown"]
5070 );
5071
5072 for endpoint in [
5073 OAuthParameterEndpoint::RevocationForm,
5074 OAuthParameterEndpoint::IntrospectionForm,
5075 ] {
5076 let mut form = OAuthParameterAdmission::admit(
5077 endpoint,
5078 b"token=opaque-value&token_type_hint=refresh_token&client_id=demo&client_secret=&resource=https%3A%2F%2Fresource.example%2Fapi",
5079 )
5080 .expect("token-like form must admit its defined values");
5081 assert_eq!(form.source(), OAuthParameterSource::Form);
5082 assert_eq!(
5083 take_parameter_value(&mut form, OAuthParameterName::Token),
5084 Some("opaque-value".to_string())
5085 );
5086 assert_eq!(
5087 take_parameter_value(&mut form, OAuthParameterName::TokenTypeHint),
5088 Some("refresh_token".to_string())
5089 );
5090 assert_eq!(
5091 take_parameter_value(&mut form, OAuthParameterName::ClientId),
5092 Some("demo".to_string())
5093 );
5094 assert_eq!(
5095 take_parameter_value(&mut form, OAuthParameterName::ClientSecret),
5096 None
5097 );
5098 assert_eq!(
5099 take_parameter_value(&mut form, OAuthParameterName::Resource),
5100 None
5101 );
5102 assert_eq!(
5103 form.unknown_parameters()
5104 .map(OAuthAdmittedParameter::name)
5105 .collect::<Vec<_>>(),
5106 vec!["resource"]
5107 );
5108 }
5109 }
5110
5111 #[test]
5112 fn parameter_admission_rejects_malformed_encoding_and_controls_before_state() {
5113 for (input, expected) in [
5114 (
5115 b"grant_type=authorization_code&code=%".as_slice(),
5116 OAuthParameterAdmissionError::MalformedPercentEncoding,
5117 ),
5118 (
5119 b"grant_type=authorization_code&code=%GG".as_slice(),
5120 OAuthParameterAdmissionError::MalformedPercentEncoding,
5121 ),
5122 (
5123 b"grant_type=authorization_code&code=%FF".as_slice(),
5124 OAuthParameterAdmissionError::InvalidUtf8,
5125 ),
5126 (
5127 b"grant_type=authorization_code&code=%0A".as_slice(),
5128 OAuthParameterAdmissionError::ControlCharacter,
5129 ),
5130 (
5131 b"grant_type=authorization_code&code=raw\ncontrol".as_slice(),
5132 OAuthParameterAdmissionError::ControlCharacter,
5133 ),
5134 (
5135 b"grant_type=authorization_code&=code".as_slice(),
5136 OAuthParameterAdmissionError::EmptyName,
5137 ),
5138 ] {
5139 let error = OAuthParameterAdmission::admit(OAuthParameterEndpoint::TokenForm, input)
5140 .expect_err("malformed token form must be rejected before endpoint logic");
5141 assert_eq!(error, expected);
5142 }
5143 }
5144
5145 #[test]
5146 fn parameter_admission_accepts_exact_limits_and_rejects_each_n_plus_one_without_state() {
5147 let server = OAuthServer::with_defaults();
5148 let before = server.stats();
5149
5150 let exact_body = unknown_form_with_value_lengths(&[4_096, 4_096, 4_096, 4_061]);
5153 assert_eq!(exact_body.len(), MAX_OAUTH_FORM_BODY_BYTES);
5154 let form = OAuthParameterAdmission::admit(OAuthParameterEndpoint::TokenForm, &exact_body)
5155 .expect("exact form-body limit must be admitted");
5156 assert_eq!(form.parameters().len(), 4);
5157
5158 let mut form_n_plus_one = exact_body.clone();
5159 form_n_plus_one.push(b'x');
5160 let error =
5161 OAuthParameterAdmission::admit(OAuthParameterEndpoint::TokenForm, &form_n_plus_one)
5162 .expect_err("form body N+1 must reject before endpoint logic");
5163 assert_eq!(error, OAuthParameterAdmissionError::InputTooLarge);
5164
5165 assert_eq!(exact_body.len(), MAX_OAUTH_AUTHORIZATION_QUERY_BYTES);
5166 let query =
5167 OAuthParameterAdmission::admit(OAuthParameterEndpoint::AuthorizationQuery, &exact_body)
5168 .expect("exact authorization-query limit must be admitted");
5169 assert_eq!(query.parameters().len(), 4);
5170
5171 let mut query_n_plus_one = exact_body.clone();
5172 query_n_plus_one.push(b'x');
5173 let error = OAuthParameterAdmission::admit(
5174 OAuthParameterEndpoint::AuthorizationQuery,
5175 &query_n_plus_one,
5176 )
5177 .expect_err("authorization query N+1 must reject before endpoint logic");
5178 assert_eq!(error, OAuthParameterAdmissionError::InputTooLarge);
5179
5180 let exact_pair_lengths = [1; MAX_OAUTH_PARAMETER_PAIRS];
5181 let exact_pairs = unknown_form_with_value_lengths(&exact_pair_lengths);
5182 let pairs = OAuthParameterAdmission::admit(OAuthParameterEndpoint::TokenForm, &exact_pairs)
5183 .expect("64 nonempty pairs must be admitted");
5184 assert_eq!(pairs.parameters().len(), MAX_OAUTH_PARAMETER_PAIRS);
5185
5186 let mut pairs_n_plus_one = exact_pairs;
5187 pairs_n_plus_one.extend_from_slice(b"&unknown=x");
5188 let error =
5189 OAuthParameterAdmission::admit(OAuthParameterEndpoint::TokenForm, &pairs_n_plus_one)
5190 .expect_err("65th nonempty pair must reject before endpoint logic");
5191 assert_eq!(error, OAuthParameterAdmissionError::TooManyPairs);
5192
5193 let mut exact_name = vec![b'n'; MAX_OAUTH_PARAMETER_NAME_BYTES];
5194 exact_name.push(b'=');
5195 let name = OAuthParameterAdmission::admit(OAuthParameterEndpoint::TokenForm, &exact_name)
5196 .expect("256-byte decoded name must be admitted");
5197 assert_eq!(
5198 name.parameters()[0].name().len(),
5199 MAX_OAUTH_PARAMETER_NAME_BYTES
5200 );
5201
5202 let mut name_n_plus_one = exact_name;
5203 name_n_plus_one.insert(MAX_OAUTH_PARAMETER_NAME_BYTES, b'n');
5204 let error =
5205 OAuthParameterAdmission::admit(OAuthParameterEndpoint::TokenForm, &name_n_plus_one)
5206 .expect_err("257-byte decoded name must reject before endpoint logic");
5207 assert_eq!(error, OAuthParameterAdmissionError::NameTooLarge);
5208
5209 let exact_value = unknown_form_with_value_lengths(&[MAX_OAUTH_PARAMETER_VALUE_BYTES]);
5210 let value = OAuthParameterAdmission::admit(OAuthParameterEndpoint::TokenForm, &exact_value)
5211 .expect("4096-byte decoded value must be admitted");
5212 assert_eq!(
5213 value.parameters()[0].value_len(),
5214 MAX_OAUTH_PARAMETER_VALUE_BYTES
5215 );
5216
5217 let mut value_n_plus_one = exact_value;
5218 value_n_plus_one.push(b'x');
5219 let error =
5220 OAuthParameterAdmission::admit(OAuthParameterEndpoint::TokenForm, &value_n_plus_one)
5221 .expect_err("4097-byte decoded value must reject before endpoint logic");
5222 assert_eq!(error, OAuthParameterAdmissionError::ValueTooLarge);
5223
5224 assert_oauth_stats_unchanged(&before, &server.stats());
5225 }
5226
5227 fn issue_access_token_via_auth_code(
5228 server: &OAuthServer,
5229 client_id: &str,
5230 redirect_uri: &str,
5231 scopes: &[&str],
5232 _subject: &str,
5233 ) -> TokenResponse {
5234 let code_verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk".to_string();
5235 let code_challenge = compute_s256_challenge(&code_verifier).expect("valid verifier");
5236 let auth_request = AuthorizationRequest {
5237 response_type: "code".to_string(),
5238 client_id: client_id.to_string(),
5239 redirect_uri: redirect_uri.to_string(),
5240 scopes: scopes.iter().map(|scope| (*scope).to_string()).collect(),
5241 resource: None,
5242 state: Some("oauth-test-state".to_string()),
5243 code_challenge,
5244 code_challenge_method: CodeChallengeMethod::S256,
5245 };
5246
5247 let (code, _redirect) = server.authorize(&auth_request).expect("authorize");
5248 server
5249 .token(&TokenRequest {
5250 grant_type: "authorization_code".to_string(),
5251 code: Some(code),
5252 redirect_uri: Some(redirect_uri.to_string()),
5253 client_id: client_id.to_string(),
5254 client_secret: None,
5255 code_verifier: Some(code_verifier),
5256 refresh_token: None,
5257 scopes: None,
5258 resource: None,
5259 })
5260 .expect("token exchange")
5261 }
5262
5263 fn bounded_test_client(client_id: &str) -> OAuthClient {
5264 OAuthClient::builder(client_id)
5265 .redirect_uri("http://127.0.0.1/callback")
5266 .build()
5267 .expect("valid test client")
5268 }
5269
5270 fn exact_ascii_value(prefix: &str, byte_len: usize) -> String {
5271 assert!(prefix.len() <= byte_len);
5272 let mut value = String::with_capacity(byte_len);
5273 value.push_str(prefix);
5274 value.extend(std::iter::repeat_n('x', byte_len - prefix.len()));
5275 value
5276 }
5277
5278 fn authorization_code_digest(value: &str) -> CredentialDigest {
5279 digest_credential(CredentialKind::AuthorizationCode, value)
5280 .expect("valid authorization-code fixture")
5281 }
5282
5283 fn access_token_digest(value: &str) -> CredentialDigest {
5284 digest_credential(CredentialKind::AccessToken, value).expect("valid access-token fixture")
5285 }
5286
5287 fn refresh_token_digest(value: &str) -> CredentialDigest {
5288 digest_credential(CredentialKind::RefreshToken, value).expect("valid refresh-token fixture")
5289 }
5290
5291 fn test_grant_id(tag: u8) -> OAuthGrantId {
5292 OAuthGrantId::from_bytes([tag; 32])
5293 }
5294
5295 fn test_registration_epoch(tag: u8) -> OAuthRegistrationEpoch {
5296 OAuthRegistrationEpoch([tag; OAUTH_REGISTRATION_EPOCH_BYTES])
5297 }
5298
5299 fn assert_registration_rejects_mutation<F>(mutate: F, expected: &str)
5300 where
5301 F: FnOnce(&mut OAuthClient),
5302 {
5303 let server = OAuthServer::with_defaults();
5304 let mut client = bounded_test_client("bounded-client");
5305 mutate(&mut client);
5306 let error = server
5307 .register_client(client)
5308 .expect_err("mutated client must be rejected before retention");
5309 assert!(matches!(&error, OAuthError::InvalidRequest(_)));
5310 assert_eq!(error.description(), expected);
5311 assert!(server.state.read().unwrap().clients.is_empty());
5312 }
5313
5314 fn assert_client_build_error(result: Result<OAuthClient, OAuthError>, expected: &str) {
5315 let error = result.expect_err("out-of-bounds client must not build");
5316 assert!(matches!(&error, OAuthError::InvalidRequest(_)));
5317 assert_eq!(error.description(), expected);
5318 }
5319
5320 fn bounded_authorization_request(client_id: &str) -> AuthorizationRequest {
5321 let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
5322 AuthorizationRequest {
5323 response_type: "code".to_string(),
5324 client_id: client_id.to_string(),
5325 redirect_uri: "http://127.0.0.1/callback".to_string(),
5326 scopes: Vec::new(),
5327 resource: None,
5328 state: None,
5329 code_challenge: compute_s256_challenge(verifier).expect("valid verifier"),
5330 code_challenge_method: CodeChallengeMethod::S256,
5331 }
5332 }
5333
5334 fn approved_resource_request(client_id: &str) -> AuthorizationRequest {
5335 AuthorizationRequest {
5336 resource: Some("https://resource.example/api".to_string()),
5337 state: Some("approval-state".to_string()),
5338 scopes: vec!["read".to_string()],
5339 ..bounded_authorization_request(client_id)
5340 }
5341 }
5342
5343 fn resource_code_exchange_request(client_id: &str, code: &str, resource: &str) -> TokenRequest {
5344 TokenRequest {
5345 resource: Some(resource.to_string()),
5346 ..bounded_code_exchange_request(client_id, code)
5347 }
5348 }
5349
5350 fn resource_refresh_request(
5351 client_id: &str,
5352 refresh_token: &str,
5353 resource: &str,
5354 ) -> TokenRequest {
5355 TokenRequest {
5356 resource: Some(resource.to_string()),
5357 ..bounded_refresh_request(client_id, refresh_token)
5358 }
5359 }
5360
5361 fn insert_expired_authorization_code_cleanup_canary(
5362 server: &OAuthServer,
5363 client_id: &str,
5364 ) -> CredentialDigest {
5365 let expired_code = base64url_encode(&[0xa7_u8; 32]);
5366 let digest = authorization_code_digest(&expired_code);
5367 let now = Instant::now();
5368 let mut state = server.state.write().expect("state");
5369 let registration_epoch = state
5370 .clients
5371 .get(client_id)
5372 .expect("registered client")
5373 .registration_epoch;
5374 state.authorization_codes.insert(
5375 digest,
5376 AuthorizationCode {
5377 client_id: client_id.to_string(),
5378 redirect_uri: "http://127.0.0.1/callback".to_string(),
5379 scopes: Vec::new(),
5380 resource: None,
5381 code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM".to_string(),
5382 code_challenge_method: CodeChallengeMethod::S256,
5383 issued_at: now,
5384 expires_at: now - Duration::from_secs(1),
5385 subject: None,
5386 state: None,
5387 registration_epoch,
5388 },
5389 );
5390 digest
5391 }
5392
5393 #[test]
5394 fn authorization_approval_backend_is_called_once_and_receives_only_redacted_facts() {
5395 const CLIENT_SECRET: &str = "approval-client-secret-canary";
5396 const CODE_CANARY: &str = "approval-code-canary";
5397 const TOKEN_CANARY: &str = "approval-token-canary";
5398 let backend = Arc::new(CountingApprovalBackend::new(ApprovalTestMode::Exact));
5399 let server = server_with_counting_approval(Arc::clone(&backend));
5400 server
5401 .register_client(
5402 OAuthClient::builder("approval-client")
5403 .secret(CLIENT_SECRET)
5404 .redirect_uri("http://127.0.0.1/callback")
5405 .scope("read")
5406 .build()
5407 .expect("bounded confidential client"),
5408 )
5409 .expect("register client");
5410 let request = approved_resource_request("approval-client");
5411 let before = server.stats();
5412
5413 let (code, _) = server.authorize(&request).expect("approved authorization");
5414
5415 assert_eq!(backend.calls.load(Ordering::SeqCst), 1);
5416 assert_eq!(
5417 server.stats().authorization_codes,
5418 before.authorization_codes + 1
5419 );
5420 let state = server.state.read().expect("state");
5421 let retained = state
5422 .authorization_codes
5423 .get(&authorization_code_digest(&code))
5424 .expect("approved code retained");
5425 assert_eq!(retained.subject.as_deref(), Some("approved-subject"));
5426 assert_eq!(retained.scopes, ["read"]);
5427 assert_eq!(
5428 retained.resource.as_deref(),
5429 Some("https://resource.example/api")
5430 );
5431 let observed = backend
5432 .observed_debug
5433 .lock()
5434 .expect("observed request")
5435 .join("\n");
5436 for canary in [CLIENT_SECRET, CODE_CANARY, TOKEN_CANARY] {
5437 assert!(!observed.contains(canary));
5438 }
5439 assert!(observed.contains("AuthorizationApprovalRequest"));
5440 }
5441
5442 #[test]
5443 fn approved_resource_survives_code_exchange_introspection_auth_and_refresh_rotation() {
5444 const RESOURCE: &str = "https://resource.example/api";
5445 let backend = Arc::new(CountingApprovalBackend::new(ApprovalTestMode::Exact));
5446 let server = Arc::new(server_with_counting_approval(Arc::clone(&backend)));
5447 server
5448 .register_client(
5449 OAuthClient::builder("resource-client")
5450 .redirect_uri("http://127.0.0.1/callback")
5451 .scope("read")
5452 .build()
5453 .expect("bounded client"),
5454 )
5455 .expect("register client");
5456
5457 let (code, _) = server
5458 .authorize(&approved_resource_request("resource-client"))
5459 .expect("approved authorization");
5460 let issued = server
5461 .token(&resource_code_exchange_request(
5462 "resource-client",
5463 &code,
5464 RESOURCE,
5465 ))
5466 .expect("exact resource code exchange");
5467 let initial_access = server
5468 .validate_access_token(&issued.access_token)
5469 .expect("issued access token introspection");
5470 assert_eq!(initial_access.resource.as_deref(), Some(RESOURCE));
5471 let refresh = issued.refresh_token.expect("refresh token");
5472
5473 let auth = server
5474 .token_verifier()
5475 .verify(
5476 &McpContext::new(asupersync::Cx::for_testing(), 1),
5477 AuthRequest {
5478 method: "tools/list",
5479 params: None,
5480 transport_authorization: None,
5481 request_id: 1,
5482 },
5483 &AccessToken {
5484 scheme: "Bearer".to_string(),
5485 token: issued.access_token,
5486 },
5487 )
5488 .expect("token verifier accepts issued access token");
5489 assert_eq!(
5490 auth.claims
5491 .as_ref()
5492 .and_then(|facts| facts["resource"].as_str()),
5493 Some(RESOURCE)
5494 );
5495
5496 let rotated = server
5497 .token(&bounded_refresh_request("resource-client", &refresh))
5498 .expect("omitted-resource refresh preserves the bound resource");
5499 let rotated_access = server
5500 .validate_access_token(&rotated.access_token)
5501 .expect("rotated access token introspection");
5502 assert_eq!(rotated_access.resource.as_deref(), Some(RESOURCE));
5503 let rotated_refresh = rotated.refresh_token.expect("rotated refresh token");
5504 assert_eq!(
5505 server
5506 .state
5507 .read()
5508 .expect("state")
5509 .refresh_tokens
5510 .get(&refresh_token_digest(&rotated_refresh))
5511 .and_then(|token| token.resource.as_deref()),
5512 Some(RESOURCE)
5513 );
5514 assert_eq!(backend.calls.load(Ordering::SeqCst), 1);
5515 }
5516
5517 #[test]
5518 fn refresh_resource_mismatch_rejects_without_consuming_or_widening_the_grant() {
5519 const RESOURCE: &str = "https://resource.example/api";
5520 const WRONG_RESOURCE: &str = "https://resource.example/other";
5521 let backend = Arc::new(CountingApprovalBackend::new(ApprovalTestMode::Exact));
5522 let server = server_with_counting_approval(Arc::clone(&backend));
5523 server
5524 .register_client(
5525 OAuthClient::builder("resource-client")
5526 .redirect_uri("http://127.0.0.1/callback")
5527 .scope("read")
5528 .build()
5529 .expect("bounded client"),
5530 )
5531 .expect("register client");
5532 let (code, _) = server
5533 .authorize(&approved_resource_request("resource-client"))
5534 .expect("approved authorization");
5535 let issued = server
5536 .token(&resource_code_exchange_request(
5537 "resource-client",
5538 &code,
5539 RESOURCE,
5540 ))
5541 .expect("exact resource code exchange");
5542 let refresh = issued.refresh_token.expect("refresh token");
5543 let cleanup_canary =
5544 insert_expired_authorization_code_cleanup_canary(&server, "resource-client");
5545 let before = server.stats();
5546
5547 let error = server
5548 .token(&resource_refresh_request(
5549 "resource-client",
5550 &refresh,
5551 WRONG_RESOURCE,
5552 ))
5553 .expect_err("only the resource differs");
5554
5555 assert!(matches!(error, OAuthError::InvalidGrant(_)));
5556 assert_oauth_stats_unchanged(&before, &server.stats());
5557 assert_eq!(backend.calls.load(Ordering::SeqCst), 1);
5558 {
5559 let state = server.state.read().expect("state");
5560 assert!(state.authorization_codes.contains_key(&cleanup_canary));
5561 assert_eq!(
5562 state
5563 .refresh_tokens
5564 .get(&refresh_token_digest(&refresh))
5565 .and_then(|token| token.resource.as_deref()),
5566 Some(RESOURCE)
5567 );
5568 }
5569
5570 let rotated = server
5571 .token(&resource_refresh_request(
5572 "resource-client",
5573 &refresh,
5574 RESOURCE,
5575 ))
5576 .expect("unchanged refresh remains usable with its exact resource");
5577 assert_eq!(
5578 server
5579 .validate_access_token(&rotated.access_token)
5580 .and_then(|token| token.resource),
5581 Some(RESOURCE.to_string())
5582 );
5583 }
5584
5585 #[test]
5586 fn authorization_code_resource_mismatch_rejects_before_expiry_cleanup() {
5587 const RESOURCE: &str = "https://resource.example/api";
5588 const WRONG_RESOURCE: &str = "https://resource.example/other";
5589 let backend = Arc::new(CountingApprovalBackend::new(ApprovalTestMode::Exact));
5590 let server = server_with_counting_approval(Arc::clone(&backend));
5591 server
5592 .register_client(
5593 OAuthClient::builder("resource-client")
5594 .redirect_uri("http://127.0.0.1/callback")
5595 .scope("read")
5596 .build()
5597 .expect("bounded client"),
5598 )
5599 .expect("register client");
5600 let (code, _) = server
5601 .authorize(&approved_resource_request("resource-client"))
5602 .expect("approved authorization");
5603 let code_digest = authorization_code_digest(&code);
5604 let cleanup_canary =
5605 insert_expired_authorization_code_cleanup_canary(&server, "resource-client");
5606 let before = server.stats();
5607
5608 let error = server
5609 .token(&resource_code_exchange_request(
5610 "resource-client",
5611 &code,
5612 WRONG_RESOURCE,
5613 ))
5614 .expect_err("only the requested resource differs");
5615
5616 assert!(matches!(error, OAuthError::InvalidGrant(_)));
5617 assert_oauth_stats_unchanged(&before, &server.stats());
5618 assert_eq!(backend.calls.load(Ordering::SeqCst), 1);
5619 let state = server.state.read().expect("state");
5620 assert!(state.authorization_codes.contains_key(&cleanup_canary));
5621 assert_eq!(
5622 state
5623 .authorization_codes
5624 .get(&code_digest)
5625 .and_then(|code| code.resource.as_deref()),
5626 Some(RESOURCE)
5627 );
5628 drop(state);
5629
5630 let issued = server
5631 .token(&resource_code_exchange_request(
5632 "resource-client",
5633 &code,
5634 RESOURCE,
5635 ))
5636 .expect("unchanged code remains exchangeable with its exact resource");
5637 assert_eq!(
5638 server
5639 .validate_access_token(&issued.access_token)
5640 .and_then(|token| token.resource),
5641 Some(RESOURCE.to_string())
5642 );
5643 }
5644
5645 #[test]
5646 fn authorization_approval_binding_generation_scope_and_resource_mismatches_do_not_mutate() {
5647 for mode in [
5648 ApprovalTestMode::WrongBinding,
5649 ApprovalTestMode::WrongGeneration,
5650 ApprovalTestMode::WrongScopes,
5651 ApprovalTestMode::WrongResource,
5652 ] {
5653 let backend = Arc::new(CountingApprovalBackend::new(mode));
5654 let server = server_with_counting_approval(Arc::clone(&backend));
5655 server
5656 .register_client(
5657 OAuthClient::builder("approval-client")
5658 .redirect_uri("http://127.0.0.1/callback")
5659 .scope("read")
5660 .build()
5661 .expect("bounded client"),
5662 )
5663 .expect("register client");
5664 let before = server.stats();
5665
5666 let error = server
5667 .authorize(&approved_resource_request("approval-client"))
5668 .expect_err("one changed approval fact must reject");
5669
5670 assert!(matches!(error, OAuthError::AccessDenied(_)));
5671 assert_eq!(backend.calls.load(Ordering::SeqCst), 1);
5672 assert_oauth_stats_unchanged(&before, &server.stats());
5673 }
5674 }
5675
5676 #[test]
5677 fn authorization_approval_denial_error_and_cancellation_do_not_create_codes() {
5678 for mode in [
5679 ApprovalTestMode::Denied,
5680 ApprovalTestMode::Error,
5681 ApprovalTestMode::Cancelled,
5682 ] {
5683 let backend = Arc::new(CountingApprovalBackend::new(mode));
5684 let server = server_with_counting_approval(Arc::clone(&backend));
5685 server
5686 .register_client(
5687 OAuthClient::builder("approval-client")
5688 .redirect_uri("http://127.0.0.1/callback")
5689 .scope("read")
5690 .build()
5691 .expect("bounded client"),
5692 )
5693 .expect("register client");
5694 let before = server.stats();
5695
5696 let error = server
5697 .authorize(&approved_resource_request("approval-client"))
5698 .expect_err("non-approved disposition must reject");
5699
5700 assert!(matches!(
5701 error,
5702 OAuthError::AccessDenied(_) | OAuthError::TemporarilyUnavailable(_)
5703 ));
5704 assert_eq!(backend.calls.load(Ordering::SeqCst), 1);
5705 assert_oauth_stats_unchanged(&before, &server.stats());
5706 }
5707 }
5708
5709 #[test]
5710 fn default_oauth_server_construction_is_fail_closed_without_an_approval_backend() {
5711 let server = OAuthServer::new(OAuthServerConfig::default());
5712 server
5713 .register_client(
5714 OAuthClient::builder("approval-client")
5715 .redirect_uri("http://127.0.0.1/callback")
5716 .scope("read")
5717 .build()
5718 .expect("bounded client"),
5719 )
5720 .expect("register client");
5721 let before = server.stats();
5722
5723 let error = server
5724 .authorize(&approved_resource_request("approval-client"))
5725 .expect_err("default construction must not silently approve");
5726
5727 assert!(matches!(error, OAuthError::AccessDenied(_)));
5728 assert_oauth_stats_unchanged(&before, &server.stats());
5729 }
5730
5731 fn bounded_refresh_request(client_id: &str, refresh_token: &str) -> TokenRequest {
5732 TokenRequest {
5733 grant_type: "refresh_token".to_string(),
5734 code: None,
5735 redirect_uri: None,
5736 client_id: client_id.to_string(),
5737 client_secret: None,
5738 code_verifier: None,
5739 refresh_token: Some(refresh_token.to_string()),
5740 scopes: None,
5741 resource: None,
5742 }
5743 }
5744
5745 fn bounded_code_exchange_request(client_id: &str, code: &str) -> TokenRequest {
5746 TokenRequest {
5747 grant_type: "authorization_code".to_string(),
5748 code: Some(code.to_string()),
5749 redirect_uri: Some("http://127.0.0.1/callback".to_string()),
5750 client_id: client_id.to_string(),
5751 client_secret: None,
5752 code_verifier: Some("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk".to_string()),
5753 refresh_token: None,
5754 scopes: None,
5755 resource: None,
5756 }
5757 }
5758
5759 #[test]
5760 fn test_client_builder() {
5761 let client = OAuthClient::builder("test-client")
5762 .redirect_uri("http://127.0.0.1:3000/callback")
5763 .scope("read")
5764 .scope("write")
5765 .name("Test Client")
5766 .build()
5767 .unwrap();
5768
5769 assert_eq!(client.client_id, "test-client");
5770 assert_eq!(client.client_type, ClientType::Public);
5771 assert_eq!(client.redirect_uris.len(), 1);
5772 assert!(client.allowed_scopes.contains("read"));
5773 assert!(client.allowed_scopes.contains("write"));
5774 }
5775
5776 #[test]
5777 fn client_retention_exact_maxima_build_and_register() {
5778 let client_id = exact_ascii_value("client-", MAX_OAUTH_CLIENT_ID_BYTES);
5779 let credential = exact_ascii_value("credential-", MAX_OAUTH_CLIENT_CREDENTIAL_BYTES);
5780 let redirect_uris: Vec<_> = (0..MAX_OAUTH_REDIRECT_URIS_PER_CLIENT)
5781 .map(|index| {
5782 exact_ascii_value(
5783 &format!("https://example.com/callback/{index}/"),
5784 MAX_OAUTH_REDIRECT_URI_BYTES,
5785 )
5786 })
5787 .collect();
5788 let scopes: Vec<_> = (0..MAX_OAUTH_SCOPES_PER_CLIENT)
5789 .map(|index| exact_ascii_value(&format!("scope-{index}-"), MAX_OAUTH_SCOPE_BYTES))
5790 .collect();
5791 let name = exact_ascii_value("name-", MAX_OAUTH_CLIENT_NAME_BYTES);
5792 let description = exact_ascii_value("description-", MAX_OAUTH_CLIENT_DESCRIPTION_BYTES);
5793
5794 let client = OAuthClient::builder(client_id.clone())
5795 .secret(credential)
5796 .redirect_uris(redirect_uris)
5797 .scopes(scopes)
5798 .name(name)
5799 .description(description)
5800 .build()
5801 .expect("every exact retention boundary is admitted");
5802 let server = OAuthServer::with_defaults();
5803 server
5804 .register_client(client)
5805 .expect("registration revalidation admits exact boundaries");
5806
5807 let state = server.state.read().unwrap();
5808 let retained = state.clients.get(&client_id).expect("retained client");
5809 assert_eq!(retained.metadata.client_id.len(), MAX_OAUTH_CLIENT_ID_BYTES);
5810 assert!(retained.secret_verifier.is_some());
5811 assert_eq!(
5812 retained.metadata.redirect_uris.len(),
5813 MAX_OAUTH_REDIRECT_URIS_PER_CLIENT
5814 );
5815 assert!(
5816 retained
5817 .metadata
5818 .redirect_uris
5819 .iter()
5820 .all(|uri| uri.len() == MAX_OAUTH_REDIRECT_URI_BYTES)
5821 );
5822 assert_eq!(
5823 retained.metadata.allowed_scopes.len(),
5824 MAX_OAUTH_SCOPES_PER_CLIENT
5825 );
5826 assert!(
5827 retained
5828 .metadata
5829 .allowed_scopes
5830 .iter()
5831 .all(|scope| scope.len() == MAX_OAUTH_SCOPE_BYTES)
5832 );
5833 assert_eq!(
5834 retained.metadata.name.as_deref().map(str::len),
5835 Some(MAX_OAUTH_CLIENT_NAME_BYTES)
5836 );
5837 assert_eq!(
5838 retained.metadata.description.as_deref().map(str::len),
5839 Some(MAX_OAUTH_CLIENT_DESCRIPTION_BYTES)
5840 );
5841 }
5842
5843 #[test]
5844 fn client_authentication_accepts_exact_bound_and_rejects_one_past() {
5845 let credential = exact_ascii_value("credential-", MAX_OAUTH_CLIENT_CREDENTIAL_BYTES);
5846 let client = OAuthClient::builder("confidential")
5847 .secret(credential.clone())
5848 .redirect_uri("https://example.com/callback")
5849 .build()
5850 .unwrap();
5851
5852 assert!(client.authenticate(Some(&credential)));
5853
5854 let mut same_length_wrong_credential = credential.clone();
5855 same_length_wrong_credential.pop();
5856 same_length_wrong_credential.push('y');
5857 assert!(!client.authenticate(Some(&same_length_wrong_credential)));
5858
5859 let one_past_credential = "x".repeat(MAX_OAUTH_CLIENT_CREDENTIAL_BYTES + 1);
5860 assert!(!client.authenticate(Some(&one_past_credential)));
5861 assert!(!client.authenticate(None));
5862 }
5863
5864 #[test]
5865 fn client_builder_rejects_one_past_every_retention_bound() {
5866 assert_client_build_error(
5867 OAuthClient::builder("x".repeat(MAX_OAUTH_CLIENT_ID_BYTES + 1))
5868 .redirect_uri("https://example.com/callback")
5869 .build(),
5870 OAUTH_CLIENT_ID_RETENTION_ERROR,
5871 );
5872 assert_client_build_error(
5873 OAuthClient::builder("client")
5874 .secret("x".repeat(MAX_OAUTH_CLIENT_CREDENTIAL_BYTES + 1))
5875 .redirect_uri("https://example.com/callback")
5876 .build(),
5877 OAUTH_CLIENT_CREDENTIAL_RETENTION_ERROR,
5878 );
5879 assert_client_build_error(
5880 OAuthClient::builder("client")
5881 .redirect_uris(
5882 (0..=MAX_OAUTH_REDIRECT_URIS_PER_CLIENT)
5883 .map(|index| format!("https://example.com/{index}")),
5884 )
5885 .build(),
5886 OAUTH_CLIENT_REDIRECT_COUNT_ERROR,
5887 );
5888 assert_client_build_error(
5889 OAuthClient::builder("client")
5890 .redirect_uri(exact_ascii_value(
5891 "https://example.com/",
5892 MAX_OAUTH_REDIRECT_URI_BYTES + 1,
5893 ))
5894 .build(),
5895 OAUTH_CLIENT_REDIRECT_VALUE_ERROR,
5896 );
5897 assert_client_build_error(
5898 OAuthClient::builder("client")
5899 .redirect_uri("https://example.com/callback")
5900 .scopes((0..=MAX_OAUTH_SCOPES_PER_CLIENT).map(|index| format!("scope-{index}")))
5901 .build(),
5902 OAUTH_CLIENT_SCOPE_COUNT_ERROR,
5903 );
5904 assert_client_build_error(
5905 OAuthClient::builder("client")
5906 .redirect_uri("https://example.com/callback")
5907 .scope("x".repeat(MAX_OAUTH_SCOPE_BYTES + 1))
5908 .build(),
5909 OAUTH_CLIENT_SCOPE_VALUE_ERROR,
5910 );
5911 assert_client_build_error(
5912 OAuthClient::builder("client")
5913 .redirect_uri("https://example.com/callback")
5914 .name("x".repeat(MAX_OAUTH_CLIENT_NAME_BYTES + 1))
5915 .build(),
5916 OAUTH_CLIENT_NAME_RETENTION_ERROR,
5917 );
5918 assert_client_build_error(
5919 OAuthClient::builder("client")
5920 .redirect_uri("https://example.com/callback")
5921 .description("x".repeat(MAX_OAUTH_CLIENT_DESCRIPTION_BYTES + 1))
5922 .build(),
5923 OAUTH_CLIENT_DESCRIPTION_RETENTION_ERROR,
5924 );
5925 assert_client_build_error(
5926 OAuthClient::builder("client")
5927 .secret("")
5928 .redirect_uri("https://example.com/callback")
5929 .build(),
5930 OAUTH_CLIENT_CREDENTIAL_RETENTION_ERROR,
5931 );
5932 assert_client_build_error(
5933 OAuthClient::builder("client").redirect_uri("").build(),
5934 OAUTH_CLIENT_REDIRECT_VALUE_ERROR,
5935 );
5936 assert_client_build_error(
5937 OAuthClient::builder("client")
5938 .redirect_uri("https://example.com/callback")
5939 .scope("")
5940 .build(),
5941 OAUTH_CLIENT_SCOPE_VALUE_ERROR,
5942 );
5943 }
5944
5945 #[test]
5946 fn client_builder_rejects_control_and_bidi_display_metadata() {
5947 for unsafe_value in [
5948 "line\nbreak",
5949 "right-to-left\u{202e}override",
5950 "isolated\u{2067}segment\u{2069}",
5951 "paragraph\u{2029}break",
5952 ] {
5953 assert_client_build_error(
5954 OAuthClient::builder("client")
5955 .redirect_uri("https://example.com/callback")
5956 .name(unsafe_value)
5957 .build(),
5958 OAUTH_CLIENT_NAME_RETENTION_ERROR,
5959 );
5960 assert_client_build_error(
5961 OAuthClient::builder("client")
5962 .redirect_uri("https://example.com/callback")
5963 .description(unsafe_value)
5964 .build(),
5965 OAUTH_CLIENT_DESCRIPTION_RETENTION_ERROR,
5966 );
5967 }
5968 }
5969
5970 #[test]
5971 fn client_builder_rejects_reserved_authorization_response_query_parameters() {
5972 for query in [
5973 "code=attacker",
5974 "state=attacker",
5975 "error=attacker",
5976 "error_description=attacker",
5977 "error_uri=https%3A%2F%2Fattacker.example",
5978 "iss=https%3A%2F%2Fattacker.example",
5979 "c%6fde=percent-encoded-name",
5980 "safe=value&state=attacker",
5981 "safe=value;error=attacker",
5982 ] {
5983 assert_client_build_error(
5984 OAuthClient::builder("client")
5985 .redirect_uri(format!("https://example.com/callback?{query}"))
5986 .build(),
5987 OAUTH_CLIENT_REDIRECT_VALUE_ERROR,
5988 );
5989 }
5990
5991 OAuthClient::builder("client")
5992 .redirect_uri("https://example.com/callback?safe=value")
5993 .build()
5994 .expect("unrelated redirect query parameters remain valid");
5995 }
5996
5997 #[test]
5998 fn registration_revalidates_every_publicly_mutable_client_bound() {
5999 assert_registration_rejects_mutation(
6000 |client| client.client_id = "x".repeat(MAX_OAUTH_CLIENT_ID_BYTES + 1),
6001 OAUTH_CLIENT_ID_RETENTION_ERROR,
6002 );
6003 assert_registration_rejects_mutation(
6004 |client| {
6005 client.client_secret = Some(ClientSecret::new(
6006 "x".repeat(MAX_OAUTH_CLIENT_CREDENTIAL_BYTES + 1),
6007 ));
6008 client.client_type = ClientType::Confidential;
6009 },
6010 OAUTH_CLIENT_CREDENTIAL_RETENTION_ERROR,
6011 );
6012 assert_registration_rejects_mutation(
6013 |client| {
6014 client.redirect_uris = vec![
6015 "https://example.com/callback".to_string();
6016 MAX_OAUTH_REDIRECT_URIS_PER_CLIENT + 1
6017 ];
6018 },
6019 OAUTH_CLIENT_REDIRECT_COUNT_ERROR,
6020 );
6021 assert_registration_rejects_mutation(
6022 |client| {
6023 client.redirect_uris = vec![exact_ascii_value(
6024 "https://example.com/",
6025 MAX_OAUTH_REDIRECT_URI_BYTES + 1,
6026 )];
6027 },
6028 OAUTH_CLIENT_REDIRECT_VALUE_ERROR,
6029 );
6030 assert_registration_rejects_mutation(
6031 |client| {
6032 client.allowed_scopes = (0..=MAX_OAUTH_SCOPES_PER_CLIENT)
6033 .map(|index| format!("scope-{index}"))
6034 .collect();
6035 },
6036 OAUTH_CLIENT_SCOPE_COUNT_ERROR,
6037 );
6038 assert_registration_rejects_mutation(
6039 |client| {
6040 client.allowed_scopes = HashSet::from(["x".repeat(MAX_OAUTH_SCOPE_BYTES + 1)]);
6041 },
6042 OAUTH_CLIENT_SCOPE_VALUE_ERROR,
6043 );
6044 assert_registration_rejects_mutation(
6045 |client| client.name = Some("x".repeat(MAX_OAUTH_CLIENT_NAME_BYTES + 1)),
6046 OAUTH_CLIENT_NAME_RETENTION_ERROR,
6047 );
6048 assert_registration_rejects_mutation(
6049 |client| client.name = Some("spoof\u{202e}name".to_string()),
6050 OAUTH_CLIENT_NAME_RETENTION_ERROR,
6051 );
6052 assert_registration_rejects_mutation(
6053 |client| {
6054 client.description = Some("x".repeat(MAX_OAUTH_CLIENT_DESCRIPTION_BYTES + 1));
6055 },
6056 OAUTH_CLIENT_DESCRIPTION_RETENTION_ERROR,
6057 );
6058 assert_registration_rejects_mutation(
6059 |client| client.client_type = ClientType::Confidential,
6060 OAUTH_CLIENT_CREDENTIAL_CLASS_ERROR,
6061 );
6062 assert_registration_rejects_mutation(
6063 |client| client.redirect_uris = vec!["javascript:alert(1)".to_string()],
6064 OAUTH_CLIENT_REDIRECT_VALUE_ERROR,
6065 );
6066 assert_registration_rejects_mutation(
6067 |client| {
6068 client.redirect_uris =
6069 vec!["https://example.com/callback?code=attacker".to_string()];
6070 },
6071 OAUTH_CLIENT_REDIRECT_VALUE_ERROR,
6072 );
6073 assert_registration_rejects_mutation(
6074 |client| client.allowed_scopes = HashSet::from(["read write".to_string()]),
6075 OAUTH_CLIENT_SCOPE_VALUE_ERROR,
6076 );
6077 }
6078
6079 #[test]
6080 fn authorization_retention_accepts_exact_bounds() {
6081 let scopes: Vec<_> = (0..MAX_OAUTH_SCOPES_PER_CLIENT)
6082 .map(|index| exact_ascii_value(&format!("scope-{index}-"), MAX_OAUTH_SCOPE_BYTES))
6083 .collect();
6084 let server = OAuthServer::with_defaults();
6085 let client = OAuthClient::builder("bounded")
6086 .redirect_uri("http://127.0.0.1/callback")
6087 .scopes(scopes.clone())
6088 .build()
6089 .unwrap();
6090 server.register_client(client).unwrap();
6091
6092 let mut request = bounded_authorization_request("bounded");
6093 request.scopes = scopes;
6094 request.state = Some("s".repeat(MAX_OAUTH_STATE_BYTES));
6095 let (code, _) = server.authorize(&request).unwrap();
6096
6097 let state = server.state.read().unwrap();
6098 let retained = state
6099 .authorization_codes
6100 .get(&authorization_code_digest(&code))
6101 .unwrap();
6102 assert_eq!(retained.scopes.len(), MAX_OAUTH_SCOPES_PER_CLIENT);
6103 assert_eq!(
6104 retained.state.as_ref().map(String::len),
6105 Some(MAX_OAUTH_STATE_BYTES)
6106 );
6107 assert_eq!(
6108 retained.subject.as_ref().map(String::len),
6109 Some("oauth-test-subject".len())
6110 );
6111 }
6112
6113 #[test]
6114 fn authorization_scope_duplicates_are_canonicalized_before_retention() {
6115 let server = OAuthServer::with_defaults();
6116 let client = OAuthClient::builder("bounded")
6117 .redirect_uri("http://127.0.0.1/callback")
6118 .scope("read")
6119 .scope("write")
6120 .build()
6121 .unwrap();
6122 server.register_client(client).unwrap();
6123
6124 let mut request = bounded_authorization_request("bounded");
6125 request.scopes = vec!["read".to_string(), "read".to_string(), "write".to_string()];
6126 let (code, _) = server.authorize(&request).unwrap();
6127
6128 let state = server.state.read().unwrap();
6129 assert_eq!(
6130 state
6131 .authorization_codes
6132 .get(&authorization_code_digest(&code))
6133 .unwrap()
6134 .scopes,
6135 vec!["read".to_string(), "write".to_string()]
6136 );
6137 }
6138
6139 #[test]
6140 fn authorization_retention_rejects_one_past_before_token_draw() {
6141 let server = OAuthServer::with_defaults();
6142 let client = OAuthClient::builder("bounded")
6143 .redirect_uri("http://127.0.0.1/callback")
6144 .scope("read")
6145 .build()
6146 .unwrap();
6147 server.register_client(client).unwrap();
6148 let draws = std::cell::Cell::new(0);
6149
6150 let request = bounded_authorization_request("bounded");
6151 let error = server
6152 .authorize_with_token_draw(
6153 &AuthorizationRequest {
6154 state: Some("s".repeat(MAX_OAUTH_STATE_BYTES + 1)),
6155 ..request.clone()
6156 },
6157 || {
6158 draws.set(draws.get() + 1);
6159 draw_security_identifier().map_err(|_| "unexpected RNG failure")
6160 },
6161 )
6162 .unwrap_err();
6163 assert_eq!(
6164 error.description(),
6165 OAUTH_AUTHORIZATION_STATE_RETENTION_ERROR
6166 );
6167 assert_eq!(draws.get(), 0);
6168
6169 let error = server
6170 .authorize_with_token_draw(
6171 &AuthorizationRequest {
6172 scopes: vec!["x".repeat(MAX_OAUTH_SCOPE_BYTES + 1)],
6173 ..request.clone()
6174 },
6175 || {
6176 draws.set(draws.get() + 1);
6177 draw_security_identifier().map_err(|_| "unexpected RNG failure")
6178 },
6179 )
6180 .unwrap_err();
6181 assert_eq!(error.description(), OAUTH_REQUEST_SCOPE_VALUE_ERROR);
6182 assert_eq!(draws.get(), 0);
6183
6184 let error = server
6185 .authorize_with_token_draw(
6186 &AuthorizationRequest {
6187 scopes: vec!["read".to_string(); MAX_OAUTH_SCOPES_PER_CLIENT + 1],
6188 ..request
6189 },
6190 || {
6191 draws.set(draws.get() + 1);
6192 draw_security_identifier().map_err(|_| "unexpected RNG failure")
6193 },
6194 )
6195 .unwrap_err();
6196 assert_eq!(error.description(), OAUTH_REQUEST_SCOPE_COUNT_ERROR);
6197 assert_eq!(draws.get(), 0);
6198 assert!(server.state.read().unwrap().authorization_codes.is_empty());
6199 }
6200
6201 #[test]
6202 fn test_confidential_client() {
6203 let client = OAuthClient::builder("test-client")
6204 .secret("super-secret")
6205 .redirect_uri("http://127.0.0.1:3000/callback")
6206 .build()
6207 .unwrap();
6208
6209 assert_eq!(client.client_type, ClientType::Confidential);
6210 assert!(client.authenticate(Some("super-secret")));
6211 assert!(!client.authenticate(Some("wrong-secret")));
6212 assert!(!client.authenticate(None));
6213 }
6214
6215 #[test]
6216 fn test_redirect_uri_validation() {
6217 let client = OAuthClient::builder("test-client")
6218 .redirect_uri("http://127.0.0.1:3000/callback")
6219 .redirect_uri("https://example.com/oauth/callback")
6220 .build()
6221 .unwrap();
6222
6223 assert!(client.validate_redirect_uri("http://127.0.0.1:3000/callback"));
6225 assert!(client.validate_redirect_uri("https://example.com/oauth/callback"));
6226
6227 assert!(client.validate_redirect_uri("http://127.0.0.1:8080/callback"));
6229
6230 assert!(!client.validate_redirect_uri("http://127.0.0.1:3000/other"));
6232 assert!(!client.validate_redirect_uri("http://localhost:3000/callback"));
6233 assert!(!client.validate_redirect_uri("https://evil.com/callback"));
6234 assert!(!client.validate_redirect_uri("http://localhost:3000@evil.example/callback"));
6235 assert!(!client.validate_redirect_uri("https://example.com/oauth/callback#fragment"));
6236
6237 let mut confused = OAuthClient::builder("confused")
6240 .redirect_uri("http://127.0.0.1/callback")
6241 .build()
6242 .unwrap();
6243 confused.redirect_uris = vec!["http://localhost:3000@evil.example/callback".to_string()];
6244 assert!(!confused.validate_redirect_uri("http://localhost:3000@evil.example/callback"));
6245
6246 let mut fragmented = OAuthClient::builder("fragmented")
6249 .redirect_uri("https://example.com/callback")
6250 .build()
6251 .unwrap();
6252 fragmented.redirect_uris =
6253 vec!["https://example.com/callback#registered-fragment".to_string()];
6254 assert!(
6255 !fragmented.validate_redirect_uri("https://example.com/callback#registered-fragment")
6256 );
6257 }
6258
6259 #[test]
6260 fn redirect_registration_rejects_unsafe_urls_with_fixed_error() {
6261 for uri in [
6262 "http://localhost:3000/callback",
6263 "http://example.com/callback",
6264 "http://127.1/callback",
6265 "http://0x7f000001/callback",
6266 "http://[0:0:0:0:0:0:0:1]/callback",
6267 "javascript:alert(1)",
6268 "/relative/callback",
6269 "https://user:password@example.com/callback",
6270 "https://@example.com/callback",
6271 "https://example.com/callback#fragment",
6272 "https://example.com/callback\r\nheader",
6273 ] {
6274 let error = OAuthClient::builder("client")
6275 .redirect_uri(uri)
6276 .build()
6277 .expect_err("unsafe redirect URI must fail closed");
6278 assert_eq!(error.description(), OAUTH_CLIENT_REDIRECT_VALUE_ERROR);
6279 assert!(!error.description().contains(uri));
6280 }
6281
6282 for uri in [
6283 "https://example.com/callback",
6284 "http://127.0.0.1:3000/callback",
6285 "http://[::1]:3000/callback",
6286 ] {
6287 assert!(
6288 OAuthClient::builder("client")
6289 .redirect_uri(uri)
6290 .build()
6291 .is_ok()
6292 );
6293 }
6294 }
6295
6296 #[test]
6297 fn loopback_redirect_exception_changes_only_the_port_bytes() {
6298 let client = OAuthClient::builder("native-client")
6299 .redirect_uri("http://127.0.0.1:3000/a/callback?resource=%2Fone&mode=x")
6300 .build()
6301 .unwrap();
6302
6303 assert!(
6304 client
6305 .validate_redirect_uri("http://127.0.0.1:49152/a/callback?resource=%2Fone&mode=x")
6306 );
6307 assert!(!client.validate_redirect_uri(
6308 "http://127.0.0.1:49152/a/../a/callback?resource=%2Fone&mode=x"
6309 ));
6310 assert!(
6311 !client
6312 .validate_redirect_uri("http://127.0.0.1:49152/a/callback?resource=%2fone&mode=x")
6313 );
6314 assert!(
6315 !client
6316 .validate_redirect_uri("http://127.0.0.1:49152/a/callback?mode=x&resource=%2Fone")
6317 );
6318 assert!(
6319 !client
6320 .validate_redirect_uri("http://127.0.0.1:049152/a/callback?resource=%2Fone&mode=x")
6321 );
6322 }
6323
6324 #[test]
6325 fn test_scope_validation() {
6326 let client = OAuthClient::builder("test-client")
6327 .redirect_uri("http://127.0.0.1:3000/callback")
6328 .scope("read")
6329 .scope("write")
6330 .build()
6331 .unwrap();
6332
6333 assert!(client.validate_scopes(&["read".to_string()]));
6334 assert!(client.validate_scopes(&["read".to_string(), "write".to_string()]));
6335 assert!(!client.validate_scopes(&["admin".to_string()]));
6336 }
6337
6338 #[test]
6339 fn test_oauth_server_client_registration() {
6340 let server = OAuthServer::with_defaults();
6341
6342 let client = OAuthClient::builder("test-client")
6343 .redirect_uri("http://127.0.0.1:3000/callback")
6344 .build()
6345 .unwrap();
6346
6347 server.register_client(client).unwrap();
6348
6349 let client2 = OAuthClient::builder("test-client")
6351 .redirect_uri("http://127.0.0.1:3000/callback")
6352 .build()
6353 .unwrap();
6354 assert!(server.register_client(client2).is_err());
6355
6356 assert!(server.get_client("test-client").is_some());
6358 assert!(server.get_client("nonexistent").is_none());
6359 }
6360
6361 #[test]
6362 fn server_client_reads_return_secret_free_metadata() {
6363 const SECRET: &str = "metadata-must-not-clone-this-client-secret";
6364 let server = OAuthServer::with_defaults();
6365 let client = OAuthClient::builder("confidential-client")
6366 .secret(SECRET)
6367 .redirect_uri("https://example.com/callback")
6368 .scope("read")
6369 .name("Confidential Client")
6370 .description("metadata read model")
6371 .build()
6372 .unwrap();
6373 let registered_at = client.registered_at;
6374 server.register_client(client).unwrap();
6375
6376 let metadata = server
6377 .get_client("confidential-client")
6378 .expect("registered client metadata");
6379 assert_eq!(metadata.client_id, "confidential-client");
6380 assert_eq!(metadata.client_type, ClientType::Confidential);
6381 assert_eq!(metadata.registered_at, registered_at);
6382 assert_eq!(metadata.redirect_uris, ["https://example.com/callback"]);
6383 assert!(metadata.allowed_scopes.contains("read"));
6384 assert_eq!(metadata.name.as_deref(), Some("Confidential Client"));
6385 assert_eq!(metadata.description.as_deref(), Some("metadata read model"));
6386 assert!(!format!("{metadata:?}").contains(SECRET));
6387
6388 let listed = server.list_clients();
6389 assert_eq!(listed, [metadata]);
6390 assert!(!format!("{listed:?}").contains(SECRET));
6391 }
6392
6393 #[test]
6394 fn registration_replaces_plaintext_client_secret_with_verifier() {
6395 const SECRET: &str = "registration-only-confidential-secret";
6396 let server = OAuthServer::with_defaults();
6397 let client = OAuthClient::builder("confidential-client")
6398 .secret(SECRET)
6399 .redirect_uri("https://example.com/callback")
6400 .build()
6401 .unwrap();
6402 assert!(client.has_client_secret());
6403 server.register_client(client).unwrap();
6404
6405 let state = server.state.read().unwrap();
6406 let registered = state.clients.get("confidential-client").unwrap();
6407 let verifier = registered.secret_verifier.expect("stored verifier");
6408 assert!(verifier.verify(SECRET.as_bytes()));
6409 assert!(!verifier.verify(b"wrong-secret"));
6410 assert!(!format!("{verifier:?}").contains(SECRET));
6411 assert_eq!(registered.metadata.client_type, ClientType::Confidential);
6412 }
6413
6414 #[test]
6415 fn public_client_rejects_supplied_secret_before_grant_access() {
6416 let server = OAuthServer::with_defaults();
6417 server
6418 .register_client(bounded_test_client("public"))
6419 .unwrap();
6420 let (code, _) = server
6421 .authorize(&bounded_authorization_request("public"))
6422 .unwrap();
6423 let mut exchange = bounded_code_exchange_request("public", &code);
6424 exchange.client_secret = Some("must-not-be-accepted".to_string());
6425
6426 let exchange_error = server.token(&exchange).unwrap_err();
6427 assert_eq!(exchange_error.error_code(), "invalid_client");
6428 assert_eq!(
6429 exchange_error.description(),
6430 OAUTH_CLIENT_AUTHENTICATION_ERROR
6431 );
6432 assert!(
6433 server
6434 .state
6435 .read()
6436 .unwrap()
6437 .authorization_codes
6438 .contains_key(&authorization_code_digest(&code))
6439 );
6440
6441 let issued = issue_access_token_via_auth_code(
6442 &server,
6443 "public",
6444 "http://127.0.0.1/callback",
6445 &[],
6446 "subject",
6447 );
6448 let refresh = issued.refresh_token.as_deref().unwrap();
6449 let mut refresh_request = bounded_refresh_request("public", refresh);
6450 refresh_request.client_secret = Some("must-not-be-accepted".to_string());
6451 let refresh_error = server.token(&refresh_request).unwrap_err();
6452 assert_eq!(refresh_error.error_code(), "invalid_client");
6453 assert_eq!(refresh_error.description(), exchange_error.description());
6454
6455 let revoke_error = server
6456 .revoke(&issued.access_token, "public", Some("must-not-be-accepted"))
6457 .unwrap_err();
6458 assert_eq!(revoke_error.error_code(), "invalid_client");
6459 assert_eq!(revoke_error.description(), exchange_error.description());
6460 assert!(server.validate_access_token(&issued.access_token).is_some());
6461 }
6462
6463 #[test]
6464 fn invalid_client_response_precedes_code_and_refresh_lookup() {
6465 let server = OAuthServer::with_defaults();
6466 let client = OAuthClient::builder("confidential")
6467 .secret("correct-secret")
6468 .redirect_uri("http://127.0.0.1/callback")
6469 .build()
6470 .unwrap();
6471 server.register_client(client).unwrap();
6472
6473 let absent_credential = base64url_encode(&[0x77_u8; 32]);
6474 let mut code_request = bounded_code_exchange_request("confidential", &absent_credential);
6475 code_request.client_secret = Some("wrong-secret".to_string());
6476 let wrong_secret = server.token(&code_request).unwrap_err();
6477
6478 code_request.client_id = "unknown-client".to_string();
6479 let unknown_client = server.token(&code_request).unwrap_err();
6480 assert_eq!(wrong_secret.error_code(), "invalid_client");
6481 assert_eq!(unknown_client.error_code(), "invalid_client");
6482 assert_eq!(wrong_secret.description(), unknown_client.description());
6483 assert_eq!(
6484 wrong_secret.description(),
6485 OAUTH_CLIENT_AUTHENTICATION_ERROR
6486 );
6487
6488 let mut refresh_request = bounded_refresh_request("confidential", &absent_credential);
6489 refresh_request.client_secret = Some("wrong-secret".to_string());
6490 let refresh_error = server.token(&refresh_request).unwrap_err();
6491 assert_eq!(refresh_error.error_code(), "invalid_client");
6492 assert_eq!(refresh_error.description(), wrong_secret.description());
6493 }
6494
6495 #[test]
6496 fn test_authorization_flow() {
6497 let server = OAuthServer::with_defaults();
6498
6499 let client = OAuthClient::builder("test-client")
6500 .redirect_uri("http://127.0.0.1:3000/callback")
6501 .scope("read")
6502 .build()
6503 .unwrap();
6504 server.register_client(client).unwrap();
6505
6506 let request = AuthorizationRequest {
6508 response_type: "code".to_string(),
6509 client_id: "test-client".to_string(),
6510 redirect_uri: "http://127.0.0.1:3000/callback".to_string(),
6511 scopes: vec!["read".to_string()],
6512 resource: None,
6513 state: Some("xyz".to_string()),
6514 code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM".to_string(),
6515 code_challenge_method: CodeChallengeMethod::S256,
6516 };
6517
6518 let (code, redirect) = server.authorize(&request).unwrap();
6519
6520 assert!(!code.is_empty());
6521 assert!(redirect.contains("code="));
6522 assert!(redirect.contains("state=xyz"));
6523 }
6524
6525 #[test]
6526 fn test_pkce_required() {
6527 let server = OAuthServer::with_defaults();
6528
6529 let client = OAuthClient::builder("test-client")
6530 .redirect_uri("http://127.0.0.1:3000/callback")
6531 .build()
6532 .unwrap();
6533 server.register_client(client).unwrap();
6534
6535 let request = AuthorizationRequest {
6537 response_type: "code".to_string(),
6538 client_id: "test-client".to_string(),
6539 redirect_uri: "http://127.0.0.1:3000/callback".to_string(),
6540 scopes: vec![],
6541 resource: None,
6542 state: None,
6543 code_challenge: String::new(), code_challenge_method: CodeChallengeMethod::S256,
6545 };
6546
6547 let result = server.authorize(&request);
6548 assert!(matches!(result, Err(OAuthError::InvalidRequest(_))));
6549 }
6550
6551 #[test]
6552 fn authorization_path_rejects_plain_and_malformed_s256_challenges() {
6553 let server = OAuthServer::with_defaults();
6554 let client = OAuthClient::builder("test-client")
6555 .redirect_uri("http://127.0.0.1:3000/callback")
6556 .build()
6557 .unwrap();
6558 server.register_client(client).unwrap();
6559
6560 let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
6561 let plain = AuthorizationRequest {
6562 response_type: "code".to_string(),
6563 client_id: "test-client".to_string(),
6564 redirect_uri: "http://127.0.0.1:3000/callback".to_string(),
6565 scopes: vec![],
6566 resource: None,
6567 state: None,
6568 code_challenge: verifier.to_string(),
6569 code_challenge_method: CodeChallengeMethod::Plain,
6570 };
6571 let error = server.authorize(&plain).unwrap_err();
6572 assert!(matches!(error, OAuthError::InvalidRequest(_)));
6573
6574 let malformed_s256 = AuthorizationRequest {
6575 code_challenge: "!".repeat(43),
6576 code_challenge_method: CodeChallengeMethod::S256,
6577 ..plain
6578 };
6579 let error = server.authorize(&malformed_s256).unwrap_err();
6580 assert!(matches!(error, OAuthError::InvalidRequest(_)));
6581 assert!(server.state.read().unwrap().authorization_codes.is_empty());
6582 }
6583
6584 #[test]
6585 fn authorization_approval_decision_rejects_empty_subject() {
6586 let request = AuthorizationApprovalRequest {
6587 binding: AuthorizationApprovalBinding {
6588 client_id: "c1".to_string(),
6589 redirect_uri: "http://127.0.0.1/callback".to_string(),
6590 scopes: Vec::new(),
6591 resource: None,
6592 state: None,
6593 code_challenge: compute_s256_challenge(
6594 "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
6595 )
6596 .expect("valid challenge"),
6597 code_challenge_method: CodeChallengeMethod::S256,
6598 registration_epoch: test_registration_epoch(1),
6599 },
6600 };
6601
6602 let error = request
6603 .approve(
6604 String::new(),
6605 Vec::new(),
6606 None,
6607 AuthorizationApprovalGeneration::from_bytes([1; 32]),
6608 )
6609 .expect_err("an empty subject is not a usable approval identity");
6610
6611 assert!(matches!(error, OAuthError::InvalidRequest(_)));
6612 }
6613
6614 #[test]
6615 fn direct_issuance_rejects_empty_subject_before_token_draw() {
6616 let server = OAuthServer::with_defaults();
6617 server.register_client(bounded_test_client("c1")).unwrap();
6618 let draws = std::cell::Cell::new(0);
6619
6620 let error = server
6621 .issue_tokens_with_draw("c1", &[], Some(""), || {
6622 draws.set(draws.get() + 1);
6623 draw_security_identifier().map_err(|_| "unexpected RNG failure")
6624 })
6625 .expect_err("an empty subject is not a usable owner identity");
6626
6627 assert!(matches!(error, OAuthError::InvalidRequest(_)));
6628 assert_eq!(draws.get(), 0);
6629 let state = server.state.read().unwrap();
6630 assert!(state.access_tokens.is_empty());
6631 assert!(state.refresh_tokens.is_empty());
6632 }
6633
6634 #[test]
6635 fn authorization_rejects_client_reregistration_during_code_draw() {
6636 let server = Arc::new(OAuthServer::with_defaults());
6637 server.register_client(bounded_test_client("c1")).unwrap();
6638 let request = bounded_authorization_request("c1");
6639 let (draw_started_tx, draw_started_rx) = std::sync::mpsc::sync_channel(0);
6640 let (resume_draw_tx, resume_draw_rx) = std::sync::mpsc::sync_channel(0);
6641 let authorizing_server = Arc::clone(&server);
6642
6643 let authorizing = std::thread::spawn(move || {
6644 authorizing_server.authorize_with_token_draw(&request, || {
6645 draw_started_tx.send(()).expect("signal code draw");
6646 resume_draw_rx.recv().expect("resume code draw");
6647 draw_security_identifier().map_err(|_| "unexpected RNG failure")
6648 })
6649 });
6650
6651 draw_started_rx.recv().expect("authorization reached draw");
6652 server.unregister_client("c1").unwrap();
6653 server.register_client(bounded_test_client("c1")).unwrap();
6654 resume_draw_tx.send(()).expect("resume authorization");
6655 let error = authorizing
6656 .join()
6657 .expect("authorization thread")
6658 .expect_err("old authorization must not transfer to a new registration");
6659
6660 assert!(matches!(error, OAuthError::InvalidClient(_)));
6661 assert!(server.state.read().unwrap().authorization_codes.is_empty());
6662 }
6663
6664 #[test]
6665 fn test_token_generation() {
6666 let value = generate_token().unwrap();
6667
6668 assert_eq!(value.len(), 43);
6669 assert!(!value.contains('='));
6670 assert!(
6671 value
6672 .chars()
6673 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
6674 );
6675 }
6676
6677 #[test]
6678 fn opaque_credential_digests_are_domain_separated_and_redacted() {
6679 let value = base64url_encode(&[0x42_u8; 32]);
6680 let authorization_code = authorization_code_digest(&value);
6681 let access_token = access_token_digest(&value);
6682 let refresh_token = refresh_token_digest(&value);
6683
6684 assert_ne!(authorization_code, access_token);
6685 assert_ne!(authorization_code, refresh_token);
6686 assert_ne!(access_token, refresh_token);
6687 assert!(!format!("{authorization_code:?}").contains(&value));
6688 assert!(validate_opaque_credential(&value, "invalid").is_ok());
6689 assert!(validate_opaque_credential(&format!("{value}="), "invalid").is_err());
6690 assert!(validate_opaque_credential(&value[..42], "invalid").is_err());
6691 }
6692
6693 #[test]
6694 fn pkce_s256_matches_rfc_7636_and_enforces_fixed_input() {
6695 let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
6696 let challenge = compute_s256_challenge(verifier).unwrap();
6697 assert_eq!(challenge, "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM");
6698 assert!(validate_s256_code_challenge(&challenge).is_ok());
6699 assert!(validate_s256_code_challenge("short").is_err());
6700 assert!(validate_s256_code_challenge(&"!".repeat(43)).is_err());
6701 let noncanonical = format!("{}N", &challenge[..42]);
6702 assert!(validate_s256_code_challenge(&noncanonical).is_err());
6703
6704 assert!(compute_s256_challenge(&"A".repeat(43)).is_ok());
6705 assert!(compute_s256_challenge(&"~".repeat(128)).is_ok());
6706 assert!(compute_s256_challenge(&"A".repeat(42)).is_err());
6707 assert!(compute_s256_challenge(&"A".repeat(129)).is_err());
6708 assert!(compute_s256_challenge(&format!("{}%", "A".repeat(42))).is_err());
6709 assert!(compute_s256_challenge(&"é".repeat(43)).is_err());
6710 }
6711
6712 #[test]
6713 fn authorization_draw_failure_precedes_code_storage() {
6714 let server = OAuthServer::with_defaults();
6715 let client = OAuthClient::builder("test-client")
6716 .redirect_uri("http://127.0.0.1:3000/callback")
6717 .build()
6718 .unwrap();
6719 server.register_client(client).unwrap();
6720
6721 let request = AuthorizationRequest {
6722 response_type: "code".to_string(),
6723 client_id: "test-client".to_string(),
6724 redirect_uri: "http://127.0.0.1:3000/callback".to_string(),
6725 scopes: vec![],
6726 resource: None,
6727 state: None,
6728 code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM".to_string(),
6729 code_challenge_method: CodeChallengeMethod::S256,
6730 };
6731 let draw_calls = std::cell::Cell::new(0);
6732
6733 let result = server.authorize_with_token_draw(&request, || {
6734 draw_calls.set(draw_calls.get() + 1);
6735 Err::<SecurityIdentifier, _>("forced security-identifier draw failure")
6736 });
6737
6738 assert!(matches!(result, Err(OAuthError::ServerError(_))));
6739 assert_eq!(draw_calls.get(), 1);
6740 assert!(server.state.read().unwrap().authorization_codes.is_empty());
6741 }
6742
6743 #[test]
6744 fn authorization_lifetime_starts_after_credential_generation() {
6745 let server = OAuthServer::with_defaults();
6746 server.register_client(bounded_test_client("c1")).unwrap();
6747 let final_draw_completed = std::cell::Cell::new(None);
6748
6749 let (code, _) = server
6750 .authorize_with_token_draw(&bounded_authorization_request("c1"), || {
6751 let identifier = draw_security_identifier()
6752 .map_err(|_| "unexpected operating-system RNG failure")?;
6753 final_draw_completed.set(Some(Instant::now()));
6754 Ok::<_, &str>(identifier)
6755 })
6756 .expect("authorization succeeds");
6757
6758 let state = server.state.read().unwrap();
6759 let stored = state
6760 .authorization_codes
6761 .get(&authorization_code_digest(&code))
6762 .expect("stored authorization code");
6763 assert!(
6764 stored.issued_at
6765 >= final_draw_completed
6766 .get()
6767 .expect("draw completion timestamp")
6768 );
6769 assert_eq!(
6770 stored
6771 .expires_at
6772 .saturating_duration_since(stored.issued_at),
6773 server.config.authorization_code_lifetime
6774 );
6775 }
6776
6777 #[test]
6778 fn second_token_draw_failure_commits_neither_token() {
6779 let server = OAuthServer::with_defaults();
6780 server
6781 .register_client(bounded_test_client("client"))
6782 .unwrap();
6783 let draw_calls = std::cell::Cell::new(0);
6784
6785 let result = server.issue_tokens_with_draw("client", &[], None, || {
6786 let call = draw_calls.get() + 1;
6787 draw_calls.set(call);
6788 if call == 1 {
6789 draw_security_identifier().map_err(|_| "unexpected operating-system RNG failure")
6790 } else {
6791 Err("forced second security-identifier draw failure")
6792 }
6793 });
6794
6795 assert!(matches!(result, Err(OAuthError::ServerError(_))));
6796 assert_eq!(draw_calls.get(), 2);
6797 let state = server.state.read().unwrap();
6798 assert!(state.access_tokens.is_empty());
6799 assert!(state.refresh_tokens.is_empty());
6800 }
6801
6802 #[test]
6803 fn token_pair_consumes_two_fresh_security_identifier_draws() {
6804 let server = OAuthServer::with_defaults();
6805 server
6806 .register_client(bounded_test_client("client"))
6807 .unwrap();
6808 let draw_calls = std::cell::Cell::new(0);
6809 let final_draw_completed = std::cell::Cell::new(None);
6810
6811 let response = server
6812 .issue_tokens_with_draw("client", &[], None, || {
6813 let call = draw_calls.get() + 1;
6814 draw_calls.set(call);
6815 let identifier = draw_security_identifier()
6816 .map_err(|_| "unexpected operating-system RNG failure")?;
6817 if call == 2 {
6818 final_draw_completed.set(Some(Instant::now()));
6819 }
6820 Ok::<_, &str>(identifier)
6821 })
6822 .unwrap();
6823
6824 assert_eq!(draw_calls.get(), 2);
6825 assert_eq!(response.access_token.len(), 43);
6826 assert_eq!(response.refresh_token.as_deref().unwrap().len(), 43);
6827 let state = server.state.read().unwrap();
6828 assert_eq!(state.access_tokens.len(), 1);
6829 assert_eq!(state.refresh_tokens.len(), 1);
6830 let access = state
6831 .access_tokens
6832 .get(&access_token_digest(&response.access_token))
6833 .expect("returned access token was committed");
6834 let refresh = state
6835 .refresh_tokens
6836 .get(&refresh_token_digest(
6837 response
6838 .refresh_token
6839 .as_deref()
6840 .expect("token pair contains refresh token"),
6841 ))
6842 .expect("returned refresh token was committed");
6843 assert!(access.token.is_empty());
6844 assert!(refresh.token.is_empty());
6845 let final_draw_completed = final_draw_completed
6846 .get()
6847 .expect("second draw completion timestamp");
6848 assert!(access.metadata.issued_at >= final_draw_completed);
6849 assert!(refresh.metadata.issued_at >= final_draw_completed);
6850 }
6851
6852 #[test]
6853 fn refresh_access_draw_failure_preserves_existing_token_state() {
6854 let server = OAuthServer::with_defaults();
6855 let client = OAuthClient::builder("client")
6856 .redirect_uri("http://127.0.0.1/callback")
6857 .build()
6858 .unwrap();
6859 server.register_client(client).unwrap();
6860 let issued = server.issue_tokens("client", &[], Some("subject")).unwrap();
6861 let refresh_token = issued.refresh_token.unwrap();
6862 let request = TokenRequest {
6863 grant_type: "refresh_token".to_string(),
6864 code: None,
6865 redirect_uri: None,
6866 client_id: "client".to_string(),
6867 client_secret: None,
6868 code_verifier: None,
6869 refresh_token: Some(refresh_token.clone()),
6870 scopes: None,
6871 resource: None,
6872 };
6873 let draw_calls = std::cell::Cell::new(0);
6874
6875 let result = server.token_refresh_token_with_draw(&request, || {
6876 draw_calls.set(draw_calls.get() + 1);
6877 Err::<SecurityIdentifier, _>("forced refresh access-token draw failure")
6878 });
6879
6880 assert!(matches!(result, Err(OAuthError::ServerError(_))));
6881 assert_eq!(draw_calls.get(), 1);
6882 let state = server.state.read().unwrap();
6883 assert_eq!(state.access_tokens.len(), 1);
6884 assert_eq!(state.refresh_tokens.len(), 1);
6885 assert!(
6886 state
6887 .refresh_tokens
6888 .contains_key(&refresh_token_digest(&refresh_token))
6889 );
6890 }
6891
6892 #[test]
6893 fn test_base64url_encode() {
6894 assert_eq!(base64url_encode(b""), "");
6896 assert_eq!(base64url_encode(b"f"), "Zg");
6897 assert_eq!(base64url_encode(b"fo"), "Zm8");
6898 assert_eq!(base64url_encode(b"foo"), "Zm9v");
6899 assert_eq!(base64url_encode(b"foob"), "Zm9vYg");
6900 assert_eq!(base64url_encode(b"fooba"), "Zm9vYmE");
6901 assert_eq!(base64url_encode(b"foobar"), "Zm9vYmFy");
6902 }
6903
6904 #[test]
6905 fn test_url_encode() {
6906 assert_eq!(url_encode("hello"), "hello");
6907 assert_eq!(url_encode("hello world"), "hello%20world");
6908 assert_eq!(url_encode("a=b&c=d"), "a%3Db%26c%3Dd");
6909 }
6910
6911 #[test]
6912 fn test_constant_time_eq() {
6913 assert!(constant_time_eq("hello", "hello"));
6914 assert!(!constant_time_eq("hello", "world"));
6915 assert!(!constant_time_eq("hello", "hell"));
6916 }
6917
6918 #[test]
6919 fn test_loopback_match() {
6920 assert!(loopback_match(
6921 "http://127.0.0.1:3000/callback",
6922 "http://127.0.0.1:8080/callback"
6923 ));
6924 assert!(!loopback_match(
6925 "http://127.0.0.1:3000/callback",
6926 "http://localhost:8080/callback"
6927 ));
6928 assert!(!loopback_match(
6929 "http://127.0.0.1:3000/callback",
6930 "http://127.0.0.1:3000/other"
6931 ));
6932 }
6933
6934 #[test]
6935 fn test_oauth_server_stats() {
6936 let server = OAuthServer::with_defaults();
6937
6938 let stats = server.stats();
6939 assert_eq!(stats.clients, 0);
6940 assert_eq!(stats.access_tokens, 0);
6941
6942 let client = OAuthClient::builder("test-client")
6943 .redirect_uri("http://127.0.0.1:3000/callback")
6944 .build()
6945 .unwrap();
6946 server.register_client(client).unwrap();
6947
6948 let stats = server.stats();
6949 assert_eq!(stats.clients, 1);
6950 }
6951
6952 #[test]
6953 fn test_code_challenge_method_parse() {
6954 assert_eq!(
6955 CodeChallengeMethod::parse("plain"),
6956 Some(CodeChallengeMethod::Plain)
6957 );
6958 assert_eq!(
6959 CodeChallengeMethod::parse("S256"),
6960 Some(CodeChallengeMethod::S256)
6961 );
6962 assert_eq!(CodeChallengeMethod::parse("unknown"), None);
6963 }
6964
6965 #[test]
6966 fn test_oauth_error_display() {
6967 let err = OAuthError::InvalidRequest("missing parameter".to_string());
6968 assert_eq!(err.error_code(), "invalid_request");
6969 assert_eq!(err.description(), "missing parameter");
6970 assert_eq!(err.to_string(), "invalid_request: missing parameter");
6971 }
6972
6973 #[test]
6974 fn test_token_revocation() {
6975 let server = Arc::new(OAuthServer::with_defaults());
6976
6977 let client = OAuthClient::builder("test-client")
6979 .redirect_uri("http://127.0.0.1:3000/callback")
6980 .scope("read")
6981 .build()
6982 .unwrap();
6983 server.register_client(client).unwrap();
6984
6985 let token_response = issue_access_token_via_auth_code(
6986 server.as_ref(),
6987 "test-client",
6988 "http://127.0.0.1:3000/callback",
6989 &["read"],
6990 "user123",
6991 );
6992
6993 assert!(
6995 server
6996 .validate_access_token(&token_response.access_token)
6997 .is_some()
6998 );
6999
7000 server
7002 .revoke(&token_response.access_token, "test-client", None)
7003 .unwrap();
7004
7005 assert!(
7007 server
7008 .validate_access_token(&token_response.access_token)
7009 .is_none()
7010 );
7011 }
7012
7013 #[test]
7014 fn test_client_unregistration() {
7015 let server = OAuthServer::with_defaults();
7016
7017 let client = OAuthClient::builder("test-client")
7018 .redirect_uri("http://127.0.0.1:3000/callback")
7019 .build()
7020 .unwrap();
7021 server.register_client(client).unwrap();
7022
7023 assert!(server.get_client("test-client").is_some());
7024
7025 server.unregister_client("test-client").unwrap();
7026
7027 assert!(server.get_client("test-client").is_none());
7028
7029 assert!(server.unregister_client("test-client").is_err());
7031 }
7032
7033 #[test]
7034 fn test_token_verifier() {
7035 let server = Arc::new(OAuthServer::with_defaults());
7036
7037 let client = OAuthClient::builder("test-client")
7039 .redirect_uri("http://127.0.0.1:3000/callback")
7040 .scope("read")
7041 .build()
7042 .unwrap();
7043 server.register_client(client).unwrap();
7044
7045 let token_response = issue_access_token_via_auth_code(
7046 server.as_ref(),
7047 "test-client",
7048 "http://127.0.0.1:3000/callback",
7049 &["read"],
7050 "user123",
7051 );
7052
7053 let verifier = server.token_verifier();
7055 let cx = asupersync::Cx::for_testing();
7056 let mcp_ctx = McpContext::new(cx, 1);
7057 let auth_request = AuthRequest {
7058 method: "test",
7059 params: None,
7060 transport_authorization: None,
7061 request_id: 1,
7062 };
7063
7064 let access = AccessToken {
7066 scheme: "Bearer".to_string(),
7067 token: token_response.access_token.clone(),
7068 };
7069 let result = verifier.verify(&mcp_ctx, auth_request, &access);
7070 assert!(result.is_ok());
7071 let auth = result.unwrap();
7072 assert_eq!(auth.subject, Some("oauth-test-subject".to_string()));
7073 assert_eq!(auth.scopes, vec!["read".to_string()]);
7074
7075 let invalid = AccessToken {
7077 scheme: "Bearer".to_string(),
7078 token: "invalid-value".to_string(),
7079 };
7080 let result = verifier.verify(&mcp_ctx, auth_request, &invalid);
7081 assert!(result.is_err());
7082
7083 let wrong_scheme = AccessToken {
7085 scheme: "Basic".to_string(),
7086 token: token_response.access_token,
7087 };
7088 let result = verifier.verify(&mcp_ctx, auth_request, &wrong_scheme);
7089 assert!(result.is_err());
7090 }
7091
7092 #[test]
7097 fn config_default_values() {
7098 let c = OAuthServerConfig::default();
7099 assert_eq!(c.issuer, "https://fastmcp.invalid/");
7100 assert_eq!(c.access_token_lifetime, Duration::from_mins(15));
7101 assert_eq!(c.refresh_token_lifetime, Duration::from_hours(720));
7102 assert_eq!(c.authorization_code_lifetime, Duration::from_mins(5));
7103 assert!(c.allow_public_clients);
7104 assert_eq!(c.min_code_verifier_length, 43);
7105 assert_eq!(c.max_code_verifier_length, 128);
7106 assert_eq!(c.max_clients, DEFAULT_MAX_OAUTH_CLIENTS);
7107 assert_eq!(c.max_authorization_codes, DEFAULT_MAX_AUTHORIZATION_CODES);
7108 assert_eq!(
7109 c.max_authorization_codes_per_client,
7110 DEFAULT_MAX_AUTHORIZATION_CODES_PER_CLIENT
7111 );
7112 assert_eq!(c.max_access_tokens, DEFAULT_MAX_ACCESS_TOKENS);
7113 assert_eq!(
7114 c.max_access_tokens_per_client,
7115 DEFAULT_MAX_ACCESS_TOKENS_PER_CLIENT
7116 );
7117 assert_eq!(c.max_refresh_tokens, DEFAULT_MAX_REFRESH_TOKENS);
7118 assert_eq!(
7119 c.max_refresh_tokens_per_client,
7120 DEFAULT_MAX_REFRESH_TOKENS_PER_CLIENT
7121 );
7122 assert_eq!(
7123 c.max_revocation_tombstones,
7124 DEFAULT_MAX_REVOCATION_TOMBSTONES
7125 );
7126 assert_eq!(
7127 c.max_revocation_tombstones_per_client,
7128 DEFAULT_MAX_REVOCATION_TOMBSTONES_PER_CLIENT
7129 );
7130 assert!(c.validate().is_ok());
7131 }
7132
7133 #[test]
7134 fn config_debug_and_clone() {
7135 let c = OAuthServerConfig::default();
7136 let debug = format!("{:?}", c);
7137 assert!(debug.contains("OAuthServerConfig"));
7138 assert!(debug.contains("https://fastmcp.invalid"));
7139
7140 let cloned = c.clone();
7141 assert_eq!(cloned.issuer, "https://fastmcp.invalid/");
7142 }
7143
7144 #[test]
7145 fn config_enforces_rfc7636_verifier_bounds_and_ordering() {
7146 let invalid = [
7147 OAuthServerConfig {
7148 min_code_verifier_length: PKCE_CODE_VERIFIER_MIN_BYTES - 1,
7149 ..OAuthServerConfig::default()
7150 },
7151 OAuthServerConfig {
7152 max_code_verifier_length: PKCE_CODE_VERIFIER_MAX_BYTES + 1,
7153 ..OAuthServerConfig::default()
7154 },
7155 OAuthServerConfig {
7156 min_code_verifier_length: PKCE_CODE_VERIFIER_MAX_BYTES,
7157 max_code_verifier_length: PKCE_CODE_VERIFIER_MIN_BYTES,
7158 ..OAuthServerConfig::default()
7159 },
7160 ];
7161 for config in invalid {
7162 let error = config.validate().expect_err("invalid PKCE policy");
7163 assert!(matches!(&error, OAuthError::ServerError(_)));
7164 assert!(error.description().contains("PKCE verifier bounds"));
7165 }
7166
7167 OAuthServerConfig {
7168 min_code_verifier_length: PKCE_CODE_VERIFIER_MIN_BYTES,
7169 max_code_verifier_length: PKCE_CODE_VERIFIER_MIN_BYTES,
7170 ..OAuthServerConfig::default()
7171 }
7172 .validate()
7173 .expect("a coherent strict RFC 7636 subset is valid");
7174 }
7175
7176 #[test]
7177 fn config_enforces_nonzero_bounded_coherent_lifetimes() {
7178 let invalid = [
7179 OAuthServerConfig {
7180 access_token_lifetime: Duration::ZERO,
7181 ..OAuthServerConfig::default()
7182 },
7183 OAuthServerConfig {
7184 refresh_token_lifetime: Duration::ZERO,
7185 ..OAuthServerConfig::default()
7186 },
7187 OAuthServerConfig {
7188 authorization_code_lifetime: Duration::ZERO,
7189 ..OAuthServerConfig::default()
7190 },
7191 OAuthServerConfig {
7192 access_token_lifetime: Duration::from_millis(999),
7193 ..OAuthServerConfig::default()
7194 },
7195 OAuthServerConfig {
7196 access_token_lifetime: Duration::from_secs(1),
7197 refresh_token_lifetime: Duration::from_millis(999),
7198 ..OAuthServerConfig::default()
7199 },
7200 OAuthServerConfig {
7201 authorization_code_lifetime: Duration::from_millis(999),
7202 ..OAuthServerConfig::default()
7203 },
7204 OAuthServerConfig {
7205 access_token_lifetime: MAX_ACCESS_TOKEN_LIFETIME + Duration::from_secs(1),
7206 ..OAuthServerConfig::default()
7207 },
7208 OAuthServerConfig {
7209 refresh_token_lifetime: MAX_REFRESH_TOKEN_LIFETIME + Duration::from_secs(1),
7210 ..OAuthServerConfig::default()
7211 },
7212 OAuthServerConfig {
7213 authorization_code_lifetime: MAX_AUTHORIZATION_CODE_LIFETIME
7214 + Duration::from_secs(1),
7215 ..OAuthServerConfig::default()
7216 },
7217 OAuthServerConfig {
7218 access_token_lifetime: Duration::from_secs(2),
7219 refresh_token_lifetime: Duration::from_secs(1),
7220 ..OAuthServerConfig::default()
7221 },
7222 ];
7223 for config in invalid {
7224 assert!(matches!(config.validate(), Err(OAuthError::ServerError(_))));
7225 }
7226
7227 OAuthServerConfig {
7228 access_token_lifetime: MAX_ACCESS_TOKEN_LIFETIME,
7229 refresh_token_lifetime: MAX_REFRESH_TOKEN_LIFETIME,
7230 authorization_code_lifetime: MAX_AUTHORIZATION_CODE_LIFETIME,
7231 ..OAuthServerConfig::default()
7232 }
7233 .validate()
7234 .expect("exact lifetime ceilings are admitted");
7235
7236 OAuthServerConfig {
7237 access_token_lifetime: Duration::from_secs(1),
7238 refresh_token_lifetime: Duration::from_secs(1),
7239 authorization_code_lifetime: Duration::from_secs(1),
7240 ..OAuthServerConfig::default()
7241 }
7242 .validate()
7243 .expect("one-second credential lifetimes are admitted");
7244 }
7245
7246 #[test]
7247 fn one_second_token_lifetime_has_positive_wire_expiry() {
7248 let server = OAuthServer::new(OAuthServerConfig {
7249 access_token_lifetime: Duration::from_secs(1),
7250 refresh_token_lifetime: Duration::from_secs(1),
7251 ..OAuthServerConfig::default()
7252 });
7253 server.register_client(bounded_test_client("c1")).unwrap();
7254
7255 let response = server.issue_tokens("c1", &[], None).unwrap();
7256
7257 assert_eq!(response.expires_in, 1);
7258 }
7259
7260 #[test]
7261 fn config_rejects_per_client_caps_above_global_caps() {
7262 let invalid = [
7263 OAuthServerConfig {
7264 max_authorization_codes: 1,
7265 max_authorization_codes_per_client: 2,
7266 ..OAuthServerConfig::default()
7267 },
7268 OAuthServerConfig {
7269 max_access_tokens: 1,
7270 max_access_tokens_per_client: 2,
7271 ..OAuthServerConfig::default()
7272 },
7273 OAuthServerConfig {
7274 max_refresh_tokens: 1,
7275 max_refresh_tokens_per_client: 2,
7276 ..OAuthServerConfig::default()
7277 },
7278 OAuthServerConfig {
7279 max_revocation_tombstones: 1,
7280 max_revocation_tombstones_per_client: 2,
7281 ..OAuthServerConfig::default()
7282 },
7283 ];
7284 for config in invalid {
7285 let error = config.validate().expect_err("incoherent state cap");
7286 assert!(matches!(&error, OAuthError::ServerError(_)));
7287 assert!(error.description().contains("must not exceed"));
7288 }
7289 }
7290
7291 #[test]
7296 fn client_type_debug_and_eq() {
7297 assert_eq!(ClientType::Public, ClientType::Public);
7298 assert_ne!(ClientType::Public, ClientType::Confidential);
7299 let debug = format!("{:?}", ClientType::Confidential);
7300 assert!(debug.contains("Confidential"));
7301 }
7302
7303 #[test]
7304 fn client_type_copy() {
7305 let t = ClientType::Public;
7306 let t2 = t; assert_eq!(t, t2);
7308 }
7309
7310 #[test]
7315 fn client_debug_is_redacted_and_client_is_not_consumed() {
7316 let client = OAuthClient::builder("dbg")
7317 .redirect_uri("http://127.0.0.1/cb")
7318 .build()
7319 .unwrap();
7320 let debug = format!("{:?}", client);
7321 assert!(debug.contains("OAuthClient"));
7322 assert!(debug.contains("client_id_len"));
7323 assert!(!debug.contains("dbg"));
7324
7325 assert_eq!(client.client_id, "dbg");
7326 }
7327
7328 #[test]
7329 fn client_authenticate_public_no_secret() {
7330 let client = OAuthClient::builder("pub")
7331 .redirect_uri("http://127.0.0.1/cb")
7332 .build()
7333 .unwrap();
7334 assert!(client.authenticate(None));
7336 assert!(!client.authenticate(Some("any")));
7338 }
7339
7340 #[test]
7341 fn client_validate_redirect_uri_non_localhost() {
7342 let client = OAuthClient::builder("c")
7343 .redirect_uri("https://example.com/cb")
7344 .build()
7345 .unwrap();
7346 assert!(client.validate_redirect_uri("https://example.com/cb"));
7348 assert!(!client.validate_redirect_uri("https://example.com/cb2"));
7349 assert!(!client.validate_redirect_uri("https://other.com/cb"));
7350 }
7351
7352 #[test]
7353 fn client_validate_redirect_uri_localhost_ipv6() {
7354 let client = OAuthClient::builder("c")
7355 .redirect_uri("http://[::1]:3000/callback")
7356 .build()
7357 .unwrap();
7358 assert!(client.validate_redirect_uri("http://[::1]:8080/callback"));
7360 assert!(!client.validate_redirect_uri("http://localhost:9000/callback"));
7362 assert!(!client.validate_redirect_uri("http://127.0.0.1:9000/callback"));
7363 }
7364
7365 #[test]
7366 fn client_validate_scopes_empty() {
7367 let client = OAuthClient::builder("c")
7368 .redirect_uri("http://127.0.0.1/cb")
7369 .scope("read")
7370 .build()
7371 .unwrap();
7372 assert!(client.validate_scopes(&[]));
7374 }
7375
7376 #[test]
7381 fn client_builder_debug() {
7382 let builder = OAuthClient::builder("test-id");
7383 let debug = format!("{:?}", builder);
7384 assert!(debug.contains("OAuthClientBuilder"));
7385 assert!(debug.contains("client_id_len"));
7386 assert!(!debug.contains("test-id"));
7387 }
7388
7389 #[test]
7390 fn client_builder_empty_id_fails() {
7391 let result = OAuthClient::builder("")
7392 .redirect_uri("http://127.0.0.1/cb")
7393 .build();
7394 assert!(result.is_err());
7395 }
7396
7397 #[test]
7398 fn client_builder_no_redirect_uris_fails() {
7399 let result = OAuthClient::builder("c").build();
7400 assert!(result.is_err());
7401 }
7402
7403 #[test]
7404 fn client_builder_redirect_uris_multiple() {
7405 let client = OAuthClient::builder("c")
7406 .redirect_uris(vec!["http://127.0.0.1/a", "http://127.0.0.1/b"])
7407 .build()
7408 .unwrap();
7409 assert_eq!(client.redirect_uris.len(), 2);
7410 }
7411
7412 #[test]
7413 fn client_builder_scopes_multiple() {
7414 let client = OAuthClient::builder("c")
7415 .redirect_uri("http://127.0.0.1/cb")
7416 .scopes(vec!["r", "w", "admin"])
7417 .build()
7418 .unwrap();
7419 assert_eq!(client.allowed_scopes.len(), 3);
7420 }
7421
7422 #[test]
7423 fn client_builder_description() {
7424 let client = OAuthClient::builder("c")
7425 .redirect_uri("http://127.0.0.1/cb")
7426 .description("A test app")
7427 .build()
7428 .unwrap();
7429 assert_eq!(client.description, Some("A test app".to_string()));
7430 }
7431
7432 #[test]
7437 fn code_challenge_method_as_str() {
7438 assert_eq!(CodeChallengeMethod::Plain.as_str(), "plain");
7439 assert_eq!(CodeChallengeMethod::S256.as_str(), "S256");
7440 }
7441
7442 #[test]
7443 fn code_challenge_method_clone_copy_eq() {
7444 let m = CodeChallengeMethod::S256;
7445 let m2 = m; assert_eq!(m, m2);
7447 let m3 = m.clone();
7448 assert_eq!(m, m3);
7449 }
7450
7451 #[test]
7456 fn authorization_code_not_expired_initially() {
7457 let code = AuthorizationCode {
7458 client_id: "c".to_string(),
7459 redirect_uri: "http://127.0.0.1/cb".to_string(),
7460 scopes: vec![],
7461 resource: None,
7462 code_challenge: "challenge".to_string(),
7463 code_challenge_method: CodeChallengeMethod::Plain,
7464 issued_at: Instant::now(),
7465 expires_at: Instant::now()
7466 .checked_add(Duration::from_secs(600))
7467 .expect("test deadline"),
7468 subject: None,
7469 state: None,
7470 registration_epoch: test_registration_epoch(1),
7471 };
7472 assert!(!code.is_expired());
7473 }
7474
7475 #[test]
7476 fn authorization_code_expired() {
7477 let code = AuthorizationCode {
7478 client_id: "c".to_string(),
7479 redirect_uri: "http://127.0.0.1/cb".to_string(),
7480 scopes: vec![],
7481 resource: None,
7482 code_challenge: "challenge".to_string(),
7483 code_challenge_method: CodeChallengeMethod::Plain,
7484 issued_at: Instant::now() - Duration::from_secs(100),
7485 expires_at: Instant::now() - Duration::from_secs(1),
7486 subject: None,
7487 state: None,
7488 registration_epoch: test_registration_epoch(1),
7489 };
7490 assert!(code.is_expired());
7491 }
7492
7493 #[test]
7494 fn authorization_code_rejects_plain_verifier_method() {
7495 let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
7496 let code = AuthorizationCode {
7497 client_id: "c".to_string(),
7498 redirect_uri: "http://127.0.0.1/cb".to_string(),
7499 scopes: vec![],
7500 resource: None,
7501 code_challenge: verifier.to_string(),
7502 code_challenge_method: CodeChallengeMethod::Plain,
7503 issued_at: Instant::now(),
7504 expires_at: Instant::now()
7505 .checked_add(Duration::from_secs(600))
7506 .expect("test deadline"),
7507 subject: None,
7508 state: None,
7509 registration_epoch: test_registration_epoch(1),
7510 };
7511 assert!(!code.validate_code_verifier(verifier));
7512 assert!(!code.validate_code_verifier("wrong"));
7513 }
7514
7515 #[test]
7516 fn authorization_code_validate_s256() {
7517 let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
7518 let challenge = compute_s256_challenge(verifier).unwrap();
7519 let code = AuthorizationCode {
7520 client_id: "c".to_string(),
7521 redirect_uri: "http://127.0.0.1/cb".to_string(),
7522 scopes: vec![],
7523 resource: None,
7524 code_challenge: challenge,
7525 code_challenge_method: CodeChallengeMethod::S256,
7526 issued_at: Instant::now(),
7527 expires_at: Instant::now()
7528 .checked_add(Duration::from_secs(600))
7529 .expect("test deadline"),
7530 subject: None,
7531 state: None,
7532 registration_epoch: test_registration_epoch(1),
7533 };
7534 assert!(code.validate_code_verifier(verifier));
7535 assert!(!code.validate_code_verifier("wrong-verifier"));
7536 }
7537
7538 #[test]
7539 fn authorization_code_debug_and_clone() {
7540 let code = AuthorizationCode {
7541 client_id: "cid".to_string(),
7542 redirect_uri: "http://127.0.0.1/cb".to_string(),
7543 scopes: vec!["read".to_string()],
7544 resource: None,
7545 code_challenge: "ch".to_string(),
7546 code_challenge_method: CodeChallengeMethod::Plain,
7547 issued_at: Instant::now(),
7548 expires_at: Instant::now()
7549 .checked_add(Duration::from_secs(60))
7550 .expect("test deadline"),
7551 subject: Some("user".to_string()),
7552 state: Some("state".to_string()),
7553 registration_epoch: test_registration_epoch(1),
7554 };
7555 let debug = format!("{:?}", code);
7556 assert!(debug.contains("AuthorizationCode"));
7557 let cloned = code.clone();
7558 assert_eq!(cloned.client_id, "cid");
7559 }
7560
7561 #[test]
7566 fn token_type_as_str() {
7567 assert_eq!(TokenType::Bearer.as_str(), "bearer");
7568 }
7569
7570 #[test]
7571 fn token_type_debug_clone_copy_eq() {
7572 let t = TokenType::Bearer;
7573 let t2 = t; assert_eq!(t, t2);
7575 let t3 = t.clone();
7576 assert_eq!(t, t3);
7577 let debug = format!("{:?}", t);
7578 assert!(debug.contains("Bearer"));
7579 }
7580
7581 #[test]
7586 fn oauth_token_not_expired() {
7587 let token = OAuthToken {
7588 token: String::new(),
7589 token_type: TokenType::Bearer,
7590 client_id: "c".to_string(),
7591 scopes: vec![],
7592 resource: None,
7593 issued_at: Instant::now(),
7594 expires_at: Instant::now()
7595 .checked_add(Duration::from_secs(3600))
7596 .expect("test deadline"),
7597 subject: None,
7598 is_refresh_token: false,
7599 };
7600 assert!(!token.is_expired());
7601 assert!(token.expires_in_secs() > 0);
7602 }
7603
7604 #[test]
7605 fn oauth_token_expired() {
7606 let token = OAuthToken {
7607 token: String::new(),
7608 token_type: TokenType::Bearer,
7609 client_id: "c".to_string(),
7610 scopes: vec![],
7611 resource: None,
7612 issued_at: Instant::now() - Duration::from_secs(100),
7613 expires_at: Instant::now() - Duration::from_secs(1),
7614 subject: None,
7615 is_refresh_token: false,
7616 };
7617 assert!(token.is_expired());
7618 assert_eq!(token.expires_in_secs(), 0);
7619 }
7620
7621 #[test]
7622 fn oauth_token_debug_and_clone() {
7623 let token = OAuthToken {
7624 token: String::new(),
7625 token_type: TokenType::Bearer,
7626 client_id: "c".to_string(),
7627 scopes: vec!["read".to_string()],
7628 resource: None,
7629 issued_at: Instant::now(),
7630 expires_at: Instant::now()
7631 .checked_add(Duration::from_secs(60))
7632 .expect("test deadline"),
7633 subject: Some("user".to_string()),
7634 is_refresh_token: true,
7635 };
7636 let debug = format!("{:?}", token);
7637 assert!(debug.contains("OAuthToken"));
7638 let cloned = token.clone();
7639 assert_eq!(cloned.client_id, "c");
7640 assert!(cloned.is_refresh_token);
7641 }
7642
7643 #[test]
7648 fn token_response_serialize_without_optional_fields() {
7649 let resp = TokenResponse {
7650 access_token: "at".to_string(),
7651 token_type: "bearer".to_string(),
7652 expires_in: 3600,
7653 refresh_token: None,
7654 scope: None,
7655 };
7656 let json = serde_json::to_string(&resp).unwrap();
7657 assert!(!json.contains("refresh_token"));
7658 assert!(!json.contains("scope"));
7659 }
7660
7661 #[test]
7662 fn token_response_serialize_with_optional_fields() {
7663 let resp = TokenResponse {
7664 access_token: "at".to_string(),
7665 token_type: "bearer".to_string(),
7666 expires_in: 3600,
7667 refresh_token: Some("rt".to_string()),
7668 scope: Some("read write".to_string()),
7669 };
7670 let json = serde_json::to_string(&resp).unwrap();
7671 assert!(json.contains("refresh_token"));
7672 assert!(json.contains("scope"));
7673 }
7674
7675 #[test]
7680 fn authorization_request_debug_and_clone() {
7681 let req = AuthorizationRequest {
7682 response_type: "code".to_string(),
7683 client_id: "c".to_string(),
7684 redirect_uri: "http://127.0.0.1/cb".to_string(),
7685 scopes: vec!["read".to_string()],
7686 resource: None,
7687 state: Some("s".to_string()),
7688 code_challenge: "ch".to_string(),
7689 code_challenge_method: CodeChallengeMethod::S256,
7690 };
7691 let debug = format!("{:?}", req);
7692 assert!(debug.contains("AuthorizationRequest"));
7693 let cloned = req.clone();
7694 assert_eq!(cloned.client_id, "c");
7695 }
7696
7697 #[test]
7698 fn token_request_debug_is_redacted() {
7699 let req = TokenRequest {
7700 grant_type: "authorization_code".to_string(),
7701 code: Some("code".to_string()),
7702 redirect_uri: Some("http://127.0.0.1/cb".to_string()),
7703 client_id: "c".to_string(),
7704 client_secret: None,
7705 code_verifier: Some("verifier".to_string()),
7706 refresh_token: None,
7707 scopes: None,
7708 resource: None,
7709 };
7710 let debug = format!("{:?}", req);
7711 assert!(debug.contains("TokenRequest"));
7712 assert_eq!(req.grant_type, "authorization_code");
7713 }
7714
7715 #[test]
7716 fn oauth_debug_surfaces_redact_secret_and_identity_canaries() {
7717 const CANARY: &str = "oauth-debug-secret-identity-canary";
7718 let now = Instant::now();
7719 let client = OAuthClient::builder(format!("client-{CANARY}"))
7720 .secret(format!("secret-{CANARY}"))
7721 .redirect_uri(format!("https://{CANARY}.example/callback"))
7722 .scope(format!("scope-{CANARY}"))
7723 .name(format!("name-{CANARY}"))
7724 .description(format!("description-{CANARY}"))
7725 .build()
7726 .unwrap();
7727 let client_metadata = OAuthClientMetadata::from(&client);
7728 let builder = OAuthClient::builder(format!("builder-{CANARY}"))
7729 .secret(format!("builder-secret-{CANARY}"))
7730 .redirect_uri(format!("https://{CANARY}.example/builder"))
7731 .scope(format!("builder-scope-{CANARY}"))
7732 .name(format!("builder-name-{CANARY}"))
7733 .description(format!("builder-description-{CANARY}"));
7734 let authorization_code = AuthorizationCode {
7735 client_id: format!("client-{CANARY}"),
7736 redirect_uri: format!("https://{CANARY}.example/code"),
7737 scopes: vec![format!("scope-{CANARY}")],
7738 resource: None,
7739 code_challenge: format!("challenge-{CANARY}"),
7740 code_challenge_method: CodeChallengeMethod::S256,
7741 issued_at: now,
7742 expires_at: now
7743 .checked_add(Duration::from_secs(60))
7744 .expect("test deadline"),
7745 subject: Some(format!("subject-{CANARY}")),
7746 state: Some(format!("state-{CANARY}")),
7747 registration_epoch: test_registration_epoch(1),
7748 };
7749 let oauth_token = OAuthToken {
7750 token: String::new(),
7751 token_type: TokenType::Bearer,
7752 client_id: format!("client-{CANARY}"),
7753 scopes: vec![format!("scope-{CANARY}")],
7754 resource: None,
7755 issued_at: now,
7756 expires_at: now
7757 .checked_add(Duration::from_secs(60))
7758 .expect("test deadline"),
7759 subject: Some(format!("subject-{CANARY}")),
7760 is_refresh_token: true,
7761 };
7762 let token_response = TokenResponse {
7763 access_token: format!("access-{CANARY}"),
7764 token_type: format!("type-{CANARY}"),
7765 expires_in: 60,
7766 refresh_token: Some(format!("refresh-{CANARY}")),
7767 scope: Some(format!("scope-{CANARY}")),
7768 };
7769 let authorization_request = AuthorizationRequest {
7770 response_type: format!("response-{CANARY}"),
7771 client_id: format!("client-{CANARY}"),
7772 redirect_uri: format!("https://{CANARY}.example/request"),
7773 scopes: vec![format!("scope-{CANARY}")],
7774 resource: None,
7775 state: Some(format!("state-{CANARY}")),
7776 code_challenge: format!("challenge-{CANARY}"),
7777 code_challenge_method: CodeChallengeMethod::S256,
7778 };
7779 let token_request = TokenRequest {
7780 grant_type: format!("grant-{CANARY}"),
7781 code: Some(format!("code-{CANARY}")),
7782 redirect_uri: Some(format!("https://{CANARY}.example/token")),
7783 client_id: format!("client-{CANARY}"),
7784 client_secret: Some(format!("secret-{CANARY}")),
7785 code_verifier: Some(format!("verifier-{CANARY}")),
7786 refresh_token: Some(format!("refresh-{CANARY}")),
7787 scopes: Some(vec![format!("scope-{CANARY}")]),
7788 resource: None,
7789 };
7790 let error = OAuthError::InvalidGrant(format!("error-{CANARY}"));
7791
7792 let wire = serde_json::to_value(&token_response).unwrap();
7793 assert_eq!(wire["access_token"], format!("access-{CANARY}"));
7794 assert_eq!(wire["refresh_token"], format!("refresh-{CANARY}"));
7795
7796 let debug_outputs = [
7797 format!("{client:?}"),
7798 format!("{client_metadata:?}"),
7799 format!("{builder:?}"),
7800 format!("{authorization_code:?}"),
7801 format!("{oauth_token:?}"),
7802 format!("{token_response:?}"),
7803 format!("{authorization_request:?}"),
7804 format!("{token_request:?}"),
7805 format!("{error:?}"),
7806 ];
7807
7808 for debug in debug_outputs {
7809 assert!(
7810 !debug.contains(CANARY),
7811 "sensitive canary leaked through Debug: {debug}"
7812 );
7813 assert!(
7814 debug.contains("_len") || debug.contains("_count") || debug.contains("_present"),
7815 "Debug output lacked safe structural metadata: {debug}"
7816 );
7817 }
7818 }
7819
7820 #[test]
7825 fn oauth_error_all_codes() {
7826 let cases: Vec<(OAuthError, &str)> = vec![
7827 (OAuthError::InvalidRequest("x".into()), "invalid_request"),
7828 (OAuthError::InvalidClient("x".into()), "invalid_client"),
7829 (OAuthError::InvalidGrant("x".into()), "invalid_grant"),
7830 (
7831 OAuthError::UnauthorizedClient("x".into()),
7832 "unauthorized_client",
7833 ),
7834 (
7835 OAuthError::UnsupportedGrantType("x".into()),
7836 "unsupported_grant_type",
7837 ),
7838 (OAuthError::InvalidScope("x".into()), "invalid_scope"),
7839 (OAuthError::ServerError("x".into()), "server_error"),
7840 (
7841 OAuthError::TemporarilyUnavailable("x".into()),
7842 "temporarily_unavailable",
7843 ),
7844 (OAuthError::AccessDenied("x".into()), "access_denied"),
7845 (
7846 OAuthError::UnsupportedResponseType("x".into()),
7847 "unsupported_response_type",
7848 ),
7849 ];
7850 for (err, expected_code) in cases {
7851 assert_eq!(err.error_code(), expected_code);
7852 assert_eq!(err.description(), "x");
7853 }
7854 }
7855
7856 #[test]
7857 fn oauth_error_debug_and_clone() {
7858 let err = OAuthError::ServerError("test".into());
7859 let debug = format!("{:?}", err);
7860 assert!(debug.contains("ServerError"));
7861 let cloned = err.clone();
7862 assert_eq!(cloned.description(), "test");
7863 }
7864
7865 #[test]
7866 fn oauth_error_is_std_error() {
7867 let err = OAuthError::InvalidGrant("x".into());
7868 let _: &dyn std::error::Error = &err;
7869 }
7870
7871 #[test]
7872 fn oauth_error_into_mcp_error_forbidden() {
7873 let err: McpError = OAuthError::InvalidClient("c".into()).into();
7875 assert!(err.message.contains("invalid_client"));
7876 let err: McpError = OAuthError::UnauthorizedClient("c".into()).into();
7877 assert!(err.message.contains("unauthorized_client"));
7878 let err: McpError = OAuthError::AccessDenied("d".into()).into();
7879 assert!(err.message.contains("access_denied"));
7880 }
7881
7882 #[test]
7883 fn oauth_error_into_mcp_error_invalid_request() {
7884 let err: McpError = OAuthError::InvalidScope("s".into()).into();
7886 assert!(err.message.contains("invalid_scope"));
7887 let err: McpError = OAuthError::UnsupportedGrantType("g".into()).into();
7888 assert!(err.message.contains("unsupported_grant_type"));
7889 }
7890
7891 #[test]
7896 fn server_config_accessor() {
7897 let config = OAuthServerConfig {
7898 issuer: "https://issuer.example/".to_string(),
7899 ..OAuthServerConfig::default()
7900 };
7901 let server = OAuthServer::new(config);
7902 assert_eq!(server.config().issuer, "https://issuer.example/");
7903 }
7904
7905 #[test]
7906 fn server_register_public_not_allowed() {
7907 let config = OAuthServerConfig {
7908 allow_public_clients: false,
7909 ..OAuthServerConfig::default()
7910 };
7911 let server = OAuthServer::new(config);
7912
7913 let client = OAuthClient::builder("c")
7914 .redirect_uri("http://127.0.0.1/cb")
7915 .build()
7916 .unwrap();
7917 let result = server.register_client(client);
7918 assert!(matches!(result, Err(OAuthError::InvalidClient(_))));
7919 }
7920
7921 #[test]
7922 fn server_list_clients() {
7923 let server = OAuthServer::with_defaults();
7924 assert!(server.list_clients().is_empty());
7925
7926 let client = OAuthClient::builder("a")
7927 .redirect_uri("http://127.0.0.1/cb")
7928 .build()
7929 .unwrap();
7930 server.register_client(client).unwrap();
7931 assert_eq!(server.list_clients().len(), 1);
7932 }
7933
7934 #[test]
7935 fn server_authorize_unsupported_response_type() {
7936 let server = OAuthServer::with_defaults();
7937 let client = OAuthClient::builder("c")
7938 .redirect_uri("http://127.0.0.1/cb")
7939 .build()
7940 .unwrap();
7941 server.register_client(client).unwrap();
7942
7943 let req = AuthorizationRequest {
7944 response_type: "token".to_string(), client_id: "c".to_string(),
7946 redirect_uri: "http://127.0.0.1/cb".to_string(),
7947 scopes: vec![],
7948 resource: None,
7949 state: None,
7950 code_challenge: "ch".to_string(),
7951 code_challenge_method: CodeChallengeMethod::S256,
7952 };
7953 let result = server.authorize(&req);
7954 assert!(matches!(
7955 result,
7956 Err(OAuthError::UnsupportedResponseType(_))
7957 ));
7958 }
7959
7960 #[test]
7961 fn server_authorize_invalid_redirect() {
7962 let server = OAuthServer::with_defaults();
7963 let client = OAuthClient::builder("c")
7964 .redirect_uri("http://127.0.0.1/cb")
7965 .build()
7966 .unwrap();
7967 server.register_client(client).unwrap();
7968
7969 let req = AuthorizationRequest {
7970 response_type: "code".to_string(),
7971 client_id: "c".to_string(),
7972 redirect_uri: "https://evil.com/cb".to_string(),
7973 scopes: vec![],
7974 resource: None,
7975 state: None,
7976 code_challenge: "ch".to_string(),
7977 code_challenge_method: CodeChallengeMethod::S256,
7978 };
7979 let result = server.authorize(&req);
7980 assert!(matches!(result, Err(OAuthError::InvalidRequest(_))));
7981 }
7982
7983 #[test]
7984 fn server_authorize_invalid_scope() {
7985 let server = OAuthServer::with_defaults();
7986 let client = OAuthClient::builder("c")
7987 .redirect_uri("http://127.0.0.1/cb")
7988 .scope("read")
7989 .build()
7990 .unwrap();
7991 server.register_client(client).unwrap();
7992
7993 let req = AuthorizationRequest {
7994 response_type: "code".to_string(),
7995 client_id: "c".to_string(),
7996 redirect_uri: "http://127.0.0.1/cb".to_string(),
7997 scopes: vec!["admin".to_string()],
7998 resource: None,
7999 state: None,
8000 code_challenge: "ch".to_string(),
8001 code_challenge_method: CodeChallengeMethod::S256,
8002 };
8003 let result = server.authorize(&req);
8004 assert!(matches!(result, Err(OAuthError::InvalidScope(_))));
8005 }
8006
8007 #[test]
8008 fn server_authorize_unknown_client() {
8009 let server = OAuthServer::with_defaults();
8010 let req = AuthorizationRequest {
8011 response_type: "code".to_string(),
8012 client_id: "nonexistent".to_string(),
8013 redirect_uri: "http://127.0.0.1/cb".to_string(),
8014 scopes: vec![],
8015 resource: None,
8016 state: None,
8017 code_challenge: "ch".to_string(),
8018 code_challenge_method: CodeChallengeMethod::S256,
8019 };
8020 let result = server.authorize(&req);
8021 assert!(matches!(result, Err(OAuthError::InvalidClient(_))));
8022 }
8023
8024 #[test]
8025 fn server_token_unsupported_grant_type() {
8026 let server = OAuthServer::with_defaults();
8027 let req = TokenRequest {
8028 grant_type: "client_credentials".to_string(),
8029 code: None,
8030 redirect_uri: None,
8031 client_id: "c".to_string(),
8032 client_secret: None,
8033 code_verifier: None,
8034 refresh_token: None,
8035 scopes: None,
8036 resource: None,
8037 };
8038 let result = server.token(&req);
8039 assert!(matches!(result, Err(OAuthError::UnsupportedGrantType(_))));
8040 }
8041
8042 #[test]
8043 fn server_token_auth_code_missing_code() {
8044 let server = OAuthServer::with_defaults();
8045 let req = TokenRequest {
8046 grant_type: "authorization_code".to_string(),
8047 code: None, redirect_uri: Some("http://127.0.0.1/cb".to_string()),
8049 client_id: "c".to_string(),
8050 client_secret: None,
8051 code_verifier: Some("v".repeat(43)),
8052 refresh_token: None,
8053 scopes: None,
8054 resource: None,
8055 };
8056 let result = server.token(&req);
8057 assert!(matches!(result, Err(OAuthError::InvalidRequest(_))));
8058 }
8059
8060 #[test]
8061 fn server_token_auth_code_missing_redirect() {
8062 let server = OAuthServer::with_defaults();
8063 let req = TokenRequest {
8064 grant_type: "authorization_code".to_string(),
8065 code: Some("code".to_string()),
8066 redirect_uri: None, client_id: "c".to_string(),
8068 client_secret: None,
8069 code_verifier: Some("v".repeat(43)),
8070 refresh_token: None,
8071 scopes: None,
8072 resource: None,
8073 };
8074 let result = server.token(&req);
8075 assert!(matches!(result, Err(OAuthError::InvalidRequest(_))));
8076 }
8077
8078 #[test]
8079 fn server_token_auth_code_missing_verifier() {
8080 let server = OAuthServer::with_defaults();
8081 let req = TokenRequest {
8082 grant_type: "authorization_code".to_string(),
8083 code: Some("code".to_string()),
8084 redirect_uri: Some("http://127.0.0.1/cb".to_string()),
8085 client_id: "c".to_string(),
8086 client_secret: None,
8087 code_verifier: None, refresh_token: None,
8089 scopes: None,
8090 resource: None,
8091 };
8092 let result = server.token(&req);
8093 assert!(matches!(result, Err(OAuthError::InvalidRequest(_))));
8094 }
8095
8096 #[test]
8097 fn server_token_auth_code_verifier_too_short() {
8098 let mut config = OAuthServerConfig::default();
8099 config.min_code_verifier_length = PKCE_CODE_VERIFIER_MIN_BYTES;
8103 let server = OAuthServer::new(config);
8104 let client = OAuthClient::builder("c")
8105 .redirect_uri("http://127.0.0.1/cb")
8106 .build()
8107 .unwrap();
8108 server.register_client(client).unwrap();
8109
8110 let issued_verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
8112 let verifier = "short"; let req = AuthorizationRequest {
8114 response_type: "code".to_string(),
8115 client_id: "c".to_string(),
8116 redirect_uri: "http://127.0.0.1/cb".to_string(),
8117 scopes: vec![],
8118 resource: None,
8119 state: None,
8120 code_challenge: compute_s256_challenge(issued_verifier).unwrap(),
8121 code_challenge_method: CodeChallengeMethod::S256,
8122 };
8123 let (code, _) = server.authorize(&req).unwrap();
8124 let stored_code = code.clone();
8125
8126 let token_req = TokenRequest {
8127 grant_type: "authorization_code".to_string(),
8128 code: Some(code),
8129 redirect_uri: Some("http://127.0.0.1/cb".to_string()),
8130 client_id: "c".to_string(),
8131 client_secret: None,
8132 code_verifier: Some(verifier.to_string()),
8133 refresh_token: None,
8134 scopes: None,
8135 resource: None,
8136 };
8137 let result = server.token(&token_req);
8138 assert!(matches!(result, Err(OAuthError::InvalidGrant(_))));
8142 assert!(
8143 server
8144 .state
8145 .read()
8146 .unwrap()
8147 .authorization_codes
8148 .contains_key(&authorization_code_digest(&stored_code))
8149 );
8150 }
8151
8152 #[test]
8153 fn server_full_auth_code_flow_with_s256() {
8154 let server = OAuthServer::with_defaults();
8155 let client = OAuthClient::builder("c")
8156 .redirect_uri("http://127.0.0.1/cb")
8157 .scope("read")
8158 .build()
8159 .unwrap();
8160 server.register_client(client).unwrap();
8161
8162 let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
8163 let challenge = compute_s256_challenge(verifier).unwrap();
8164
8165 let auth_req = AuthorizationRequest {
8166 response_type: "code".to_string(),
8167 client_id: "c".to_string(),
8168 redirect_uri: "http://127.0.0.1/cb".to_string(),
8169 scopes: vec!["read".to_string()],
8170 resource: None,
8171 state: None,
8172 code_challenge: challenge,
8173 code_challenge_method: CodeChallengeMethod::S256,
8174 };
8175 let (code, _) = server.authorize(&auth_req).unwrap();
8176
8177 let token_req = TokenRequest {
8178 grant_type: "authorization_code".to_string(),
8179 code: Some(code),
8180 redirect_uri: Some("http://127.0.0.1/cb".to_string()),
8181 client_id: "c".to_string(),
8182 client_secret: None,
8183 code_verifier: Some(verifier.to_string()),
8184 refresh_token: None,
8185 scopes: None,
8186 resource: None,
8187 };
8188 let resp = server.token(&token_req).unwrap();
8189 assert!(!resp.access_token.is_empty());
8190 assert!(resp.refresh_token.is_some());
8191 assert_eq!(resp.token_type, "bearer");
8192 assert_eq!(resp.scope, Some("read".to_string()));
8193 }
8194
8195 #[test]
8196 fn server_token_code_already_used() {
8197 let server = OAuthServer::with_defaults();
8198 let client = OAuthClient::builder("c")
8199 .redirect_uri("http://127.0.0.1/cb")
8200 .build()
8201 .unwrap();
8202 server.register_client(client).unwrap();
8203
8204 let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
8205 let auth_req = AuthorizationRequest {
8206 response_type: "code".to_string(),
8207 client_id: "c".to_string(),
8208 redirect_uri: "http://127.0.0.1/cb".to_string(),
8209 scopes: vec![],
8210 resource: None,
8211 state: None,
8212 code_challenge: compute_s256_challenge(verifier).unwrap(),
8213 code_challenge_method: CodeChallengeMethod::S256,
8214 };
8215 let (code, _) = server.authorize(&auth_req).unwrap();
8216
8217 let token_req = TokenRequest {
8218 grant_type: "authorization_code".to_string(),
8219 code: Some(code.clone()),
8220 redirect_uri: Some("http://127.0.0.1/cb".to_string()),
8221 client_id: "c".to_string(),
8222 client_secret: None,
8223 code_verifier: Some(verifier.to_string()),
8224 refresh_token: None,
8225 scopes: None,
8226 resource: None,
8227 };
8228 server.token(&token_req).unwrap();
8230 let result = server.token(&token_req);
8232 assert!(matches!(result, Err(OAuthError::InvalidGrant(_))));
8233 }
8234
8235 #[test]
8236 fn server_validate_access_token_nonexistent() {
8237 let server = OAuthServer::with_defaults();
8238 assert!(server.validate_access_token("nonexistent").is_none());
8239 }
8240
8241 #[test]
8242 fn server_unregister_client_revokes_tokens() {
8243 let server = OAuthServer::with_defaults();
8244 let client = OAuthClient::builder("c")
8245 .redirect_uri("http://127.0.0.1/cb")
8246 .scope("read")
8247 .build()
8248 .unwrap();
8249 server.register_client(client).unwrap();
8250
8251 let resp = issue_access_token_via_auth_code(
8252 &server,
8253 "c",
8254 "http://127.0.0.1/cb",
8255 &["read"],
8256 "user",
8257 );
8258 assert!(server.validate_access_token(&resp.access_token).is_some());
8259
8260 server.unregister_client("c").unwrap();
8261 assert!(server.validate_access_token(&resp.access_token).is_none());
8262 }
8263
8264 #[test]
8265 fn server_cleanup_expired_removes_old_tokens() {
8266 let server = OAuthServer::with_defaults();
8267 let client = OAuthClient::builder("c")
8268 .redirect_uri("http://127.0.0.1/cb")
8269 .build()
8270 .unwrap();
8271 server.register_client(client).unwrap();
8272
8273 let response =
8274 issue_access_token_via_auth_code(&server, "c", "http://127.0.0.1/cb", &[], "user");
8275 let refresh = response.refresh_token.expect("refresh token");
8276 let expired_at = Instant::now();
8277 {
8278 let mut state = server.state.write().unwrap();
8279 let access = state
8280 .access_tokens
8281 .get_mut(&access_token_digest(&response.access_token))
8282 .expect("stored access token");
8283 access.metadata.expires_at = expired_at;
8284 access.family_expires_at = expired_at;
8285 let refresh = state
8286 .refresh_tokens
8287 .get_mut(&refresh_token_digest(&refresh))
8288 .expect("stored refresh token");
8289 refresh.metadata.expires_at = expired_at;
8290 refresh.family_expires_at = expired_at;
8291 }
8292
8293 let stats_before = server.stats();
8294 server.cleanup_expired();
8295 let stats_after = server.stats();
8296
8297 assert_eq!(stats_before.access_tokens, 1);
8298 assert_eq!(stats_before.refresh_tokens, 1);
8299 assert_eq!(stats_after.access_tokens, 0);
8300 assert_eq!(stats_after.refresh_tokens, 0);
8301 }
8302
8303 #[test]
8308 fn server_stats_default() {
8309 let stats = OAuthServerStats::default();
8310 assert_eq!(stats.clients, 0);
8311 assert_eq!(stats.authorization_codes, 0);
8312 assert_eq!(stats.access_tokens, 0);
8313 assert_eq!(stats.refresh_tokens, 0);
8314 assert_eq!(stats.revoked_tokens, 0);
8315 }
8316
8317 #[test]
8318 fn server_stats_debug_and_clone() {
8319 let stats = OAuthServerStats {
8320 clients: 1,
8321 access_tokens: 5,
8322 ..OAuthServerStats::default()
8323 };
8324 let debug = format!("{:?}", stats);
8325 assert!(debug.contains("OAuthServerStats"));
8326 let cloned = stats.clone();
8327 assert_eq!(cloned.clients, 1);
8328 }
8329
8330 #[test]
8335 fn is_loopback_redirect_tests() {
8336 assert!(!is_loopback_redirect("http://localhost:3000/cb"));
8337 assert!(is_loopback_redirect("http://127.0.0.1:8080/cb"));
8338 assert!(is_loopback_redirect("http://[::1]:9000/cb"));
8339 assert!(!is_loopback_redirect("https://example.com/cb"));
8340 assert!(!is_loopback_redirect("http://evil.com/cb"));
8341 assert!(!is_loopback_redirect(
8342 "http://localhost:3000@evil.example/cb"
8343 ));
8344 assert!(!is_loopback_redirect("http://localhost.evil.example/cb"));
8345 assert!(!is_loopback_redirect("http://127.0.0.1:not-a-port/cb"));
8346 assert!(!is_loopback_redirect("http://127.0.0.1:+80/cb"));
8347 assert!(!is_loopback_redirect("http://127.0.0.1:65536/cb"));
8348 assert!(!is_loopback_redirect("http://127.0.0.1:3000/cb#fragment"));
8349 }
8350
8351 #[test]
8352 fn compute_s256_challenge_deterministic() {
8353 let v = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
8354 let c1 = compute_s256_challenge(v).unwrap();
8355 let c2 = compute_s256_challenge(v).unwrap();
8356 assert_eq!(c1, c2);
8357 assert!(!c1.is_empty());
8358 }
8359
8360 #[test]
8361 fn url_encode_special_chars() {
8362 assert_eq!(url_encode("a b"), "a%20b");
8363 assert_eq!(url_encode("a+b"), "a%2Bb");
8364 assert_eq!(url_encode("a/b"), "a%2Fb");
8365 assert_eq!(url_encode("safe-_~."), "safe-_~.");
8366 }
8367
8368 #[test]
8369 fn constant_time_eq_same_length_different() {
8370 assert!(!constant_time_eq("abc", "abd"));
8371 }
8372
8373 #[test]
8374 fn loopback_match_different_paths_fail() {
8375 assert!(!loopback_match(
8376 "http://127.0.0.1:3000/a",
8377 "http://127.0.0.1:3000/b"
8378 ));
8379 assert!(!loopback_match(
8380 "http://127.0.0.1:3000/cb",
8381 "http://localhost:3000@evil.example/cb"
8382 ));
8383 }
8384
8385 #[test]
8386 fn loopback_match_non_http_fails() {
8387 assert!(!loopback_match("ftp://127.0.0.1/a", "ftp://127.0.0.1/a"));
8388 }
8389
8390 #[test]
8395 fn server_refresh_token_flow() {
8396 let server = OAuthServer::with_defaults();
8397 let client = OAuthClient::builder("c1")
8398 .redirect_uri("http://127.0.0.1/cb")
8399 .scope("read")
8400 .scope("write")
8401 .build()
8402 .unwrap();
8403 server.register_client(client).unwrap();
8404
8405 let token_resp = issue_access_token_via_auth_code(
8406 &server,
8407 "c1",
8408 "http://127.0.0.1/cb",
8409 &["read", "write"],
8410 "user1",
8411 );
8412 let refresh = token_resp.refresh_token.unwrap();
8413
8414 let new_resp = server
8416 .token(&TokenRequest {
8417 grant_type: "refresh_token".to_string(),
8418 code: None,
8419 redirect_uri: None,
8420 client_id: "c1".to_string(),
8421 client_secret: None,
8422 code_verifier: None,
8423 refresh_token: Some(refresh.clone()),
8424 scopes: None,
8425 resource: None,
8426 })
8427 .unwrap();
8428
8429 assert_ne!(new_resp.access_token, token_resp.access_token);
8431 assert_eq!(new_resp.token_type, "bearer");
8432 let rotated_refresh = new_resp
8434 .refresh_token
8435 .as_deref()
8436 .expect("refresh flow must rotate the refresh token");
8437 assert_ne!(rotated_refresh, refresh);
8438 assert!(new_resp.scope.is_some());
8440 }
8441
8442 #[test]
8443 fn server_refresh_token_scope_narrowing() {
8444 let server = OAuthServer::with_defaults();
8445 let client = OAuthClient::builder("c1")
8446 .redirect_uri("http://127.0.0.1/cb")
8447 .scope("read")
8448 .scope("write")
8449 .build()
8450 .unwrap();
8451 server.register_client(client).unwrap();
8452
8453 let token_resp = issue_access_token_via_auth_code(
8454 &server,
8455 "c1",
8456 "http://127.0.0.1/cb",
8457 &["read", "write"],
8458 "user1",
8459 );
8460 let refresh = token_resp.refresh_token.unwrap();
8461
8462 let new_resp = server
8464 .token(&TokenRequest {
8465 grant_type: "refresh_token".to_string(),
8466 code: None,
8467 redirect_uri: None,
8468 client_id: "c1".to_string(),
8469 client_secret: None,
8470 code_verifier: None,
8471 refresh_token: Some(refresh),
8472 scopes: Some(vec!["read".to_string()]),
8473 resource: None,
8474 })
8475 .unwrap();
8476
8477 assert_eq!(new_resp.scope, Some("read".to_string()));
8478 }
8479
8480 #[test]
8481 fn server_refresh_token_invalid_scope() {
8482 let server = OAuthServer::with_defaults();
8483 let client = OAuthClient::builder("c1")
8484 .redirect_uri("http://127.0.0.1/cb")
8485 .scope("read")
8486 .build()
8487 .unwrap();
8488 server.register_client(client).unwrap();
8489
8490 let token_resp = issue_access_token_via_auth_code(
8491 &server,
8492 "c1",
8493 "http://127.0.0.1/cb",
8494 &["read"],
8495 "user1",
8496 );
8497 let refresh = token_resp.refresh_token.unwrap();
8498
8499 let err = server
8501 .token(&TokenRequest {
8502 grant_type: "refresh_token".to_string(),
8503 code: None,
8504 redirect_uri: None,
8505 client_id: "c1".to_string(),
8506 client_secret: None,
8507 code_verifier: None,
8508 refresh_token: Some(refresh),
8509 scopes: Some(vec!["admin".to_string()]),
8510 resource: None,
8511 })
8512 .unwrap_err();
8513
8514 assert_eq!(err.error_code(), "invalid_scope");
8515 }
8516
8517 #[test]
8518 fn server_refresh_token_revoked() {
8519 let server = OAuthServer::with_defaults();
8520 let client = OAuthClient::builder("c1")
8521 .redirect_uri("http://127.0.0.1/cb")
8522 .scope("read")
8523 .build()
8524 .unwrap();
8525 server.register_client(client).unwrap();
8526
8527 let token_resp = issue_access_token_via_auth_code(
8528 &server,
8529 "c1",
8530 "http://127.0.0.1/cb",
8531 &["read"],
8532 "user1",
8533 );
8534 let access = token_resp.access_token.clone();
8535 let refresh = token_resp.refresh_token.unwrap();
8536
8537 server.revoke(&refresh, "c1", None).unwrap();
8539 assert!(server.validate_access_token(&access).is_none());
8540
8541 let err = server
8543 .token(&TokenRequest {
8544 grant_type: "refresh_token".to_string(),
8545 code: None,
8546 redirect_uri: None,
8547 client_id: "c1".to_string(),
8548 client_secret: None,
8549 code_verifier: None,
8550 refresh_token: Some(refresh),
8551 scopes: None,
8552 resource: None,
8553 })
8554 .unwrap_err();
8555
8556 assert_eq!(err.error_code(), "invalid_grant");
8557 assert_eq!(err.description(), OAUTH_INVALID_GRANT_ERROR);
8558 }
8559
8560 #[test]
8561 fn server_refresh_token_client_id_mismatch() {
8562 let server = OAuthServer::with_defaults();
8563 let client1 = OAuthClient::builder("c1")
8564 .redirect_uri("http://127.0.0.1/cb")
8565 .scope("read")
8566 .build()
8567 .unwrap();
8568 let client2 = OAuthClient::builder("c2")
8569 .redirect_uri("http://127.0.0.1/cb")
8570 .scope("read")
8571 .build()
8572 .unwrap();
8573 server.register_client(client1).unwrap();
8574 server.register_client(client2).unwrap();
8575
8576 let token_resp = issue_access_token_via_auth_code(
8577 &server,
8578 "c1",
8579 "http://127.0.0.1/cb",
8580 &["read"],
8581 "user1",
8582 );
8583 let refresh = token_resp.refresh_token.unwrap();
8584
8585 let err = server
8587 .token(&TokenRequest {
8588 grant_type: "refresh_token".to_string(),
8589 code: None,
8590 redirect_uri: None,
8591 client_id: "c2".to_string(),
8592 client_secret: None,
8593 code_verifier: None,
8594 refresh_token: Some(refresh),
8595 scopes: None,
8596 resource: None,
8597 })
8598 .unwrap_err();
8599
8600 assert_eq!(err.error_code(), "invalid_grant");
8601 assert_eq!(err.description(), OAUTH_INVALID_GRANT_ERROR);
8602 }
8603
8604 #[test]
8605 fn server_refresh_token_missing_param() {
8606 let server = OAuthServer::with_defaults();
8607 let client = OAuthClient::builder("c1")
8608 .redirect_uri("http://127.0.0.1/cb")
8609 .build()
8610 .unwrap();
8611 server.register_client(client).unwrap();
8612
8613 let err = server
8614 .token(&TokenRequest {
8615 grant_type: "refresh_token".to_string(),
8616 code: None,
8617 redirect_uri: None,
8618 client_id: "c1".to_string(),
8619 client_secret: None,
8620 code_verifier: None,
8621 refresh_token: None,
8622 scopes: None,
8623 resource: None,
8624 })
8625 .unwrap_err();
8626
8627 assert_eq!(err.error_code(), "invalid_request");
8628 assert!(err.description().contains("refresh_token"));
8629 }
8630
8631 #[test]
8632 fn server_refresh_token_not_found() {
8633 let server = OAuthServer::with_defaults();
8634 let client = OAuthClient::builder("c1")
8635 .redirect_uri("http://127.0.0.1/cb")
8636 .build()
8637 .unwrap();
8638 server.register_client(client).unwrap();
8639
8640 let err = server
8641 .token(&TokenRequest {
8642 grant_type: "refresh_token".to_string(),
8643 code: None,
8644 redirect_uri: None,
8645 client_id: "c1".to_string(),
8646 client_secret: None,
8647 code_verifier: None,
8648 refresh_token: Some("nonexistent".to_string()),
8649 scopes: None,
8650 resource: None,
8651 })
8652 .unwrap_err();
8653
8654 assert_eq!(err.error_code(), "invalid_grant");
8655 }
8656
8657 #[test]
8662 fn server_token_auth_code_redirect_uri_mismatch() {
8663 let server = OAuthServer::with_defaults();
8664 let client = OAuthClient::builder("c1")
8665 .redirect_uri("http://127.0.0.1/cb")
8666 .redirect_uri("http://127.0.0.1/cb2")
8667 .scope("read")
8668 .build()
8669 .unwrap();
8670 server.register_client(client).unwrap();
8671
8672 let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
8673 let (code, _) = server
8674 .authorize(&AuthorizationRequest {
8675 response_type: "code".to_string(),
8676 client_id: "c1".to_string(),
8677 redirect_uri: "http://127.0.0.1/cb".to_string(),
8678 scopes: vec!["read".to_string()],
8679 resource: None,
8680 state: None,
8681 code_challenge: compute_s256_challenge(verifier).unwrap(),
8682 code_challenge_method: CodeChallengeMethod::S256,
8683 })
8684 .unwrap();
8685
8686 let err = server
8688 .token(&TokenRequest {
8689 grant_type: "authorization_code".to_string(),
8690 code: Some(code.clone()),
8691 redirect_uri: Some("http://127.0.0.1/cb2".to_string()),
8692 client_id: "c1".to_string(),
8693 client_secret: None,
8694 code_verifier: Some(verifier.to_string()),
8695 refresh_token: None,
8696 scopes: None,
8697 resource: None,
8698 })
8699 .unwrap_err();
8700
8701 assert_eq!(err.error_code(), "invalid_grant");
8702 assert_eq!(err.description(), OAUTH_INVALID_GRANT_ERROR);
8703 assert!(
8704 server
8705 .state
8706 .read()
8707 .unwrap()
8708 .authorization_codes
8709 .contains_key(&authorization_code_digest(&code))
8710 );
8711 }
8712
8713 #[test]
8714 fn server_token_auth_code_client_id_mismatch() {
8715 let server = OAuthServer::with_defaults();
8716 let client1 = OAuthClient::builder("c1")
8717 .redirect_uri("http://127.0.0.1/cb")
8718 .scope("read")
8719 .build()
8720 .unwrap();
8721 let client2 = OAuthClient::builder("c2")
8722 .redirect_uri("http://127.0.0.1/cb")
8723 .scope("read")
8724 .build()
8725 .unwrap();
8726 server.register_client(client1).unwrap();
8727 server.register_client(client2).unwrap();
8728
8729 let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
8730 let (code, _) = server
8731 .authorize(&AuthorizationRequest {
8732 response_type: "code".to_string(),
8733 client_id: "c1".to_string(),
8734 redirect_uri: "http://127.0.0.1/cb".to_string(),
8735 scopes: vec!["read".to_string()],
8736 resource: None,
8737 state: None,
8738 code_challenge: compute_s256_challenge(verifier).unwrap(),
8739 code_challenge_method: CodeChallengeMethod::S256,
8740 })
8741 .unwrap();
8742
8743 let err = server
8745 .token(&TokenRequest {
8746 grant_type: "authorization_code".to_string(),
8747 code: Some(code.clone()),
8748 redirect_uri: Some("http://127.0.0.1/cb".to_string()),
8749 client_id: "c2".to_string(),
8750 client_secret: None,
8751 code_verifier: Some(verifier.to_string()),
8752 refresh_token: None,
8753 scopes: None,
8754 resource: None,
8755 })
8756 .unwrap_err();
8757
8758 assert_eq!(err.error_code(), "invalid_grant");
8759 assert_eq!(err.description(), OAUTH_INVALID_GRANT_ERROR);
8760 assert!(
8761 server
8762 .state
8763 .read()
8764 .unwrap()
8765 .authorization_codes
8766 .contains_key(&authorization_code_digest(&code))
8767 );
8768 }
8769
8770 #[test]
8771 fn server_token_auth_code_confidential_client_auth_fails() {
8772 let server = OAuthServer::with_defaults();
8773 let client = OAuthClient::builder("c1")
8774 .secret("correct-secret")
8775 .redirect_uri("http://127.0.0.1/cb")
8776 .scope("read")
8777 .build()
8778 .unwrap();
8779 server.register_client(client).unwrap();
8780
8781 let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
8782 let (code, _) = server
8783 .authorize(&AuthorizationRequest {
8784 response_type: "code".to_string(),
8785 client_id: "c1".to_string(),
8786 redirect_uri: "http://127.0.0.1/cb".to_string(),
8787 scopes: vec!["read".to_string()],
8788 resource: None,
8789 state: None,
8790 code_challenge: compute_s256_challenge(verifier).unwrap(),
8791 code_challenge_method: CodeChallengeMethod::S256,
8792 })
8793 .unwrap();
8794
8795 let err = server
8797 .token(&TokenRequest {
8798 grant_type: "authorization_code".to_string(),
8799 code: Some(code.clone()),
8800 redirect_uri: Some("http://127.0.0.1/cb".to_string()),
8801 client_id: "c1".to_string(),
8802 client_secret: Some("wrong-secret".to_string()),
8803 code_verifier: Some(verifier.to_string()),
8804 refresh_token: None,
8805 scopes: None,
8806 resource: None,
8807 })
8808 .unwrap_err();
8809
8810 assert_eq!(err.error_code(), "invalid_client");
8811 assert!(
8812 server
8813 .state
8814 .read()
8815 .unwrap()
8816 .authorization_codes
8817 .contains_key(&authorization_code_digest(&code))
8818 );
8819 }
8820
8821 #[test]
8822 fn failed_pkce_exchange_preserves_code_for_legitimate_single_use_retry() {
8823 let server = OAuthServer::with_defaults();
8824 let client = OAuthClient::builder("c1")
8825 .redirect_uri("http://127.0.0.1/cb")
8826 .build()
8827 .unwrap();
8828 server.register_client(client).unwrap();
8829
8830 let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
8831 let (code, _) = server
8832 .authorize(&AuthorizationRequest {
8833 response_type: "code".to_string(),
8834 client_id: "c1".to_string(),
8835 redirect_uri: "http://127.0.0.1/cb".to_string(),
8836 scopes: vec![],
8837 resource: None,
8838 state: None,
8839 code_challenge: compute_s256_challenge(verifier).unwrap(),
8840 code_challenge_method: CodeChallengeMethod::S256,
8841 })
8842 .unwrap();
8843
8844 let request = |code_verifier: &str| TokenRequest {
8845 grant_type: "authorization_code".to_string(),
8846 code: Some(code.clone()),
8847 redirect_uri: Some("http://127.0.0.1/cb".to_string()),
8848 client_id: "c1".to_string(),
8849 client_secret: None,
8850 code_verifier: Some(code_verifier.to_string()),
8851 refresh_token: None,
8852 scopes: None,
8853 resource: None,
8854 };
8855
8856 let malformed_verifier = "a".repeat(PKCE_CODE_VERIFIER_MIN_BYTES - 1);
8857 let error = server.token(&request(&malformed_verifier)).unwrap_err();
8858 assert!(matches!(&error, OAuthError::InvalidGrant(_)));
8859 assert_eq!(error.description(), OAUTH_INVALID_GRANT_ERROR);
8860
8861 let wrong_verifier = "A".repeat(PKCE_CODE_VERIFIER_MIN_BYTES);
8862 let error = server.token(&request(&wrong_verifier)).unwrap_err();
8863 assert!(matches!(&error, OAuthError::InvalidGrant(_)));
8864 assert_eq!(error.description(), OAUTH_INVALID_GRANT_ERROR);
8865 assert!(
8866 server
8867 .state
8868 .read()
8869 .unwrap()
8870 .authorization_codes
8871 .contains_key(&authorization_code_digest(&code))
8872 );
8873
8874 server.token(&request(verifier)).unwrap();
8875 assert!(
8876 !server
8877 .state
8878 .read()
8879 .unwrap()
8880 .authorization_codes
8881 .contains_key(&authorization_code_digest(&code))
8882 );
8883 let replay = server.token(&request(verifier)).unwrap_err();
8884 assert!(matches!(&replay, OAuthError::InvalidGrant(_)));
8885 assert_eq!(replay.description(), OAUTH_INVALID_GRANT_ERROR);
8886 }
8887
8888 #[test]
8889 fn config_cannot_weaken_fixed_rfc7636_verifier_maximum() {
8890 let mut config = OAuthServerConfig::default();
8891 config.max_code_verifier_length = usize::MAX;
8892 let error = config.validate().expect_err("RFC 7636 maximum is fixed");
8893 assert!(matches!(&error, OAuthError::ServerError(_)));
8894 assert!(error.description().contains("max_code_verifier_length"));
8895 }
8896
8897 #[test]
8902 fn server_authorize_empty_code_challenge() {
8903 let server = OAuthServer::with_defaults();
8904 let client = OAuthClient::builder("c1")
8905 .redirect_uri("http://127.0.0.1/cb")
8906 .scope("read")
8907 .build()
8908 .unwrap();
8909 server.register_client(client).unwrap();
8910
8911 let err = server
8912 .authorize(&AuthorizationRequest {
8913 response_type: "code".to_string(),
8914 client_id: "c1".to_string(),
8915 redirect_uri: "http://127.0.0.1/cb".to_string(),
8916 scopes: vec!["read".to_string()],
8917 resource: None,
8918 state: None,
8919 code_challenge: String::new(),
8920 code_challenge_method: CodeChallengeMethod::S256,
8921 })
8922 .unwrap_err();
8923
8924 assert_eq!(err.error_code(), "invalid_request");
8925 assert!(err.description().contains("code_challenge"));
8926 }
8927
8928 #[test]
8929 fn server_authorize_with_state_in_redirect() {
8930 let server = OAuthServer::with_defaults();
8931 let client = OAuthClient::builder("c1")
8932 .redirect_uri("http://127.0.0.1/cb")
8933 .scope("read")
8934 .build()
8935 .unwrap();
8936 server.register_client(client).unwrap();
8937
8938 let (code, redirect) = server
8939 .authorize(&AuthorizationRequest {
8940 response_type: "code".to_string(),
8941 client_id: "c1".to_string(),
8942 redirect_uri: "http://127.0.0.1/cb".to_string(),
8943 scopes: vec!["read".to_string()],
8944 resource: None,
8945 state: Some("my-csrf-state".to_string()),
8946 code_challenge: compute_s256_challenge(
8947 "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
8948 )
8949 .unwrap(),
8950 code_challenge_method: CodeChallengeMethod::S256,
8951 })
8952 .unwrap();
8953
8954 assert!(redirect.contains("code="));
8956 assert!(redirect.contains(&url_encode(&code)));
8957 assert!(redirect.contains("state=my-csrf-state"));
8958 let redirect = Url::parse(&redirect).unwrap();
8959 let issuers: Vec<_> = redirect
8960 .query_pairs()
8961 .filter(|(name, _)| name.as_ref() == "iss")
8962 .map(|(_, value)| value.into_owned())
8963 .collect();
8964 assert_eq!(issuers, [server.config().issuer.clone()]);
8965 }
8966
8967 #[test]
8968 fn server_authorize_redirect_with_existing_query() {
8969 let server = OAuthServer::with_defaults();
8970 let client = OAuthClient::builder("c1")
8971 .redirect_uri("http://127.0.0.1/cb?foo=bar")
8972 .scope("read")
8973 .build()
8974 .unwrap();
8975 server.register_client(client).unwrap();
8976
8977 let (_code, redirect) = server
8978 .authorize(&AuthorizationRequest {
8979 response_type: "code".to_string(),
8980 client_id: "c1".to_string(),
8981 redirect_uri: "http://127.0.0.1/cb?foo=bar".to_string(),
8982 scopes: vec!["read".to_string()],
8983 resource: None,
8984 state: None,
8985 code_challenge: compute_s256_challenge(
8986 "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
8987 )
8988 .unwrap(),
8989 code_challenge_method: CodeChallengeMethod::S256,
8990 })
8991 .unwrap();
8992
8993 assert!(redirect.starts_with("http://127.0.0.1/cb?foo=bar&code="));
8995 assert!(redirect.contains("&iss=https%3A%2F%2Ffastmcp.invalid%2F"));
8996 }
8997
8998 #[test]
9003 fn oauth_error_access_denied_into_mcp_error() {
9004 let err = OAuthError::AccessDenied("denied".to_string());
9005 let mcp: McpError = err.into();
9006 assert_eq!(mcp.code, McpErrorCode::ResourceForbidden);
9007 }
9008
9009 #[test]
9010 fn oauth_error_description_all_variants() {
9011 let cases: Vec<(OAuthError, &str)> = vec![
9012 (OAuthError::ServerError("srv".into()), "srv"),
9013 (OAuthError::TemporarilyUnavailable("tmp".into()), "tmp"),
9014 (OAuthError::UnsupportedResponseType("rt".into()), "rt"),
9015 ];
9016 for (err, expected) in cases {
9017 assert_eq!(err.description(), expected);
9018 }
9019 }
9020
9021 #[test]
9022 fn oauth_error_display_all_remaining_variants() {
9023 let err = OAuthError::TemporarilyUnavailable("try later".into());
9024 assert_eq!(format!("{err}"), "temporarily_unavailable: try later");
9025
9026 let err = OAuthError::UnsupportedResponseType("bad".into());
9027 assert_eq!(format!("{err}"), "unsupported_response_type: bad");
9028
9029 let err = OAuthError::AccessDenied("nope".into());
9030 assert_eq!(format!("{err}"), "access_denied: nope");
9031 }
9032
9033 #[test]
9038 fn server_revoke_unknown_token_succeeds() {
9039 let server = OAuthServer::with_defaults();
9040 let client = OAuthClient::builder("c1")
9041 .redirect_uri("http://127.0.0.1/cb")
9042 .build()
9043 .unwrap();
9044 server.register_client(client).unwrap();
9045
9046 server.revoke("no-such-token", "c1", None).unwrap();
9048 }
9049
9050 #[test]
9051 fn server_revoke_token_owned_by_other_client() {
9052 let server = OAuthServer::with_defaults();
9053 let client1 = OAuthClient::builder("c1")
9054 .redirect_uri("http://127.0.0.1/cb")
9055 .scope("read")
9056 .build()
9057 .unwrap();
9058 let client2 = OAuthClient::builder("c2")
9059 .redirect_uri("http://127.0.0.1/cb")
9060 .scope("read")
9061 .build()
9062 .unwrap();
9063 server.register_client(client1).unwrap();
9064 server.register_client(client2).unwrap();
9065
9066 let token_resp = issue_access_token_via_auth_code(
9067 &server,
9068 "c1",
9069 "http://127.0.0.1/cb",
9070 &["read"],
9071 "user1",
9072 );
9073 let refresh_token = token_resp
9074 .refresh_token
9075 .clone()
9076 .expect("authorization-code flow issues a refresh token");
9077
9078 server.revoke(&token_resp.access_token, "c2", None).unwrap();
9081 server.revoke(&refresh_token, "c2", None).unwrap();
9082
9083 assert!(
9085 server
9086 .validate_access_token(&token_resp.access_token)
9087 .is_some()
9088 );
9089 let state = server.state.read().unwrap();
9090 assert!(
9091 state
9092 .refresh_tokens
9093 .contains_key(&refresh_token_digest(&refresh_token))
9094 );
9095 assert!(
9096 !state
9097 .revoked_tokens
9098 .contains_key(&access_token_digest(&token_resp.access_token))
9099 );
9100 assert!(
9101 !state
9102 .revoked_tokens
9103 .contains_key(&refresh_token_digest(&refresh_token))
9104 );
9105 }
9106
9107 #[test]
9108 fn server_revoke_unknown_client_fails() {
9109 let server = OAuthServer::with_defaults();
9110 let err = server.revoke("some-token", "unknown", None).unwrap_err();
9111 assert_eq!(err.error_code(), "invalid_client");
9112 }
9113
9114 #[test]
9119 fn server_unregister_client_removes_auth_codes() {
9120 let server = OAuthServer::with_defaults();
9121 let client = OAuthClient::builder("c1")
9122 .redirect_uri("http://127.0.0.1/cb")
9123 .scope("read")
9124 .build()
9125 .unwrap();
9126 server.register_client(client).unwrap();
9127
9128 let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
9130 let (code, _) = server
9131 .authorize(&AuthorizationRequest {
9132 response_type: "code".to_string(),
9133 client_id: "c1".to_string(),
9134 redirect_uri: "http://127.0.0.1/cb".to_string(),
9135 scopes: vec!["read".to_string()],
9136 resource: None,
9137 state: None,
9138 code_challenge: compute_s256_challenge(verifier).unwrap(),
9139 code_challenge_method: CodeChallengeMethod::S256,
9140 })
9141 .unwrap();
9142
9143 {
9145 let state = server.state.read().unwrap();
9146 assert!(
9147 state
9148 .authorization_codes
9149 .contains_key(&authorization_code_digest(&code))
9150 );
9151 }
9152
9153 server.unregister_client("c1").unwrap();
9155
9156 {
9158 let state = server.state.read().unwrap();
9159 assert!(
9160 !state
9161 .authorization_codes
9162 .contains_key(&authorization_code_digest(&code))
9163 );
9164 }
9165 }
9166
9167 #[test]
9172 fn server_with_defaults_is_valid() {
9173 let server = OAuthServer::with_defaults();
9174 assert_eq!(server.config().issuer, "https://fastmcp.invalid/");
9175 assert!(server.config().allow_public_clients);
9176 }
9177
9178 #[test]
9179 fn server_get_client_none_for_unknown() {
9180 let server = OAuthServer::with_defaults();
9181 assert!(server.get_client("nonexistent").is_none());
9182 }
9183
9184 #[test]
9185 fn server_validate_access_token_after_revoke() {
9186 let server = OAuthServer::with_defaults();
9187 let client = OAuthClient::builder("c1")
9188 .redirect_uri("http://127.0.0.1/cb")
9189 .scope("read")
9190 .build()
9191 .unwrap();
9192 server.register_client(client).unwrap();
9193
9194 let resp = issue_access_token_via_auth_code(
9195 &server,
9196 "c1",
9197 "http://127.0.0.1/cb",
9198 &["read"],
9199 "user1",
9200 );
9201
9202 assert!(server.validate_access_token(&resp.access_token).is_some());
9203 server.revoke(&resp.access_token, "c1", None).unwrap();
9204 assert!(server.validate_access_token(&resp.access_token).is_none());
9205 }
9206
9207 #[test]
9208 fn token_verifier_claims_contain_client_id_and_issuer_but_no_false_iat() {
9209 let server = Arc::new(OAuthServer::with_defaults());
9210 let client = OAuthClient::builder("my-app")
9211 .redirect_uri("http://127.0.0.1/cb")
9212 .scope("read")
9213 .build()
9214 .unwrap();
9215 server.register_client(client).unwrap();
9216
9217 let token_resp = issue_access_token_via_auth_code(
9218 server.as_ref(),
9219 "my-app",
9220 "http://127.0.0.1/cb",
9221 &["read"],
9222 "user42",
9223 );
9224
9225 let verifier = server.token_verifier();
9226 let cx = asupersync::Cx::for_testing();
9227 let mcp_ctx = McpContext::new(cx, 1);
9228 let auth_request = AuthRequest {
9229 method: "test",
9230 params: None,
9231 transport_authorization: None,
9232 request_id: 1,
9233 };
9234 let access = AccessToken {
9235 scheme: "Bearer".to_string(),
9236 token: token_resp.access_token,
9237 };
9238 let auth = verifier.verify(&mcp_ctx, auth_request, &access).unwrap();
9239
9240 let claims = auth.claims.unwrap();
9242 assert_eq!(claims["client_id"], "my-app");
9243 assert_eq!(claims["iss"], "https://fastmcp.invalid/");
9244 assert!(claims.get("iat").is_none());
9245 }
9246
9247 #[test]
9248 fn token_verifier_uses_client_id_as_display_subject_when_grant_has_no_subject() {
9249 let server = Arc::new(OAuthServer::with_defaults());
9250 server
9251 .register_client(bounded_test_client("service-client"))
9252 .unwrap();
9253 let token_resp = server.issue_tokens("service-client", &[], None).unwrap();
9254 let verifier = server.token_verifier();
9255 let cx = asupersync::Cx::for_testing();
9256 let mcp_ctx = McpContext::new(cx, 1);
9257 let auth = verifier
9258 .verify(
9259 &mcp_ctx,
9260 AuthRequest {
9261 method: "test",
9262 params: None,
9263 transport_authorization: None,
9264 request_id: 1,
9265 },
9266 &AccessToken {
9267 scheme: "Bearer".to_string(),
9268 token: token_resp.access_token,
9269 },
9270 )
9271 .unwrap();
9272
9273 assert_eq!(auth.subject.as_deref(), Some("service-client"));
9274 assert_eq!(auth.claims.as_ref().unwrap()["client_id"], "service-client");
9275 assert!(auth.claims.as_ref().unwrap()["grant_subject"].is_null());
9276 assert!(auth.session_owner().is_some());
9277 }
9278
9279 #[test]
9280 fn oauth_session_owner_frames_every_identity_namespace() {
9281 let issuer = "https://issuer.example/";
9282 let epoch = test_registration_epoch(1);
9283 let stable = oauth_session_owner(issuer, "service-client", epoch, Some("subject"))
9284 .expect("bounded owner");
9285
9286 assert_eq!(
9287 stable,
9288 oauth_session_owner(issuer, "service-client", epoch, Some("subject"))
9289 .expect("stable owner")
9290 );
9291 assert_ne!(
9292 oauth_session_owner(issuer, "service-client", epoch, None).expect("client owner"),
9293 oauth_session_owner(issuer, "service-client", epoch, Some("service-client"))
9294 .expect("subject owner")
9295 );
9296 assert_ne!(
9297 stable,
9298 oauth_session_owner(
9299 "https://other-issuer.example/",
9300 "service-client",
9301 epoch,
9302 Some("subject"),
9303 )
9304 .expect("different issuer owner")
9305 );
9306 assert_ne!(
9307 stable,
9308 oauth_session_owner(issuer, "other-client", epoch, Some("subject"))
9309 .expect("different client owner")
9310 );
9311 assert_ne!(
9312 stable,
9313 oauth_session_owner(
9314 issuer,
9315 "service-client",
9316 test_registration_epoch(2),
9317 Some("subject"),
9318 )
9319 .expect("different registration owner")
9320 );
9321
9322 let first = AuthContext::with_subject("same-display").with_session_owner(stable);
9323 let second = AuthContext::with_subject("same-display").with_session_owner(
9324 oauth_session_owner(
9325 issuer,
9326 "service-client",
9327 test_registration_epoch(2),
9328 Some("subject"),
9329 )
9330 .expect("second registration owner"),
9331 );
9332 assert_ne!(
9333 crate::auth::principal_fingerprint(Some(&first)).expect("first fingerprint"),
9334 crate::auth::principal_fingerprint(Some(&second)).expect("second fingerprint")
9335 );
9336 }
9337
9338 #[test]
9339 fn refresh_preserves_owner_and_absolute_family_deadline() {
9340 let server = Arc::new(OAuthServer::with_defaults());
9341 server.register_client(bounded_test_client("c1")).unwrap();
9342 let initial = server.issue_tokens("c1", &[], Some("subject")).unwrap();
9343 let first_refresh = initial.refresh_token.expect("initial refresh token");
9344 let family_expires_at = server
9345 .state
9346 .read()
9347 .unwrap()
9348 .refresh_tokens
9349 .get(&refresh_token_digest(&first_refresh))
9350 .expect("initial refresh metadata")
9351 .family_expires_at;
9352 let cx = McpContext::new(asupersync::Cx::for_testing(), 1);
9353 let request = AuthRequest {
9354 method: "test",
9355 params: None,
9356 transport_authorization: None,
9357 request_id: 1,
9358 };
9359 let verifier = server.token_verifier();
9360 let initial_auth = verifier
9361 .verify(
9362 &cx,
9363 request,
9364 &AccessToken {
9365 scheme: "Bearer".to_string(),
9366 token: initial.access_token,
9367 },
9368 )
9369 .expect("initial token verifies");
9370
9371 let rotated = server
9372 .token(&bounded_refresh_request("c1", &first_refresh))
9373 .expect("refresh rotation");
9374 let rotated_refresh = rotated.refresh_token.expect("rotated refresh token");
9375 let rotated_auth = verifier
9376 .verify(
9377 &cx,
9378 request,
9379 &AccessToken {
9380 scheme: "Bearer".to_string(),
9381 token: rotated.access_token,
9382 },
9383 )
9384 .expect("rotated token verifies");
9385
9386 assert_eq!(initial_auth.session_owner(), rotated_auth.session_owner());
9387 assert_eq!(
9388 crate::auth::principal_fingerprint(Some(&initial_auth)).expect("initial fingerprint"),
9389 crate::auth::principal_fingerprint(Some(&rotated_auth)).expect("rotated fingerprint")
9390 );
9391 let state = server.state.read().unwrap();
9392 let stored = state
9393 .refresh_tokens
9394 .get(&refresh_token_digest(&rotated_refresh))
9395 .expect("rotated refresh metadata");
9396 assert_eq!(stored.family_expires_at, family_expires_at);
9397 assert_eq!(stored.expires_at, family_expires_at);
9398 }
9399
9400 #[test]
9401 fn refresh_clamps_access_credential_to_remaining_family_lifetime() {
9402 let server = OAuthServer::with_defaults();
9403 server.register_client(bounded_test_client("c1")).unwrap();
9404 let initial = server.issue_tokens("c1", &[], Some("subject")).unwrap();
9405 let refresh = initial.refresh_token.expect("initial refresh token");
9406 let refresh_digest = refresh_token_digest(&refresh);
9407 let family_expires_at = Instant::now()
9408 .checked_add(Duration::from_mins(5))
9409 .expect("test family deadline");
9410 {
9411 let mut state = server.state.write().unwrap();
9412 state
9413 .refresh_tokens
9414 .get_mut(&refresh_digest)
9415 .expect("stored refresh token")
9416 .family_expires_at = family_expires_at;
9417 }
9418
9419 let rotated = server
9420 .token(&bounded_refresh_request("c1", &refresh))
9421 .expect("refresh rotation within the shortened family");
9422
9423 let state = server.state.read().unwrap();
9424 let access = state
9425 .access_tokens
9426 .get(&access_token_digest(&rotated.access_token))
9427 .expect("rotated access metadata");
9428 assert_eq!(access.metadata.expires_at, family_expires_at);
9429 assert_eq!(access.family_expires_at, family_expires_at);
9430 assert_eq!(
9431 rotated.expires_in,
9432 access
9433 .metadata
9434 .expires_at
9435 .saturating_duration_since(access.metadata.issued_at)
9436 .as_secs()
9437 );
9438 assert!(rotated.expires_in > 0);
9439 assert!(rotated.expires_in <= 5 * 60);
9440 let rotated_refresh = rotated.refresh_token.expect("rotated refresh token");
9441 let refresh = state
9442 .refresh_tokens
9443 .get(&refresh_token_digest(&rotated_refresh))
9444 .expect("rotated refresh metadata");
9445 assert_eq!(refresh.metadata.expires_at, family_expires_at);
9446 assert_eq!(refresh.family_expires_at, family_expires_at);
9447 }
9448
9449 #[test]
9450 fn same_client_id_reregistration_receives_a_new_session_owner() {
9451 let server = Arc::new(OAuthServer::with_defaults());
9452 server
9453 .register_client(bounded_test_client("service-client"))
9454 .unwrap();
9455 let old = server.issue_tokens("service-client", &[], None).unwrap();
9456 let verifier = server.token_verifier();
9457 let cx = McpContext::new(asupersync::Cx::for_testing(), 1);
9458 let request = AuthRequest {
9459 method: "test",
9460 params: None,
9461 transport_authorization: None,
9462 request_id: 1,
9463 };
9464 let old_auth = verifier
9465 .verify(
9466 &cx,
9467 request,
9468 &AccessToken {
9469 scheme: "Bearer".to_string(),
9470 token: old.access_token,
9471 },
9472 )
9473 .expect("old registration token verifies");
9474
9475 server.unregister_client("service-client").unwrap();
9476 server
9477 .register_client(bounded_test_client("service-client"))
9478 .unwrap();
9479 let fresh = server.issue_tokens("service-client", &[], None).unwrap();
9480 let fresh_auth = verifier
9481 .verify(
9482 &cx,
9483 request,
9484 &AccessToken {
9485 scheme: "Bearer".to_string(),
9486 token: fresh.access_token,
9487 },
9488 )
9489 .expect("fresh registration token verifies");
9490
9491 assert_eq!(old_auth.subject, fresh_auth.subject);
9492 assert_ne!(old_auth.session_owner(), fresh_auth.session_owner());
9493 assert_ne!(
9494 crate::auth::principal_fingerprint(Some(&old_auth)).expect("old fingerprint"),
9495 crate::auth::principal_fingerprint(Some(&fresh_auth)).expect("fresh fingerprint")
9496 );
9497 }
9498
9499 #[test]
9500 fn oauth_token_expires_in_secs_positive() {
9501 let token = OAuthToken {
9502 token: String::new(),
9503 token_type: TokenType::Bearer,
9504 client_id: "c".to_string(),
9505 scopes: vec![],
9506 resource: None,
9507 issued_at: Instant::now(),
9508 expires_at: Instant::now()
9509 .checked_add(Duration::from_secs(3600))
9510 .expect("test deadline"),
9511 subject: None,
9512 is_refresh_token: false,
9513 };
9514 assert!(token.expires_in_secs() > 0);
9516 }
9517
9518 #[test]
9519 fn server_refresh_token_confidential_client_auth_fails() {
9520 let server = OAuthServer::with_defaults();
9521 let client = OAuthClient::builder("c1")
9522 .secret("correct-secret")
9523 .redirect_uri("http://127.0.0.1/cb")
9524 .scope("read")
9525 .build()
9526 .unwrap();
9527 server.register_client(client).unwrap();
9528
9529 let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
9531 let (code, _) = server
9532 .authorize(&AuthorizationRequest {
9533 response_type: "code".to_string(),
9534 client_id: "c1".to_string(),
9535 redirect_uri: "http://127.0.0.1/cb".to_string(),
9536 scopes: vec!["read".to_string()],
9537 resource: None,
9538 state: None,
9539 code_challenge: compute_s256_challenge(verifier).unwrap(),
9540 code_challenge_method: CodeChallengeMethod::S256,
9541 })
9542 .unwrap();
9543
9544 let token_resp = server
9546 .token(&TokenRequest {
9547 grant_type: "authorization_code".to_string(),
9548 code: Some(code),
9549 redirect_uri: Some("http://127.0.0.1/cb".to_string()),
9550 client_id: "c1".to_string(),
9551 client_secret: Some("correct-secret".to_string()),
9552 code_verifier: Some(verifier.to_string()),
9553 refresh_token: None,
9554 scopes: None,
9555 resource: None,
9556 })
9557 .unwrap();
9558
9559 let refresh = token_resp.refresh_token.unwrap();
9560
9561 let err = server
9563 .token(&TokenRequest {
9564 grant_type: "refresh_token".to_string(),
9565 code: None,
9566 redirect_uri: None,
9567 client_id: "c1".to_string(),
9568 client_secret: Some("wrong-secret".to_string()),
9569 code_verifier: None,
9570 refresh_token: Some(refresh.clone()),
9571 scopes: None,
9572 resource: None,
9573 })
9574 .unwrap_err();
9575
9576 assert_eq!(err.error_code(), "invalid_client");
9577 assert_eq!(err.description(), OAUTH_CLIENT_AUTHENTICATION_ERROR);
9578 assert!(!err.description().contains("wrong-secret"));
9579
9580 let one_past_secret = "x".repeat(MAX_OAUTH_CLIENT_CREDENTIAL_BYTES + 1);
9581 let oversized_err = server
9582 .token(&TokenRequest {
9583 grant_type: "refresh_token".to_string(),
9584 code: None,
9585 redirect_uri: None,
9586 client_id: "c1".to_string(),
9587 client_secret: Some(one_past_secret.clone()),
9588 code_verifier: None,
9589 refresh_token: Some(refresh),
9590 scopes: None,
9591 resource: None,
9592 })
9593 .unwrap_err();
9594
9595 assert_eq!(oversized_err.error_code(), "invalid_client");
9596 assert_eq!(
9597 oversized_err.description(),
9598 OAUTH_CLIENT_AUTHENTICATION_ERROR
9599 );
9600 assert_eq!(oversized_err.description(), err.description());
9601 assert!(!oversized_err.description().contains(&one_past_secret));
9602 }
9603
9604 #[test]
9605 fn code_challenge_method_parse_unknown() {
9606 assert!(CodeChallengeMethod::parse("sha512").is_none());
9607 assert!(CodeChallengeMethod::parse("").is_none());
9608 }
9609
9610 #[test]
9611 fn constant_time_eq_different_lengths() {
9612 assert!(!constant_time_eq("short", "longer_string"));
9613 assert!(!constant_time_eq("", "a"));
9614 }
9615
9616 #[test]
9617 fn constant_time_eq_empty_strings() {
9618 assert!(constant_time_eq("", ""));
9619 }
9620
9621 #[test]
9622 fn loopback_match_rejects_different_host_variants() {
9623 assert!(!loopback_match(
9624 "http://localhost:3000/cb",
9625 "http://127.0.0.1:8080/cb"
9626 ));
9627 assert!(!loopback_match(
9628 "http://127.0.0.1:3000/cb",
9629 "http://[::1]:9000/cb"
9630 ));
9631 }
9632
9633 #[test]
9634 fn url_encode_empty_and_unicode() {
9635 assert_eq!(url_encode(""), "");
9636 let encoded = url_encode("ü");
9638 assert!(encoded.contains('%'));
9639 }
9640
9641 #[test]
9646 fn server_revoke_confidential_client_wrong_secret() {
9647 let server = OAuthServer::with_defaults();
9648 let client = OAuthClient::builder("c1")
9649 .secret("correct")
9650 .redirect_uri("http://127.0.0.1/cb")
9651 .scope("read")
9652 .build()
9653 .unwrap();
9654 server.register_client(client).unwrap();
9655
9656 let err = server.revoke("any-token", "c1", Some("wrong")).unwrap_err();
9657 assert_eq!(err.error_code(), "invalid_client");
9658 }
9659
9660 #[test]
9661 fn server_validate_access_token_expired_returns_none() {
9662 let server = OAuthServer::with_defaults();
9663 let client = OAuthClient::builder("c1")
9664 .redirect_uri("http://127.0.0.1/cb")
9665 .scope("read")
9666 .build()
9667 .unwrap();
9668 server.register_client(client).unwrap();
9669
9670 let resp = issue_access_token_via_auth_code(
9671 &server,
9672 "c1",
9673 "http://127.0.0.1/cb",
9674 &["read"],
9675 "user1",
9676 );
9677
9678 {
9679 let mut state = server.state.write().unwrap();
9680 let stored = state
9681 .access_tokens
9682 .get_mut(&access_token_digest(&resp.access_token))
9683 .expect("stored access token");
9684 stored.metadata.expires_at = Instant::now();
9685 }
9686 assert!(server.validate_access_token(&resp.access_token).is_none());
9687 }
9688
9689 #[test]
9690 fn server_authorize_without_state_omits_state_from_redirect() {
9691 let server = OAuthServer::with_defaults();
9692 let client = OAuthClient::builder("c1")
9693 .redirect_uri("http://127.0.0.1/cb")
9694 .build()
9695 .unwrap();
9696 server.register_client(client).unwrap();
9697
9698 let (_code, redirect) = server
9699 .authorize(&AuthorizationRequest {
9700 response_type: "code".to_string(),
9701 client_id: "c1".to_string(),
9702 redirect_uri: "http://127.0.0.1/cb".to_string(),
9703 scopes: vec![],
9704 resource: None,
9705 state: None,
9706 code_challenge: compute_s256_challenge(
9707 "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
9708 )
9709 .unwrap(),
9710 code_challenge_method: CodeChallengeMethod::S256,
9711 })
9712 .unwrap();
9713
9714 assert!(redirect.contains("code="));
9715 assert!(!redirect.contains("state="));
9716 }
9717
9718 #[test]
9719 fn server_refresh_token_client_deleted_after_issue_fails_authentication_first() {
9720 let server = OAuthServer::with_defaults();
9721 let client = OAuthClient::builder("c1")
9722 .redirect_uri("http://127.0.0.1/cb")
9723 .scope("read")
9724 .build()
9725 .unwrap();
9726 server.register_client(client).unwrap();
9727
9728 let token_resp = issue_access_token_via_auth_code(
9729 &server,
9730 "c1",
9731 "http://127.0.0.1/cb",
9732 &["read"],
9733 "user1",
9734 );
9735 let refresh = token_resp.refresh_token.unwrap();
9736
9737 server.unregister_client("c1").unwrap();
9739
9740 let err = server
9741 .token(&TokenRequest {
9742 grant_type: "refresh_token".to_string(),
9743 code: None,
9744 redirect_uri: None,
9745 client_id: "c1".to_string(),
9746 client_secret: None,
9747 code_verifier: None,
9748 refresh_token: Some(refresh),
9749 scopes: None,
9750 resource: None,
9751 })
9752 .unwrap_err();
9753
9754 assert_eq!(err.error_code(), "invalid_client");
9757 assert_eq!(err.description(), OAUTH_CLIENT_AUTHENTICATION_ERROR);
9758 }
9759
9760 #[test]
9761 fn server_issue_tokens_empty_scopes_returns_no_scope() {
9762 let server = OAuthServer::with_defaults();
9763 let client = OAuthClient::builder("c1")
9764 .redirect_uri("http://127.0.0.1/cb")
9765 .build()
9766 .unwrap();
9767 server.register_client(client).unwrap();
9768
9769 let resp =
9770 issue_access_token_via_auth_code(&server, "c1", "http://127.0.0.1/cb", &[], "user1");
9771
9772 assert!(resp.scope.is_none());
9773 }
9774
9775 #[test]
9776 fn server_revoke_refresh_token_specifically() {
9777 let server = OAuthServer::with_defaults();
9778 let client = OAuthClient::builder("c1")
9779 .redirect_uri("http://127.0.0.1/cb")
9780 .scope("read")
9781 .build()
9782 .unwrap();
9783 server.register_client(client).unwrap();
9784
9785 let resp = issue_access_token_via_auth_code(
9786 &server,
9787 "c1",
9788 "http://127.0.0.1/cb",
9789 &["read"],
9790 "user1",
9791 );
9792 let access = resp.access_token.clone();
9793 let refresh = resp.refresh_token.unwrap();
9794
9795 server.revoke(&refresh, "c1", None).unwrap();
9797 assert!(server.validate_access_token(&access).is_none());
9798
9799 {
9801 let state = server.state.read().unwrap();
9802 assert!(
9803 state
9804 .revoked_tokens
9805 .contains_key(&refresh_token_digest(&refresh))
9806 );
9807 }
9808 }
9809
9810 #[test]
9811 fn refresh_revocation_cascades_only_within_the_selected_grant_family() {
9812 let server = OAuthServer::with_defaults();
9813 server.register_client(bounded_test_client("c1")).unwrap();
9814 let first = server.issue_tokens("c1", &[], None).unwrap();
9815 let second = server.issue_tokens("c1", &[], None).unwrap();
9816 let first_refresh = first.refresh_token.expect("first refresh token");
9817 let second_refresh = second.refresh_token.expect("second refresh token");
9818 let state = server.state.read().unwrap();
9819 let first_grant = state
9820 .refresh_tokens
9821 .get(&refresh_token_digest(&first_refresh))
9822 .expect("first refresh metadata")
9823 .grant_id;
9824 let second_grant = state
9825 .refresh_tokens
9826 .get(&refresh_token_digest(&second_refresh))
9827 .expect("second refresh metadata")
9828 .grant_id;
9829 assert_ne!(first_grant, second_grant);
9830 drop(state);
9831
9832 server.revoke(&first_refresh, "c1", None).unwrap();
9833
9834 assert!(server.validate_access_token(&first.access_token).is_none());
9835 assert!(server.validate_access_token(&second.access_token).is_some());
9836 let state = server.state.read().unwrap();
9837 assert!(
9838 !state
9839 .refresh_tokens
9840 .contains_key(&refresh_token_digest(&first_refresh))
9841 );
9842 assert!(
9843 state
9844 .refresh_tokens
9845 .contains_key(&refresh_token_digest(&second_refresh))
9846 );
9847 assert!(
9848 state
9849 .access_tokens
9850 .values()
9851 .all(|token| token.grant_id != first_grant)
9852 );
9853 assert!(
9854 state
9855 .refresh_tokens
9856 .values()
9857 .all(|token| token.grant_id != first_grant)
9858 );
9859 }
9860
9861 #[test]
9862 fn issuer_validation_accepts_exact_bound_and_canonical_https_urls() {
9863 let exact = exact_ascii_value("https://issuer.example/", MAX_OAUTH_ISSUER_BYTES);
9864 assert!(
9865 OAuthServer::try_new(OAuthServerConfig {
9866 issuer: exact,
9867 ..OAuthServerConfig::default()
9868 })
9869 .is_ok()
9870 );
9871 for issuer in ["https://issuer.example/", "https://issuer.example/oauth"] {
9872 assert!(
9873 OAuthServer::try_new(OAuthServerConfig {
9874 issuer: issuer.to_string(),
9875 ..OAuthServerConfig::default()
9876 })
9877 .is_ok(),
9878 "canonical HTTPS issuer should be admitted"
9879 );
9880 }
9881 }
9882
9883 #[test]
9884 fn issuer_validation_rejects_one_past_and_unsafe_urls_with_fixed_error() {
9885 let one_past = exact_ascii_value("https://issuer.example/", MAX_OAUTH_ISSUER_BYTES + 1);
9886 let invalid = [
9887 one_past.as_str(),
9888 "fastmcp",
9889 "http://issuer.example",
9890 "http://localhost:8080",
9891 "http://127.0.0.1:8080/oauth",
9892 "http://[::1]:8080/oauth",
9893 "https:issuer.example",
9894 "https://ISSUER.example/",
9895 "https://issuer.example:443/",
9896 "https://issuer.example/a/../",
9897 "https://user:password@issuer.example",
9898 "https://@issuer.example",
9899 "https://issuer.example/#fragment",
9900 "https://issuer.example/?tenant=one",
9901 "javascript:alert(1)",
9902 "https://issuer.example/\r\nheader",
9903 ];
9904
9905 for issuer in invalid {
9906 let error = match OAuthServer::try_new(OAuthServerConfig {
9907 issuer: issuer.to_string(),
9908 ..OAuthServerConfig::default()
9909 }) {
9910 Ok(_) => panic!("unsafe issuer must fail closed"),
9911 Err(error) => error,
9912 };
9913 assert_eq!(error.description(), OAUTH_ISSUER_ERROR);
9914 assert!(!error.description().contains(issuer));
9915 }
9916 }
9917
9918 #[test]
9919 fn config_accepts_each_exact_retention_hard_ceiling_in_isolation() {
9920 let valid = [
9921 (
9922 "max_clients",
9923 OAuthServerConfig {
9924 max_clients: HARD_MAX_OAUTH_CLIENTS,
9925 ..OAuthServerConfig::default()
9926 },
9927 ),
9928 (
9929 "max_authorization_codes",
9930 OAuthServerConfig {
9931 max_authorization_codes: HARD_MAX_AUTHORIZATION_CODES,
9932 ..OAuthServerConfig::default()
9933 },
9934 ),
9935 (
9936 "max_authorization_codes_per_client",
9937 OAuthServerConfig {
9938 max_authorization_codes_per_client: HARD_MAX_AUTHORIZATION_CODES_PER_CLIENT,
9939 ..OAuthServerConfig::default()
9940 },
9941 ),
9942 (
9943 "max_access_tokens",
9944 OAuthServerConfig {
9945 max_access_tokens: HARD_MAX_ACCESS_TOKENS,
9946 ..OAuthServerConfig::default()
9947 },
9948 ),
9949 (
9950 "max_access_tokens_per_client",
9951 OAuthServerConfig {
9952 max_access_tokens_per_client: HARD_MAX_ACCESS_TOKENS_PER_CLIENT,
9953 ..OAuthServerConfig::default()
9954 },
9955 ),
9956 (
9957 "max_refresh_tokens",
9958 OAuthServerConfig {
9959 max_refresh_tokens: HARD_MAX_REFRESH_TOKENS,
9960 ..OAuthServerConfig::default()
9961 },
9962 ),
9963 (
9964 "max_refresh_tokens_per_client",
9965 OAuthServerConfig {
9966 max_refresh_tokens_per_client: HARD_MAX_REFRESH_TOKENS_PER_CLIENT,
9967 ..OAuthServerConfig::default()
9968 },
9969 ),
9970 (
9971 "max_revocation_tombstones",
9972 OAuthServerConfig {
9973 max_revocation_tombstones: HARD_MAX_REVOCATION_TOMBSTONES,
9974 ..OAuthServerConfig::default()
9975 },
9976 ),
9977 (
9978 "max_revocation_tombstones_per_client",
9979 OAuthServerConfig {
9980 max_revocation_tombstones_per_client: HARD_MAX_REVOCATION_TOMBSTONES_PER_CLIENT,
9981 ..OAuthServerConfig::default()
9982 },
9983 ),
9984 ];
9985
9986 for (field, config) in valid {
9987 let result = config.validate();
9988 assert!(
9989 result.is_ok(),
9990 "exact {field} hard ceiling rejected: {result:?}"
9991 );
9992 }
9993 }
9994
9995 #[test]
9996 fn config_rejects_one_past_every_retention_hard_ceiling_before_mutation() {
9997 let invalid = [
9998 (
9999 "max_clients",
10000 OAuthServerConfig {
10001 max_clients: HARD_MAX_OAUTH_CLIENTS + 1,
10002 ..OAuthServerConfig::default()
10003 },
10004 ),
10005 (
10006 "max_authorization_codes",
10007 OAuthServerConfig {
10008 max_authorization_codes: HARD_MAX_AUTHORIZATION_CODES + 1,
10009 ..OAuthServerConfig::default()
10010 },
10011 ),
10012 (
10013 "max_authorization_codes_per_client",
10014 OAuthServerConfig {
10015 max_authorization_codes_per_client: HARD_MAX_AUTHORIZATION_CODES_PER_CLIENT + 1,
10016 ..OAuthServerConfig::default()
10017 },
10018 ),
10019 (
10020 "max_access_tokens",
10021 OAuthServerConfig {
10022 max_access_tokens: HARD_MAX_ACCESS_TOKENS + 1,
10023 ..OAuthServerConfig::default()
10024 },
10025 ),
10026 (
10027 "max_access_tokens_per_client",
10028 OAuthServerConfig {
10029 max_access_tokens_per_client: HARD_MAX_ACCESS_TOKENS_PER_CLIENT + 1,
10030 ..OAuthServerConfig::default()
10031 },
10032 ),
10033 (
10034 "max_refresh_tokens",
10035 OAuthServerConfig {
10036 max_refresh_tokens: HARD_MAX_REFRESH_TOKENS + 1,
10037 ..OAuthServerConfig::default()
10038 },
10039 ),
10040 (
10041 "max_refresh_tokens_per_client",
10042 OAuthServerConfig {
10043 max_refresh_tokens_per_client: HARD_MAX_REFRESH_TOKENS_PER_CLIENT + 1,
10044 ..OAuthServerConfig::default()
10045 },
10046 ),
10047 (
10048 "max_revocation_tombstones",
10049 OAuthServerConfig {
10050 max_revocation_tombstones: HARD_MAX_REVOCATION_TOMBSTONES + 1,
10051 ..OAuthServerConfig::default()
10052 },
10053 ),
10054 (
10055 "max_revocation_tombstones_per_client",
10056 OAuthServerConfig {
10057 max_revocation_tombstones_per_client: HARD_MAX_REVOCATION_TOMBSTONES_PER_CLIENT
10058 + 1,
10059 ..OAuthServerConfig::default()
10060 },
10061 ),
10062 ];
10063
10064 for (field, config) in invalid {
10065 let error = config
10066 .validate()
10067 .expect_err("one-past-hard-ceiling config must fail closed");
10068 assert!(matches!(&error, OAuthError::ServerError(_)));
10069 assert!(error.description().contains(field));
10070 assert!(error.description().contains("hard ceiling"));
10071 assert!(matches!(
10072 OAuthServer::try_new(config.clone()),
10073 Err(OAuthError::ServerError(_))
10074 ));
10075
10076 let server = OAuthServer::new(config);
10077 assert!(matches!(
10078 server.register_client(bounded_test_client("c1")),
10079 Err(OAuthError::ServerError(_))
10080 ));
10081 assert_eq!(server.stats().clients, 0);
10082 }
10083 }
10084
10085 #[test]
10086 fn config_enforces_checked_aggregate_retention_boundary() {
10087 let mut exact = OAuthServerConfig {
10088 max_access_tokens: HARD_MAX_ACCESS_TOKENS,
10089 ..OAuthServerConfig::default()
10090 };
10091 let subtotal_without_tombstones = exact
10092 .max_clients
10093 .checked_add(exact.max_authorization_codes)
10094 .and_then(|total| total.checked_add(exact.max_access_tokens))
10095 .and_then(|total| total.checked_add(exact.max_refresh_tokens))
10096 .expect("test subtotal is representable");
10097 exact.max_revocation_tombstones = HARD_MAX_OAUTH_RETAINED_ENTRIES
10098 .checked_sub(subtotal_without_tombstones)
10099 .expect("aggregate ceiling admits the default retention profile");
10100 assert!(exact.max_revocation_tombstones <= HARD_MAX_REVOCATION_TOMBSTONES);
10101 assert_eq!(
10102 exact
10103 .checked_global_retention_limit()
10104 .expect("exact aggregate is representable"),
10105 HARD_MAX_OAUTH_RETAINED_ENTRIES
10106 );
10107 exact
10108 .validate()
10109 .expect("exact aggregate retention ceiling is admitted");
10110
10111 let mut one_past = exact;
10112 one_past.max_revocation_tombstones = one_past
10113 .max_revocation_tombstones
10114 .checked_add(1)
10115 .expect("one-past test value is representable");
10116 let error = one_past
10117 .validate()
10118 .expect_err("one-past aggregate retention ceiling must fail closed");
10119 assert!(matches!(&error, OAuthError::ServerError(_)));
10120 assert!(error.description().contains("aggregate retained-state"));
10121 assert!(error.description().contains("hard ceiling"));
10122 assert!(matches!(
10123 OAuthServer::try_new(one_past.clone()),
10124 Err(OAuthError::ServerError(_))
10125 ));
10126 let server = OAuthServer::new(one_past);
10127 assert!(matches!(
10128 server.register_client(bounded_test_client("c1")),
10129 Err(OAuthError::ServerError(_))
10130 ));
10131 assert_eq!(server.stats().clients, 0);
10132
10133 let unrepresentable = OAuthServerConfig {
10134 max_clients: usize::MAX,
10135 ..OAuthServerConfig::default()
10136 };
10137 let error = unrepresentable
10138 .checked_global_retention_limit()
10139 .expect_err("aggregate arithmetic must reject usize overflow");
10140 assert!(error.description().contains("not representable"));
10141 }
10142
10143 #[test]
10144 fn config_rejects_every_zero_state_limit() {
10145 let invalid = [
10146 (
10147 "max_clients",
10148 OAuthServerConfig {
10149 max_clients: 0,
10150 ..OAuthServerConfig::default()
10151 },
10152 ),
10153 (
10154 "max_authorization_codes",
10155 OAuthServerConfig {
10156 max_authorization_codes: 0,
10157 ..OAuthServerConfig::default()
10158 },
10159 ),
10160 (
10161 "max_authorization_codes_per_client",
10162 OAuthServerConfig {
10163 max_authorization_codes_per_client: 0,
10164 ..OAuthServerConfig::default()
10165 },
10166 ),
10167 (
10168 "max_access_tokens",
10169 OAuthServerConfig {
10170 max_access_tokens: 0,
10171 ..OAuthServerConfig::default()
10172 },
10173 ),
10174 (
10175 "max_access_tokens_per_client",
10176 OAuthServerConfig {
10177 max_access_tokens_per_client: 0,
10178 ..OAuthServerConfig::default()
10179 },
10180 ),
10181 (
10182 "max_refresh_tokens",
10183 OAuthServerConfig {
10184 max_refresh_tokens: 0,
10185 ..OAuthServerConfig::default()
10186 },
10187 ),
10188 (
10189 "max_refresh_tokens_per_client",
10190 OAuthServerConfig {
10191 max_refresh_tokens_per_client: 0,
10192 ..OAuthServerConfig::default()
10193 },
10194 ),
10195 (
10196 "max_revocation_tombstones",
10197 OAuthServerConfig {
10198 max_revocation_tombstones: 0,
10199 ..OAuthServerConfig::default()
10200 },
10201 ),
10202 (
10203 "max_revocation_tombstones_per_client",
10204 OAuthServerConfig {
10205 max_revocation_tombstones_per_client: 0,
10206 ..OAuthServerConfig::default()
10207 },
10208 ),
10209 ];
10210
10211 for (field, config) in invalid {
10212 let error = config.validate().expect_err("zero cap must fail closed");
10213 assert!(matches!(&error, OAuthError::ServerError(_)));
10214 assert!(error.description().contains(field));
10215 assert!(matches!(
10216 OAuthServer::try_new(config.clone()),
10217 Err(OAuthError::ServerError(_))
10218 ));
10219 let server = OAuthServer::new(config);
10220 assert!(matches!(
10221 server.register_client(bounded_test_client("c1")),
10222 Err(OAuthError::ServerError(_))
10223 ));
10224 assert_eq!(server.stats().clients, 0);
10225 }
10226 }
10227
10228 #[test]
10229 fn unrepresentable_lifetimes_fail_eager_and_lazy_construction() {
10230 let invalid = [
10231 (
10232 "access_token_lifetime",
10233 OAuthServerConfig {
10234 access_token_lifetime: Duration::MAX,
10235 ..OAuthServerConfig::default()
10236 },
10237 ),
10238 (
10239 "refresh_token_lifetime",
10240 OAuthServerConfig {
10241 refresh_token_lifetime: Duration::MAX,
10242 ..OAuthServerConfig::default()
10243 },
10244 ),
10245 (
10246 "authorization_code_lifetime",
10247 OAuthServerConfig {
10248 authorization_code_lifetime: Duration::MAX,
10249 ..OAuthServerConfig::default()
10250 },
10251 ),
10252 ];
10253
10254 for (field, config) in invalid {
10255 let error = config
10256 .validate()
10257 .expect_err("Duration::MAX must not form an Instant deadline");
10258 assert!(matches!(&error, OAuthError::ServerError(_)));
10259 assert!(error.description().contains(field));
10260 assert!(matches!(
10261 OAuthServer::try_new(config.clone()),
10262 Err(OAuthError::ServerError(_))
10263 ));
10264
10265 let server = OAuthServer::new(config);
10268 assert!(matches!(
10269 server.register_client(bounded_test_client("c1")),
10270 Err(OAuthError::ServerError(_))
10271 ));
10272 assert_eq!(server.stats().clients, 0);
10273 }
10274 }
10275
10276 #[test]
10277 fn registration_rejects_exact_registered_redirect_with_fragment() {
10278 let server = OAuthServer::with_defaults();
10279 let mut client = OAuthClient::builder("fragmented")
10280 .redirect_uri("https://example.com/callback")
10281 .build()
10282 .unwrap();
10283 client.redirect_uris = vec!["https://example.com/callback#fragment".to_string()];
10284 assert!(!client.validate_redirect_uri(&client.redirect_uris[0]));
10285
10286 let error = server
10287 .register_client(client)
10288 .expect_err("fragment-bearing registration must fail closed");
10289 assert!(matches!(&error, OAuthError::InvalidRequest(_)));
10290 assert_eq!(error.description(), OAUTH_CLIENT_REDIRECT_VALUE_ERROR);
10291 assert!(server.state.read().unwrap().clients.is_empty());
10292 }
10293
10294 #[test]
10295 fn client_capacity_accepts_exact_limit_and_rejects_next() {
10296 let server = OAuthServer::new(OAuthServerConfig {
10297 max_clients: 2,
10298 ..OAuthServerConfig::default()
10299 });
10300
10301 server.register_client(bounded_test_client("c1")).unwrap();
10302 server.register_client(bounded_test_client("c2")).unwrap();
10303 let error = server
10304 .register_client(bounded_test_client("c3"))
10305 .expect_err("third client must exceed exact cap");
10306
10307 assert!(matches!(error, OAuthError::TemporarilyUnavailable(_)));
10308 assert_eq!(server.stats().clients, 2);
10309
10310 server.unregister_client("c1").unwrap();
10311 server.register_client(bounded_test_client("c3")).unwrap();
10312 assert_eq!(server.stats().clients, 2);
10313 }
10314
10315 #[test]
10316 fn authorization_code_global_capacity_is_exact_and_atomic() {
10317 let server = configured_approved_test_server(OAuthServerConfig {
10318 max_authorization_codes: 2,
10319 max_authorization_codes_per_client: 2,
10320 ..OAuthServerConfig::default()
10321 });
10322 server.register_client(bounded_test_client("c1")).unwrap();
10323 server.register_client(bounded_test_client("c2")).unwrap();
10324
10325 server
10326 .authorize(&bounded_authorization_request("c1"))
10327 .unwrap();
10328 server
10329 .authorize(&bounded_authorization_request("c1"))
10330 .unwrap();
10331 let error = server
10332 .authorize(&bounded_authorization_request("c2"))
10333 .expect_err("global authorization-code cap must reject the next code");
10334
10335 assert!(matches!(error, OAuthError::TemporarilyUnavailable(_)));
10336 let state = server.state.read().unwrap();
10337 assert_eq!(state.authorization_codes.len(), 2);
10338 assert_eq!(state.authorization_code_count_for_client("c1"), 2);
10339 assert_eq!(state.authorization_code_count_for_client("c2"), 0);
10340 }
10341
10342 #[test]
10343 fn authorization_code_per_client_capacity_is_exact_and_isolated() {
10344 let server = configured_approved_test_server(OAuthServerConfig {
10345 max_authorization_codes: 3,
10346 max_authorization_codes_per_client: 1,
10347 ..OAuthServerConfig::default()
10348 });
10349 server.register_client(bounded_test_client("c1")).unwrap();
10350 server.register_client(bounded_test_client("c2")).unwrap();
10351
10352 server
10353 .authorize(&bounded_authorization_request("c1"))
10354 .unwrap();
10355 let error = server
10356 .authorize(&bounded_authorization_request("c1"))
10357 .expect_err("per-client authorization-code cap must reject the next code");
10358 assert!(matches!(error, OAuthError::TemporarilyUnavailable(_)));
10359 server
10360 .authorize(&bounded_authorization_request("c2"))
10361 .unwrap();
10362
10363 let state = server.state.read().unwrap();
10364 assert_eq!(state.authorization_codes.len(), 2);
10365 assert_eq!(state.authorization_code_count_for_client("c1"), 1);
10366 assert_eq!(state.authorization_code_count_for_client("c2"), 1);
10367 }
10368
10369 #[test]
10370 fn access_token_global_capacity_is_exact_and_pair_atomic() {
10371 let server = OAuthServer::new(OAuthServerConfig {
10372 max_access_tokens: 2,
10373 max_access_tokens_per_client: 2,
10374 ..OAuthServerConfig::default()
10375 });
10376 server.register_client(bounded_test_client("c1")).unwrap();
10377 server.register_client(bounded_test_client("c2")).unwrap();
10378
10379 server.issue_tokens("c1", &[], None).unwrap();
10380 server.issue_tokens("c1", &[], None).unwrap();
10381 let error = server
10382 .issue_tokens("c2", &[], None)
10383 .expect_err("global access-token cap must reject the next pair");
10384
10385 assert!(matches!(error, OAuthError::TemporarilyUnavailable(_)));
10386 let state = server.state.read().unwrap();
10387 assert_eq!(state.access_tokens.len(), 2);
10388 assert_eq!(state.refresh_tokens.len(), 2);
10389 assert_eq!(state.access_token_count_for_client("c2"), 0);
10390 assert_eq!(state.refresh_token_count_for_client("c2"), 0);
10391 }
10392
10393 #[test]
10394 fn access_token_per_client_capacity_is_exact_and_isolated() {
10395 let server = OAuthServer::new(OAuthServerConfig {
10396 max_access_tokens: 3,
10397 max_access_tokens_per_client: 1,
10398 ..OAuthServerConfig::default()
10399 });
10400 server.register_client(bounded_test_client("c1")).unwrap();
10401 server.register_client(bounded_test_client("c2")).unwrap();
10402
10403 server.issue_tokens("c1", &[], None).unwrap();
10404 let error = server
10405 .issue_tokens("c1", &[], None)
10406 .expect_err("per-client access-token cap must reject the next pair");
10407 assert!(matches!(error, OAuthError::TemporarilyUnavailable(_)));
10408 server.issue_tokens("c2", &[], None).unwrap();
10409
10410 let state = server.state.read().unwrap();
10411 assert_eq!(state.access_tokens.len(), 2);
10412 assert_eq!(state.refresh_tokens.len(), 2);
10413 assert_eq!(state.access_token_count_for_client("c1"), 1);
10414 assert_eq!(state.access_token_count_for_client("c2"), 1);
10415 }
10416
10417 #[test]
10418 fn refresh_token_global_capacity_is_exact_and_pair_atomic() {
10419 let server = OAuthServer::new(OAuthServerConfig {
10420 max_refresh_tokens: 2,
10421 max_refresh_tokens_per_client: 2,
10422 ..OAuthServerConfig::default()
10423 });
10424 server.register_client(bounded_test_client("c1")).unwrap();
10425 server.register_client(bounded_test_client("c2")).unwrap();
10426
10427 server.issue_tokens("c1", &[], None).unwrap();
10428 server.issue_tokens("c1", &[], None).unwrap();
10429 let error = server
10430 .issue_tokens("c2", &[], None)
10431 .expect_err("global refresh-token cap must reject the next pair");
10432
10433 assert!(matches!(error, OAuthError::TemporarilyUnavailable(_)));
10434 let state = server.state.read().unwrap();
10435 assert_eq!(state.access_tokens.len(), 2);
10436 assert_eq!(state.refresh_tokens.len(), 2);
10437 assert_eq!(state.access_token_count_for_client("c2"), 0);
10438 assert_eq!(state.refresh_token_count_for_client("c2"), 0);
10439 }
10440
10441 #[test]
10442 fn refresh_token_per_client_capacity_is_exact_and_isolated() {
10443 let server = OAuthServer::new(OAuthServerConfig {
10444 max_refresh_tokens: 3,
10445 max_refresh_tokens_per_client: 1,
10446 ..OAuthServerConfig::default()
10447 });
10448 server.register_client(bounded_test_client("c1")).unwrap();
10449 server.register_client(bounded_test_client("c2")).unwrap();
10450
10451 server.issue_tokens("c1", &[], None).unwrap();
10452 let error = server
10453 .issue_tokens("c1", &[], None)
10454 .expect_err("per-client refresh-token cap must reject the next pair");
10455 assert!(matches!(error, OAuthError::TemporarilyUnavailable(_)));
10456 server.issue_tokens("c2", &[], None).unwrap();
10457
10458 let state = server.state.read().unwrap();
10459 assert_eq!(state.access_tokens.len(), 2);
10460 assert_eq!(state.refresh_tokens.len(), 2);
10461 assert_eq!(state.refresh_token_count_for_client("c1"), 1);
10462 assert_eq!(state.refresh_token_count_for_client("c2"), 1);
10463 }
10464
10465 #[test]
10466 fn code_exchange_capacity_failure_preserves_single_use_code_for_retry() {
10467 let server = configured_approved_test_server(OAuthServerConfig {
10468 max_access_tokens: 1,
10469 max_access_tokens_per_client: 1,
10470 ..OAuthServerConfig::default()
10471 });
10472 server.register_client(bounded_test_client("c1")).unwrap();
10473 let blocker = server.issue_tokens("c1", &[], None).unwrap();
10474 let (code, _) = server
10475 .authorize(&bounded_authorization_request("c1"))
10476 .unwrap();
10477 let request = bounded_code_exchange_request("c1", &code);
10478
10479 let error = server
10480 .token(&request)
10481 .expect_err("full access-token capacity must reject exchange");
10482 assert!(matches!(error, OAuthError::TemporarilyUnavailable(_)));
10483 assert!(
10484 server
10485 .state
10486 .read()
10487 .unwrap()
10488 .authorization_codes
10489 .contains_key(&authorization_code_digest(&code))
10490 );
10491
10492 server.revoke(&blocker.access_token, "c1", None).unwrap();
10493 server.token(&request).unwrap();
10494 assert!(
10495 !server
10496 .state
10497 .read()
10498 .unwrap()
10499 .authorization_codes
10500 .contains_key(&authorization_code_digest(&code))
10501 );
10502 }
10503
10504 #[test]
10505 fn code_exchange_refresh_capacity_failure_preserves_single_use_code_for_retry() {
10506 let server = configured_approved_test_server(OAuthServerConfig {
10507 max_access_tokens: 2,
10508 max_access_tokens_per_client: 2,
10509 max_refresh_tokens: 1,
10510 max_refresh_tokens_per_client: 1,
10511 ..OAuthServerConfig::default()
10512 });
10513 server.register_client(bounded_test_client("c1")).unwrap();
10514 let blocker = server.issue_tokens("c1", &[], None).unwrap();
10515 let blocker_refresh = blocker.refresh_token.expect("refresh token");
10516 let (code, _) = server
10517 .authorize(&bounded_authorization_request("c1"))
10518 .unwrap();
10519 let request = bounded_code_exchange_request("c1", &code);
10520
10521 let error = server
10522 .token(&request)
10523 .expect_err("full refresh-token capacity must reject exchange");
10524 assert!(matches!(error, OAuthError::TemporarilyUnavailable(_)));
10525 assert!(
10526 server
10527 .state
10528 .read()
10529 .unwrap()
10530 .authorization_codes
10531 .contains_key(&authorization_code_digest(&code))
10532 );
10533
10534 server.revoke(&blocker_refresh, "c1", None).unwrap();
10535 server.token(&request).unwrap();
10536 assert!(
10537 !server
10538 .state
10539 .read()
10540 .unwrap()
10541 .authorization_codes
10542 .contains_key(&authorization_code_digest(&code))
10543 );
10544 }
10545
10546 #[test]
10547 fn code_exchange_second_draw_failure_preserves_single_use_code_for_retry() {
10548 let server = OAuthServer::with_defaults();
10549 server.register_client(bounded_test_client("c1")).unwrap();
10550 let (code, _) = server
10551 .authorize(&bounded_authorization_request("c1"))
10552 .unwrap();
10553 let request = bounded_code_exchange_request("c1", &code);
10554 let draw_calls = std::cell::Cell::new(0);
10555
10556 let result = server.token_authorization_code_with_draw(&request, || {
10557 let call = draw_calls.get() + 1;
10558 draw_calls.set(call);
10559 if call == 1 {
10560 draw_security_identifier().map_err(|_| "unexpected operating-system RNG failure")
10561 } else {
10562 Err("forced refresh-token draw failure")
10563 }
10564 });
10565
10566 assert!(matches!(result, Err(OAuthError::ServerError(_))));
10567 assert_eq!(draw_calls.get(), 2);
10568 {
10569 let state = server.state.read().unwrap();
10570 assert!(
10571 state
10572 .authorization_codes
10573 .contains_key(&authorization_code_digest(&code))
10574 );
10575 assert!(state.access_tokens.is_empty());
10576 assert!(state.refresh_tokens.is_empty());
10577 }
10578 server.token(&request).unwrap();
10579 assert!(
10580 !server
10581 .state
10582 .read()
10583 .unwrap()
10584 .authorization_codes
10585 .contains_key(&authorization_code_digest(&code))
10586 );
10587 }
10588
10589 #[test]
10590 fn refresh_capacity_failure_preserves_presented_token_for_retry() {
10591 let server = OAuthServer::new(OAuthServerConfig {
10592 max_access_tokens: 1,
10593 max_access_tokens_per_client: 1,
10594 ..OAuthServerConfig::default()
10595 });
10596 server.register_client(bounded_test_client("c1")).unwrap();
10597 let initial = server.issue_tokens("c1", &[], None).unwrap();
10598 let old_access = initial.access_token;
10599 let old_refresh = initial.refresh_token.expect("refresh token");
10600 let request = bounded_refresh_request("c1", &old_refresh);
10601
10602 let error = server
10603 .token(&request)
10604 .expect_err("full access-token capacity must reject refresh");
10605 assert!(matches!(error, OAuthError::TemporarilyUnavailable(_)));
10606 {
10607 let state = server.state.read().unwrap();
10608 assert!(
10609 state
10610 .refresh_tokens
10611 .contains_key(&refresh_token_digest(&old_refresh))
10612 );
10613 assert!(
10614 !state
10615 .revoked_tokens
10616 .contains_key(&refresh_token_digest(&old_refresh))
10617 );
10618 assert_eq!(state.access_tokens.len(), 1);
10619 assert_eq!(state.refresh_tokens.len(), 1);
10620 }
10621
10622 server.revoke(&old_access, "c1", None).unwrap();
10623 let retried = server.token(&request).unwrap();
10624 let rotated = retried.refresh_token.expect("rotated refresh token");
10625 assert_ne!(rotated, old_refresh);
10626 let state = server.state.read().unwrap();
10627 assert!(
10628 !state
10629 .refresh_tokens
10630 .contains_key(&refresh_token_digest(&old_refresh))
10631 );
10632 assert!(
10633 state
10634 .refresh_tokens
10635 .contains_key(&refresh_token_digest(&rotated))
10636 );
10637 assert!(
10638 state
10639 .revoked_tokens
10640 .contains_key(&refresh_token_digest(&old_refresh))
10641 );
10642 }
10643
10644 #[test]
10645 fn successful_refresh_rotates_single_use_token_and_rejects_replay() {
10646 let server = OAuthServer::new(OAuthServerConfig {
10647 max_refresh_tokens: 1,
10648 max_refresh_tokens_per_client: 1,
10649 ..OAuthServerConfig::default()
10650 });
10651 server.register_client(bounded_test_client("c1")).unwrap();
10652 let initial = server.issue_tokens("c1", &[], Some("subject")).unwrap();
10653 let initial_access = initial.access_token.clone();
10654 let first_refresh = initial.refresh_token.expect("refresh token");
10655 let grant_id = server
10656 .state
10657 .read()
10658 .unwrap()
10659 .refresh_tokens
10660 .get(&refresh_token_digest(&first_refresh))
10661 .expect("initial refresh metadata")
10662 .grant_id;
10663
10664 let first_response = server
10665 .token(&bounded_refresh_request("c1", &first_refresh))
10666 .unwrap();
10667 let second_access = first_response.access_token.clone();
10668 let second_refresh = first_response
10669 .refresh_token
10670 .expect("successful refresh must rotate");
10671 assert_ne!(second_refresh, first_refresh);
10672 {
10673 let state = server.state.read().unwrap();
10674 assert_eq!(state.access_tokens.len(), 2);
10675 assert_eq!(state.refresh_tokens.len(), 1);
10676 assert!(
10677 !state
10678 .refresh_tokens
10679 .contains_key(&refresh_token_digest(&first_refresh))
10680 );
10681 assert!(
10682 state
10683 .refresh_tokens
10684 .contains_key(&refresh_token_digest(&second_refresh))
10685 );
10686 assert_eq!(
10687 state
10688 .refresh_tokens
10689 .get(&refresh_token_digest(&second_refresh))
10690 .expect("rotated refresh metadata")
10691 .grant_id,
10692 grant_id
10693 );
10694 assert!(
10695 state
10696 .revoked_tokens
10697 .contains_key(&refresh_token_digest(&first_refresh))
10698 );
10699 }
10700
10701 let replay = server
10702 .token(&bounded_refresh_request("c1", &first_refresh))
10703 .expect_err("consumed refresh token must never be reusable");
10704 assert!(matches!(&replay, OAuthError::InvalidGrant(_)));
10705 assert_eq!(replay.description(), OAUTH_INVALID_GRANT_ERROR);
10706 assert!(server.validate_access_token(&initial_access).is_none());
10707 assert!(server.validate_access_token(&second_access).is_none());
10708 assert!(matches!(
10709 server.token(&bounded_refresh_request("c1", &second_refresh)),
10710 Err(OAuthError::InvalidGrant(_))
10711 ));
10712 let state = server.state.read().unwrap();
10713 assert!(
10714 state
10715 .access_tokens
10716 .values()
10717 .all(|token| token.grant_id != grant_id)
10718 );
10719 assert!(
10720 state
10721 .refresh_tokens
10722 .values()
10723 .all(|token| token.grant_id != grant_id)
10724 );
10725 }
10726
10727 #[test]
10728 fn concurrent_refresh_replay_allows_one_rotation_then_revokes_its_family() {
10729 let server = Arc::new(OAuthServer::with_defaults());
10730 server.register_client(bounded_test_client("c1")).unwrap();
10731 let initial = server.issue_tokens("c1", &[], None).unwrap();
10732 let initial_access = initial.access_token;
10733 let refresh = initial.refresh_token.expect("refresh token");
10734 let grant_id = server
10735 .state
10736 .read()
10737 .unwrap()
10738 .refresh_tokens
10739 .get(&refresh_token_digest(&refresh))
10740 .expect("initial refresh metadata")
10741 .grant_id;
10742 let barrier = Arc::new(std::sync::Barrier::new(3));
10743
10744 let workers: Vec<_> = (0..2)
10745 .map(|_| {
10746 let server = Arc::clone(&server);
10747 let barrier = Arc::clone(&barrier);
10748 let refresh = refresh.clone();
10749 std::thread::spawn(move || {
10750 barrier.wait();
10751 server.token(&bounded_refresh_request("c1", &refresh))
10752 })
10753 })
10754 .collect();
10755 barrier.wait();
10756 let results: Vec<_> = workers
10757 .into_iter()
10758 .map(|worker| worker.join().expect("refresh worker must not panic"))
10759 .collect();
10760
10761 assert_eq!(results.iter().filter(|result| result.is_ok()).count(), 1);
10762 assert_eq!(
10763 results
10764 .iter()
10765 .filter(|result| matches!(result, Err(OAuthError::InvalidGrant(_))))
10766 .count(),
10767 1
10768 );
10769 let rotated_access = results
10770 .into_iter()
10771 .find_map(Result::ok)
10772 .expect("one refresh rotation succeeds")
10773 .access_token;
10774 assert!(server.validate_access_token(&initial_access).is_none());
10775 assert!(server.validate_access_token(&rotated_access).is_none());
10776 let state = server.state.read().unwrap();
10777 assert!(
10778 state
10779 .access_tokens
10780 .values()
10781 .all(|token| token.grant_id != grant_id)
10782 );
10783 assert!(
10784 state
10785 .refresh_tokens
10786 .values()
10787 .all(|token| token.grant_id != grant_id)
10788 );
10789 }
10790
10791 #[test]
10792 fn refresh_rotation_fails_without_evicting_a_live_replay_guard() {
10793 let server = OAuthServer::new(OAuthServerConfig {
10794 max_revocation_tombstones: 1,
10795 max_revocation_tombstones_per_client: 1,
10796 ..OAuthServerConfig::default()
10797 });
10798 server.register_client(bounded_test_client("c1")).unwrap();
10799 let initial = server.issue_tokens("c1", &[], None).unwrap();
10800 let first_refresh = initial.refresh_token.expect("initial refresh token");
10801 let rotated = server
10802 .token(&bounded_refresh_request("c1", &first_refresh))
10803 .unwrap();
10804 let second_refresh = rotated.refresh_token.expect("rotated refresh token");
10805
10806 let error = server
10807 .token(&bounded_refresh_request("c1", &second_refresh))
10808 .expect_err("rotation must fail closed when its replay guard cannot be retained");
10809 assert!(matches!(error, OAuthError::TemporarilyUnavailable(_)));
10810 let state = server.state.read().unwrap();
10811 assert!(
10812 state
10813 .refresh_tokens
10814 .contains_key(&refresh_token_digest(&second_refresh))
10815 );
10816 assert!(
10817 state
10818 .revoked_tokens
10819 .get(&refresh_token_digest(&first_refresh))
10820 .is_some_and(|tombstone| tombstone.replay_guard)
10821 );
10822 }
10823
10824 #[test]
10825 fn refresh_rotation_rejects_a_family_with_less_than_one_wire_second_left() {
10826 let server = OAuthServer::with_defaults();
10827 server.register_client(bounded_test_client("c1")).unwrap();
10828 let initial = server.issue_tokens("c1", &[], None).unwrap();
10829 let refresh = initial.refresh_token.expect("refresh token");
10830 let refresh_digest = refresh_token_digest(&refresh);
10831 let near_deadline = Instant::now()
10832 .checked_add(Duration::from_millis(999))
10833 .expect("near family deadline");
10834 {
10835 let mut state = server.state.write().unwrap();
10836 let stored = state
10837 .refresh_tokens
10838 .get_mut(&refresh_digest)
10839 .expect("stored refresh token");
10840 stored.metadata.expires_at = near_deadline;
10841 stored.family_expires_at = near_deadline;
10842 }
10843
10844 let error = server
10845 .token(&bounded_refresh_request("c1", &refresh))
10846 .expect_err("a zero-second wire credential must not be issued");
10847
10848 assert!(matches!(error, OAuthError::InvalidGrant(_)));
10849 let state = server.state.read().unwrap();
10850 assert!(!state.revoked_tokens.contains_key(&refresh_digest));
10851 }
10852
10853 #[test]
10854 fn unregister_purges_old_epoch_guards_before_same_id_reregistration() {
10855 let server = OAuthServer::new(OAuthServerConfig {
10856 max_revocation_tombstones: 1,
10857 max_revocation_tombstones_per_client: 1,
10858 ..OAuthServerConfig::default()
10859 });
10860 server.register_client(bounded_test_client("c1")).unwrap();
10861 let old = server.issue_tokens("c1", &[], None).unwrap();
10862 let old_refresh = old.refresh_token.expect("old refresh token");
10863 let rotated = server
10864 .token(&bounded_refresh_request("c1", &old_refresh))
10865 .expect("first registration rotates once");
10866 assert_eq!(server.stats().revoked_tokens, 1);
10867
10868 server.unregister_client("c1").unwrap();
10869 {
10870 let state = server.state.read().unwrap();
10871 assert!(state.authorization_codes.is_empty());
10872 assert!(state.access_tokens.is_empty());
10873 assert!(state.refresh_tokens.is_empty());
10874 assert!(state.revoked_tokens.is_empty());
10875 }
10876 server.register_client(bounded_test_client("c1")).unwrap();
10877 assert!(matches!(
10878 server.token(&bounded_refresh_request("c1", &old_refresh)),
10879 Err(OAuthError::InvalidGrant(_))
10880 ));
10881 let fresh = server.issue_tokens("c1", &[], None).unwrap();
10882 let fresh_refresh = fresh.refresh_token.expect("fresh refresh token");
10883 server
10884 .token(&bounded_refresh_request("c1", &fresh_refresh))
10885 .expect("fresh registration receives a fresh replay-guard budget");
10886
10887 let rotated_refresh = rotated.refresh_token.expect("rotated old refresh token");
10889 assert!(matches!(
10890 server.token(&bounded_refresh_request("c1", &rotated_refresh)),
10891 Err(OAuthError::InvalidGrant(_))
10892 ));
10893 }
10894
10895 #[test]
10896 fn second_refresh_draw_failure_preserves_presented_token() {
10897 let server = OAuthServer::with_defaults();
10898 server.register_client(bounded_test_client("c1")).unwrap();
10899 let initial = server.issue_tokens("c1", &[], None).unwrap();
10900 let old_refresh = initial.refresh_token.expect("refresh token");
10901 let request = bounded_refresh_request("c1", &old_refresh);
10902 let draw_calls = std::cell::Cell::new(0);
10903
10904 let result = server.token_refresh_token_with_draw(&request, || {
10905 let call = draw_calls.get() + 1;
10906 draw_calls.set(call);
10907 if call == 1 {
10908 draw_security_identifier().map_err(|_| "unexpected operating-system RNG failure")
10909 } else {
10910 Err("forced replacement refresh-token draw failure")
10911 }
10912 });
10913
10914 assert!(matches!(result, Err(OAuthError::ServerError(_))));
10915 assert_eq!(draw_calls.get(), 2);
10916 let state = server.state.read().unwrap();
10917 assert_eq!(state.access_tokens.len(), 1);
10918 assert_eq!(state.refresh_tokens.len(), 1);
10919 assert!(
10920 state
10921 .refresh_tokens
10922 .contains_key(&refresh_token_digest(&old_refresh))
10923 );
10924 assert!(
10925 !state
10926 .revoked_tokens
10927 .contains_key(&refresh_token_digest(&old_refresh))
10928 );
10929 }
10930
10931 #[test]
10932 fn revocation_tombstones_carry_expiry_and_obey_both_exact_caps() {
10933 let server = OAuthServer::new(OAuthServerConfig {
10934 max_revocation_tombstones: 2,
10935 max_revocation_tombstones_per_client: 1,
10936 ..OAuthServerConfig::default()
10937 });
10938 for client_id in ["c1", "c2", "c3"] {
10939 server
10940 .register_client(bounded_test_client(client_id))
10941 .unwrap();
10942 }
10943 let first = server.issue_tokens("c1", &[], None).unwrap();
10944 let second = server.issue_tokens("c2", &[], None).unwrap();
10945 let third = server.issue_tokens("c3", &[], None).unwrap();
10946 let first_refresh = first.refresh_token.expect("refresh token");
10947 let first_refresh_expiry = server
10948 .state
10949 .read()
10950 .unwrap()
10951 .refresh_tokens
10952 .get(&refresh_token_digest(&first_refresh))
10953 .expect("stored refresh token")
10954 .expires_at;
10955
10956 server.revoke(&first.access_token, "c1", None).unwrap();
10957 server.revoke(&first_refresh, "c1", None).unwrap();
10958 {
10959 let state = server.state.read().unwrap();
10960 assert_eq!(state.revoked_tokens.len(), 1);
10961 assert_eq!(state.revocation_tombstone_count_for_client("c1"), 1);
10962 assert_eq!(
10963 state
10964 .revoked_tokens
10965 .get(&refresh_token_digest(&first_refresh))
10966 .expect("longer-lived c1 tombstone retained")
10967 .expires_at,
10968 first_refresh_expiry
10969 );
10970 }
10971
10972 server.revoke(&second.access_token, "c2", None).unwrap();
10973 assert_eq!(server.stats().revoked_tokens, 2);
10974 server.revoke(&third.access_token, "c3", None).unwrap();
10975
10976 let state = server.state.read().unwrap();
10977 assert_eq!(state.revoked_tokens.len(), 2);
10978 for client_id in ["c1", "c2", "c3"] {
10979 assert!(state.revocation_tombstone_count_for_client(client_id) <= 1);
10980 }
10981 assert!(
10982 !state
10983 .access_tokens
10984 .contains_key(&access_token_digest(&first.access_token))
10985 );
10986 assert!(
10987 !state
10988 .access_tokens
10989 .contains_key(&access_token_digest(&second.access_token))
10990 );
10991 assert!(
10992 !state
10993 .access_tokens
10994 .contains_key(&access_token_digest(&third.access_token))
10995 );
10996 assert!(
10997 !state
10998 .refresh_tokens
10999 .contains_key(&refresh_token_digest(&first_refresh))
11000 );
11001 drop(state);
11002
11003 for access in [
11004 &first.access_token,
11005 &second.access_token,
11006 &third.access_token,
11007 ] {
11008 assert!(server.validate_access_token(access).is_none());
11009 }
11010 assert!(matches!(
11011 server.token(&bounded_refresh_request("c1", &first_refresh)),
11012 Err(OAuthError::InvalidGrant(_))
11013 ));
11014 }
11015
11016 #[test]
11017 fn every_mutation_opportunistically_cleans_all_expiry_bearing_state() {
11018 let server = OAuthServer::with_defaults();
11019 server.register_client(bounded_test_client("c1")).unwrap();
11020 let expired_at = Instant::now();
11021 let issued_at = expired_at;
11022 let expired_code = base64url_encode(&[1_u8; 32]);
11023 let expired_access = base64url_encode(&[2_u8; 32]);
11024 let expired_refresh = base64url_encode(&[3_u8; 32]);
11025 let expired_revocation = base64url_encode(&[4_u8; 32]);
11026
11027 {
11028 let mut state = server.state.write().unwrap();
11029 state.authorization_codes.insert(
11030 authorization_code_digest(&expired_code),
11031 AuthorizationCode {
11032 client_id: "c1".to_string(),
11033 redirect_uri: "http://127.0.0.1/callback".to_string(),
11034 scopes: Vec::new(),
11035 resource: None,
11036 code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM".to_string(),
11037 code_challenge_method: CodeChallengeMethod::S256,
11038 issued_at,
11039 expires_at: expired_at,
11040 subject: None,
11041 state: None,
11042 registration_epoch: test_registration_epoch(1),
11043 },
11044 );
11045 state.access_tokens.insert(
11046 access_token_digest(&expired_access),
11047 StoredOAuthToken {
11048 metadata: OAuthToken {
11049 token: String::new(),
11050 token_type: TokenType::Bearer,
11051 client_id: "c1".to_string(),
11052 scopes: Vec::new(),
11053 resource: None,
11054 issued_at,
11055 expires_at: expired_at,
11056 subject: None,
11057 is_refresh_token: false,
11058 },
11059 grant_id: test_grant_id(6),
11060 registration_epoch: test_registration_epoch(1),
11061 family_expires_at: expired_at,
11062 },
11063 );
11064 state.refresh_tokens.insert(
11065 refresh_token_digest(&expired_refresh),
11066 StoredOAuthToken {
11067 metadata: OAuthToken {
11068 token: String::new(),
11069 token_type: TokenType::Bearer,
11070 client_id: "c1".to_string(),
11071 scopes: Vec::new(),
11072 resource: None,
11073 issued_at,
11074 expires_at: expired_at,
11075 subject: None,
11076 is_refresh_token: true,
11077 },
11078 grant_id: test_grant_id(6),
11079 registration_epoch: test_registration_epoch(1),
11080 family_expires_at: expired_at,
11081 },
11082 );
11083 state.revoked_tokens.insert(
11084 refresh_token_digest(&expired_revocation),
11085 RevocationTombstone {
11086 client_id: "c1".to_string(),
11087 grant_id: test_grant_id(6),
11088 replay_guard: false,
11089 expires_at: expired_at,
11090 },
11091 );
11092 }
11093
11094 server.register_client(bounded_test_client("c2")).unwrap();
11097 let state = server.state.read().unwrap();
11098 assert!(state.authorization_codes.is_empty());
11099 assert!(state.access_tokens.is_empty());
11100 assert!(state.refresh_tokens.is_empty());
11101 assert!(state.revoked_tokens.is_empty());
11102 }
11103
11104 #[test]
11105 fn loopback_match_no_explicit_port() {
11106 assert!(loopback_match(
11107 "http://127.0.0.1/callback",
11108 "http://127.0.0.1:8080/callback"
11109 ));
11110 assert!(loopback_match(
11111 "http://127.0.0.1/callback",
11112 "http://127.0.0.1/callback"
11113 ));
11114 }
11115
11116 #[test]
11117 fn oauth_crypto_ownership_and_fallbacks_are_denied() {
11118 let source = include_str!("oauth.rs");
11119 let production = source
11120 .split_once("\n#[cfg(test)]")
11121 .map_or(source, |(production, _)| production);
11122
11123 assert!(!production.contains("getrandom::"));
11124 assert!(!production.contains("sha2::"));
11125 assert!(!production.contains("hmac::"));
11126 assert!(!production.contains("generate_token(bytes"));
11127 assert!(production.contains("draw_security_identifier"));
11128 assert!(production.contains("sha256_bounded"));
11129
11130 let token_helper_start = production
11131 .find("fn generate_token()")
11132 .expect("token helper marker");
11133 let token_helper_end = production[token_helper_start..]
11134 .find("/// Base64url encodes bytes")
11135 .map(|offset| token_helper_start + offset)
11136 .expect("token helper end marker");
11137 let token_helper = &production[token_helper_start..token_helper_end];
11138 for fallback in [
11139 "SystemTime",
11140 "Instant",
11141 "process::",
11142 "thread::",
11143 "Atomic",
11144 "rand::",
11145 "usize::MAX",
11146 ] {
11147 assert!(
11148 !token_helper.contains(fallback),
11149 "security-token fallback found: {fallback}"
11150 );
11151 }
11152 }
11153}