Skip to main content

fastmcp_server/
oauth.rs

1//! OAuth 2.0/2.1 authorization-server implementation code for MCP.
2//!
3//! This module contains authorization-server building blocks for MCP servers:
4//!
5//! - **Authorization Code Flow** with PKCE (required for OAuth 2.1)
6//! - **Token Issuance** - Access tokens and refresh tokens
7//! - **Token Revocation** - RFC 7009 token revocation
8//! - **Client Registration** - Dynamic client registration
9//! - **Scope Validation** - Fine-grained scope control
10//! - **Redirect URI Validation** - Security-critical validation
11//!
12//! # Architecture
13//!
14//! The OAuth server is designed to be modular:
15//!
16//! - [`OAuthServer`]: Main authorization server component
17//! - [`OAuthClient`]: Registered OAuth client
18//! - [`OAuthClientMetadata`]: Secret-free registered-client metadata
19//! - [`AuthorizationCode`]: Temporary code for token exchange
20//! - [`OAuthToken`]: Access and refresh tokens
21//! - [`OAuthTokenVerifier`]: Implements [`TokenVerifier`] for MCP integration
22//!
23//! # Security posture
24//!
25//! These are implementation policies. AUTH promotion and MCP 2026-07-28
26//! conformance remain unverified:
27//!
28//! - S256 PKCE is required by the implemented authorization-code path
29//! - Redirect URIs reject userinfo/fragments and otherwise require an exact
30//!   match, except that loopback ports may vary
31//! - Token material is drawn through the core security-identifier API
32//! - Authorization codes are single-use and expire quickly
33//! - Refresh tokens rotate on successful use; retained replay markers revoke
34//!   the complete live grant family before replay is rejected
35//! - Configurable retained-state counts have per-field and aggregate hard
36//!   ceilings. These bound entry counts, not exact heap bytes, and do not
37//!   qualify this implementation for production OAuth use
38//!
39//! # Example
40//!
41//! ```ignore
42//! use std::sync::Arc;
43//! use fastmcp_rust::oauth::{OAuthClient, OAuthServer, OAuthServerConfig};
44//! use fastmcp_rust::{Server, TokenAuthProvider};
45//!
46//! let oauth = Arc::new(OAuthServer::new(OAuthServerConfig::default()));
47//!
48//! // Register a client
49//! let client = OAuthClient::builder("my-client")
50//!     .redirect_uri("http://127.0.0.1:3000/callback")
51//!     .scope("read")
52//!     .scope("write")
53//!     .build()?;
54//!
55//! oauth.register_client(client)?;
56//!
57//! // Use with MCP server
58//! let verifier = oauth.token_verifier();
59//! Server::new("my-server", "1.0.0")
60//!     .auth_provider(TokenAuthProvider::new(verifier))
61//!     .build()
62//!     .run_stdio();
63//! ```
64
65use 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
101/// Maximum UTF-8 byte length of the configured OAuth issuer URL.
102pub const MAX_OAUTH_ISSUER_BYTES: usize = 2_048;
103
104/// Maximum UTF-8 byte length of a retained OAuth client identifier.
105pub const MAX_OAUTH_CLIENT_ID_BYTES: usize = 256;
106/// Maximum UTF-8 byte length of a retained OAuth client credential.
107pub const MAX_OAUTH_CLIENT_CREDENTIAL_BYTES: usize = 1_024;
108/// Maximum number of redirect URIs retained for one OAuth client.
109pub const MAX_OAUTH_REDIRECT_URIS_PER_CLIENT: usize = 16;
110/// Maximum UTF-8 byte length of one retained OAuth redirect URI.
111pub const MAX_OAUTH_REDIRECT_URI_BYTES: usize = 2_048;
112/// Maximum number of scopes retained for one OAuth client.
113pub const MAX_OAUTH_SCOPES_PER_CLIENT: usize = 64;
114/// Maximum UTF-8 byte length of one retained OAuth scope.
115pub const MAX_OAUTH_SCOPE_BYTES: usize = 256;
116/// Maximum UTF-8 byte length of a retained OAuth client display name.
117pub const MAX_OAUTH_CLIENT_NAME_BYTES: usize = 256;
118/// Maximum UTF-8 byte length of a retained OAuth client description.
119pub const MAX_OAUTH_CLIENT_DESCRIPTION_BYTES: usize = 4_096;
120/// Maximum UTF-8 byte length of an authorization grant subject.
121pub const MAX_OAUTH_SUBJECT_BYTES: usize = 1_024;
122/// Maximum UTF-8 byte length of an RFC 8707 authorization resource indicator.
123pub 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;
133/// Maximum UTF-8 byte length of an OAuth authorization `state` value.
134pub const MAX_OAUTH_STATE_BYTES: usize = 4_096;
135
136/// Maximum encoded bytes admitted by one authorization query.
137///
138/// This bound applies before percent decoding, so an attacker cannot make a
139/// small wire request allocate an unbounded decoded value.
140pub const MAX_OAUTH_AUTHORIZATION_QUERY_BYTES: usize = 16 * 1_024;
141/// Maximum encoded bytes admitted by one token-like form body.
142pub const MAX_OAUTH_FORM_BODY_BYTES: usize = 16 * 1_024;
143/// Maximum key/value pairs admitted by one OAuth endpoint request.
144pub const MAX_OAUTH_PARAMETER_PAIRS: usize = 64;
145/// Maximum decoded bytes in one OAuth parameter name.
146pub const MAX_OAUTH_PARAMETER_NAME_BYTES: usize = 256;
147/// Maximum decoded bytes in one OAuth parameter value.
148pub 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// =============================================================================
182// Raw OAuth parameter admission
183// =============================================================================
184
185/// The exact OAuth endpoint grammar used to admit an untrusted parameter
186/// sequence.
187///
188/// Authorization parameters originate in a URI query; all other profiles
189/// originate in an `application/x-www-form-urlencoded` request body. The
190/// profile is selected by the HTTP adapter's route, not by a peer-controlled
191/// content type or parameter.
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193pub enum OAuthParameterEndpoint {
194    /// Authorization endpoint query parameters.
195    AuthorizationQuery,
196    /// Token endpoint form parameters.
197    TokenForm,
198    /// Token-revocation endpoint form parameters.
199    RevocationForm,
200    /// Token-introspection endpoint form parameters.
201    IntrospectionForm,
202}
203
204impl OAuthParameterEndpoint {
205    /// Returns the only permitted wire source for this endpoint profile.
206    #[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/// The wire source that supplied an admitted parameter.
262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263pub enum OAuthParameterSource {
264    /// URI query text on the authorization endpoint.
265    Query,
266    /// `application/x-www-form-urlencoded` request body.
267    Form,
268}
269
270/// A parameter name whose value may influence the matching endpoint.
271///
272/// Names outside the selected endpoint profile remain admitted as bounded,
273/// ordered unknown parameters and never appear through
274/// [`OAuthParameterAdmission::take_defined_value`].
275#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
276pub enum OAuthParameterName {
277    /// `response_type`
278    ResponseType,
279    /// `client_id`
280    ClientId,
281    /// `redirect_uri`
282    RedirectUri,
283    /// RFC 8707 `resource`
284    Resource,
285    /// `scope`
286    Scope,
287    /// `state`
288    State,
289    /// `code_challenge`
290    CodeChallenge,
291    /// `code_challenge_method`
292    CodeChallengeMethod,
293    /// `grant_type`
294    GrantType,
295    /// `code`
296    Code,
297    /// `client_secret`
298    ClientSecret,
299    /// `code_verifier`
300    CodeVerifier,
301    /// `refresh_token`
302    RefreshToken,
303    /// `token`
304    Token,
305    /// `token_type_hint`
306    TokenTypeHint,
307}
308
309/// Immutable public native-HTTP routes for an [`OAuthServer`].
310///
311/// OAuth authorization, token, and revocation are always available. An OIDC
312/// provider can add its fixed discovery and JWKS routes only after it has
313/// bound an exact advertised public JWKS URI to an external signer.
314#[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/// Native public OIDC metadata routes bound to one OAuth server and issuer.
326#[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/// A public OAuth HTTP route configuration was unsafe or ambiguous.
377#[derive(Debug, Clone, PartialEq, Eq)]
378pub enum OAuthHttpRouteConfigurationError {
379    /// The configured endpoint base was not a canonical HTTPS URL.
380    InvalidPublicEndpointBase,
381    /// The endpoint base did not share the configured OAuth issuer origin.
382    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    /// Creates the fixed authorization, token, and revocation routes below an
402    /// explicit public HTTPS endpoint base.
403    ///
404    /// For example, `https://auth.example.test/oauth` exposes
405    /// `/oauth/authorize`, `/oauth/token`, and `/oauth/revoke`. The base is
406    /// never inferred from a request Host or forwarded header.
407    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    /// Adds fixed OIDC discovery and JWKS routes for a provider that has
450    /// already entered signer activation. The provider must be layered over
451    /// this exact OAuth server; neither issuer nor endpoint paths are inferred
452    /// from requests.
453    #[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    /// Returns the configured public endpoint base.
504    #[must_use]
505    pub fn public_endpoint_base(&self) -> &str {
506        &self.public_endpoint_base
507    }
508
509    /// Returns the exact authorization endpoint path.
510    #[must_use]
511    pub fn authorization_path(&self) -> &str {
512        &self.authorization_path
513    }
514
515    /// Returns the exact token endpoint path.
516    #[must_use]
517    pub fn token_path(&self) -> &str {
518        &self.token_path
519    }
520
521    /// Returns the exact revocation endpoint path.
522    #[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/// One decoded parameter retained in exact wire order.
585#[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    /// Returns the query or form source that supplied this value.
596    #[must_use]
597    pub const fn source(&self) -> OAuthParameterSource {
598        self.source
599    }
600
601    /// Returns this parameter's zero-based wire order.
602    #[must_use]
603    pub const fn ordinal(&self) -> usize {
604        self.ordinal
605    }
606
607    /// Returns the decoded parameter name.
608    #[must_use]
609    pub fn name(&self) -> &str {
610        &self.name
611    }
612
613    /// Returns the decoded parameter value's byte length.
614    ///
615    /// The value itself is deliberately unavailable through the public
616    /// diagnostics view. In particular, this prevents codes, refresh tokens,
617    /// client secrets, and repeated unknown values from being copied into a
618    /// log or inspection surface.
619    #[must_use]
620    pub const fn value_len(&self) -> usize {
621        self.value_len
622    }
623
624    /// Returns whether the selected endpoint profile defines this name.
625    #[must_use]
626    pub const fn is_defined(&self) -> bool {
627        self.defined
628    }
629}
630
631/// A duplicate-aware, bounded admission result for one OAuth parameter source.
632///
633/// Empty defined values are retained in [`Self::parameters`] for diagnostics,
634/// but are intentionally omitted from the sensitive taking surface. This gives the
635/// endpoint parser one missing-value representation without allowing an empty
636/// duplicate to evade defined-name duplicate rejection.
637pub struct OAuthParameterAdmission {
638    endpoint: OAuthParameterEndpoint,
639    source: OAuthParameterSource,
640    parameters: Vec<OAuthAdmittedParameter>,
641    defined: HashMap<OAuthParameterName, OAuthSensitiveParameterValue>,
642}
643
644/// One defined OAuth value retained only for the next crate-local endpoint
645/// parser.
646///
647/// This is intentionally neither `Clone` nor `Debug`. Values are moved out of
648/// [`OAuthParameterAdmission`] with [`OAuthParameterAdmission::take_defined_value`]
649/// and are zeroized if the endpoint declines to consume them.
650pub(crate) struct OAuthSensitiveParameterValue {
651    value: Zeroizing<String>,
652}
653
654impl OAuthSensitiveParameterValue {
655    /// Moves the value into the endpoint's own typed request boundary.
656    #[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/// A rejection raised before endpoint authentication, grant handling, or
675/// another stateful OAuth operation.
676#[derive(Debug, Clone, PartialEq, Eq)]
677pub enum OAuthParameterAdmissionError {
678    /// The encoded query or form body exceeds its endpoint bound.
679    InputTooLarge,
680    /// The request carries more pairs than the shared endpoint bound permits.
681    TooManyPairs,
682    /// A decoded parameter name is empty.
683    EmptyName,
684    /// A decoded parameter name exceeds its bound.
685    NameTooLarge,
686    /// A decoded parameter value exceeds its bound.
687    ValueTooLarge,
688    /// Percent decoding was incomplete or contained a non-hex digit.
689    MalformedPercentEncoding,
690    /// A percent-decoded component was not valid UTF-8.
691    InvalidUtf8,
692    /// A decoded name or value contains a control character.
693    ControlCharacter,
694    /// A profile-defined name occurs more than once, including empty values.
695    DuplicateDefinedParameter {
696        /// The endpoint-defined name that was repeated.
697        parameter: OAuthParameterName,
698        /// The query or form source containing both occurrences.
699        source: OAuthParameterSource,
700        /// Wire order of the first occurrence.
701        first_ordinal: usize,
702        /// Wire order of the rejected duplicate occurrence.
703        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    /// Strictly admits one raw authorization query or token-like form body.
730    ///
731    /// This function is deliberately pure: it performs no redirect handling,
732    /// authentication, token lookup, grant consumption, or other OAuth state
733    /// mutation. This lower layer is not a production HTTP gate: a later
734    /// adapter must select `endpoint` from its already-routed HTTP endpoint,
735    /// enforce content type and transport policy, and perform endpoint auth,
736    /// rate admission, and all stateful OAuth work after this parser returns.
737    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            // HTML form serialization permits empty segments, such as a
761            // leading/trailing ampersand or `&&`. They carry no parameter and
762            // must not consume the bounded admitted-pair budget.
763            if pair.is_empty() {
764                continue;
765            }
766            if ordinal >= MAX_OAUTH_PARAMETER_PAIRS {
767                return Err(OAuthParameterAdmissionError::TooManyPairs);
768            }
769            // In application/x-www-form-urlencoded, both `name` and `name=`
770            // mean an empty value. Required-field validation occurs only
771            // after this layer turns either spelling into an omitted typed
772            // defined value.
773            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                // An empty defined field is intentionally equivalent to an
814                // omitted field for endpoint parsing, while still counting as
815                // present for duplicate-pollution rejection.
816                if !value.is_empty() {
817                    defined.insert(defined_name, OAuthSensitiveParameterValue { value });
818                }
819            }
820            parameters.push(OAuthAdmittedParameter {
821                source,
822                ordinal,
823                // Names are the intentionally public diagnostic surface; all
824                // decoded staging buffers remain owned by `Zeroizing`.
825                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    /// Returns the selected endpoint profile.
841    #[must_use]
842    pub const fn endpoint(&self) -> OAuthParameterEndpoint {
843        self.endpoint
844    }
845
846    /// Returns the sole wire source permitted by the selected profile.
847    #[must_use]
848    pub const fn source(&self) -> OAuthParameterSource {
849        self.source
850    }
851
852    /// Returns every admitted parameter in exact decoded wire order.
853    #[must_use]
854    pub fn parameters(&self) -> &[OAuthAdmittedParameter] {
855        &self.parameters
856    }
857
858    /// Removes one nonempty endpoint-defined value for crate-local typed
859    /// endpoint parsing.
860    ///
861    /// An empty defined value is omitted, and unknown values are dropped after
862    /// validation instead of being retained. Taking is one-way: no public API
863    /// can inspect, clone, or serialize these values.
864    pub(crate) fn take_defined_value(
865        &mut self,
866        name: OAuthParameterName,
867    ) -> Option<OAuthSensitiveParameterValue> {
868        self.defined.remove(&name)
869    }
870
871    /// Iterates bounded unknown parameters in their original wire order.
872    ///
873    /// Their decoded values are zeroized immediately after admission and are
874    /// not retained by this result.
875    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    // Keep the wire-decoded scratch bytes in zeroizing storage on every
903    // return path. In particular, malformed percent escapes, invalid UTF-8,
904    // and output-limit rejection must not leave a temporary credential copy
905    // in an ordinary dropped allocation.
906    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    // Do not move the scratch allocation into `String`: copying the validated
935    // text leaves `decoded` owned by `Zeroizing`, which wipes it on both the
936    // success and error paths. Defined values are immediately wrapped in their
937    // own one-way zeroizing holder by the caller; unknown values are dropped.
938    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
952// These ceilings make accidentally persistent bearer credentials and
953// authorization codes fail closed at configuration admission. Deployments
954// needing longer-lived sessions should rotate refresh tokens instead of
955// extending access-token or authorization-code exposure.
956const 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
961/// Default maximum number of registered OAuth clients.
962pub const DEFAULT_MAX_OAUTH_CLIENTS: usize = 1_024;
963/// Default maximum number of pending authorization codes.
964pub const DEFAULT_MAX_AUTHORIZATION_CODES: usize = 16 * 1_024;
965/// Default maximum pending authorization codes for one client.
966pub const DEFAULT_MAX_AUTHORIZATION_CODES_PER_CLIENT: usize = 64;
967/// Default maximum number of active access tokens.
968pub const DEFAULT_MAX_ACCESS_TOKENS: usize = 64 * 1_024;
969/// Default maximum active access tokens for one client.
970pub const DEFAULT_MAX_ACCESS_TOKENS_PER_CLIENT: usize = 256;
971/// Default maximum number of active refresh tokens.
972pub const DEFAULT_MAX_REFRESH_TOKENS: usize = 16 * 1_024;
973/// Default maximum active refresh tokens for one client.
974pub const DEFAULT_MAX_REFRESH_TOKENS_PER_CLIENT: usize = 64;
975/// Default maximum number of retained revocation tombstones.
976pub const DEFAULT_MAX_REVOCATION_TOMBSTONES: usize = 64 * 1_024;
977/// Default maximum retained revocation tombstones for one client.
978pub const DEFAULT_MAX_REVOCATION_TOMBSTONES_PER_CLIENT: usize = 256;
979
980/// Hard configuration ceiling for registered OAuth clients.
981pub const HARD_MAX_OAUTH_CLIENTS: usize = 4 * DEFAULT_MAX_OAUTH_CLIENTS;
982/// Hard configuration ceiling for pending authorization codes.
983pub const HARD_MAX_AUTHORIZATION_CODES: usize = 2 * DEFAULT_MAX_AUTHORIZATION_CODES;
984/// Hard per-client configuration ceiling for pending authorization codes.
985pub const HARD_MAX_AUTHORIZATION_CODES_PER_CLIENT: usize =
986    4 * DEFAULT_MAX_AUTHORIZATION_CODES_PER_CLIENT;
987/// Hard configuration ceiling for active access tokens.
988pub const HARD_MAX_ACCESS_TOKENS: usize = 2 * DEFAULT_MAX_ACCESS_TOKENS;
989/// Hard per-client configuration ceiling for active access tokens.
990pub const HARD_MAX_ACCESS_TOKENS_PER_CLIENT: usize = 4 * DEFAULT_MAX_ACCESS_TOKENS_PER_CLIENT;
991/// Hard configuration ceiling for active refresh tokens.
992pub const HARD_MAX_REFRESH_TOKENS: usize = 2 * DEFAULT_MAX_REFRESH_TOKENS;
993/// Hard per-client configuration ceiling for active refresh tokens.
994pub const HARD_MAX_REFRESH_TOKENS_PER_CLIENT: usize = 4 * DEFAULT_MAX_REFRESH_TOKENS_PER_CLIENT;
995/// Hard configuration ceiling for retained revocation tombstones.
996pub const HARD_MAX_REVOCATION_TOMBSTONES: usize = 2 * DEFAULT_MAX_REVOCATION_TOMBSTONES;
997/// Hard per-client configuration ceiling for retained revocation tombstones.
998pub const HARD_MAX_REVOCATION_TOMBSTONES_PER_CLIENT: usize =
999    4 * DEFAULT_MAX_REVOCATION_TOMBSTONES_PER_CLIENT;
1000/// Hard aggregate ceiling across every globally retained OAuth state map.
1001///
1002/// This covers registered clients, pending authorization codes, live access
1003/// and refresh tokens, and revocation tombstones. Per-client limits partition
1004/// those same global entries and therefore are not added a second time.
1005pub const HARD_MAX_OAUTH_RETAINED_ENTRIES: usize = 256 * 1_024;
1006
1007// =============================================================================
1008// Configuration
1009// =============================================================================
1010
1011/// Configuration for the OAuth authorization server.
1012#[derive(Debug, Clone)]
1013pub struct OAuthServerConfig {
1014    /// Issuer identifier (URL) for this authorization server.
1015    pub issuer: String,
1016    /// Access token lifetime.
1017    pub access_token_lifetime: Duration,
1018    /// Refresh token lifetime.
1019    pub refresh_token_lifetime: Duration,
1020    /// Authorization code lifetime (should be short; the default is 5 minutes).
1021    pub authorization_code_lifetime: Duration,
1022    /// Whether to allow public clients (clients without a secret).
1023    pub allow_public_clients: bool,
1024    /// Minimum PKCE code verifier length (default: 43, min: 43, max: 128).
1025    pub min_code_verifier_length: usize,
1026    /// Maximum PKCE code verifier length.
1027    pub max_code_verifier_length: usize,
1028    /// Maximum number of registered clients (hard-capped by
1029    /// [`HARD_MAX_OAUTH_CLIENTS`]).
1030    pub max_clients: usize,
1031    /// Maximum number of pending authorization codes globally (hard-capped by
1032    /// [`HARD_MAX_AUTHORIZATION_CODES`]).
1033    pub max_authorization_codes: usize,
1034    /// Maximum number of pending authorization codes for one client
1035    /// (hard-capped by [`HARD_MAX_AUTHORIZATION_CODES_PER_CLIENT`]).
1036    pub max_authorization_codes_per_client: usize,
1037    /// Maximum number of active access tokens globally (hard-capped by
1038    /// [`HARD_MAX_ACCESS_TOKENS`]).
1039    pub max_access_tokens: usize,
1040    /// Maximum number of active access tokens for one client (hard-capped by
1041    /// [`HARD_MAX_ACCESS_TOKENS_PER_CLIENT`]).
1042    pub max_access_tokens_per_client: usize,
1043    /// Maximum number of active refresh tokens globally (hard-capped by
1044    /// [`HARD_MAX_REFRESH_TOKENS`]).
1045    pub max_refresh_tokens: usize,
1046    /// Maximum number of active refresh tokens for one client (hard-capped by
1047    /// [`HARD_MAX_REFRESH_TOKENS_PER_CLIENT`]).
1048    pub max_refresh_tokens_per_client: usize,
1049    /// Maximum number of revocation tombstones globally (hard-capped by
1050    /// [`HARD_MAX_REVOCATION_TOMBSTONES`]).
1051    pub max_revocation_tombstones: usize,
1052    /// Maximum number of revocation tombstones for one client (hard-capped by
1053    /// [`HARD_MAX_REVOCATION_TOMBSTONES_PER_CLIENT`]).
1054    pub max_revocation_tombstones_per_client: usize,
1055}
1056
1057impl Default for OAuthServerConfig {
1058    fn default() -> Self {
1059        Self {
1060            // `.invalid` is reserved for names that must never resolve. This is
1061            // syntactically safe while still making deployment configuration
1062            // visibly mandatory for interoperable issuer claims.
1063            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    /// Validates PKCE policy, state-retention limits, and token lifetimes.
1085    ///
1086    /// # Errors
1087    ///
1088    /// Returns an error when PKCE bounds are outside RFC 7636, a state cap is
1089    /// zero, above its hard ceiling, incoherent, or over the checked aggregate
1090    /// retention ceiling, or a configured lifetime is zero or excessive.
1091    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// =============================================================================
1257// OAuth Client
1258// =============================================================================
1259
1260/// OAuth client types.
1261#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1262pub enum ClientType {
1263    /// Confidential client (has a secret).
1264    Confidential,
1265    /// Public client (no secret, e.g., native apps, SPAs).
1266    Public,
1267}
1268
1269/// A confidential-client credential retained only until registration.
1270///
1271/// The bytes are zeroized when the input object is dropped. Registered server
1272/// state contains only a verifier, never this plaintext value.
1273#[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/// Bounded salted verifier used by the current in-memory development server.
1297///
1298/// It removes plaintext retention and provides fixed-width comparison, but it
1299/// is not the AUTH-06 Argon2id production verifier. Production promotion still
1300/// requires the admitted blocking-work/KDF provider described by the plan.
1301#[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
1342/// A registered OAuth client.
1343pub struct OAuthClient {
1344    /// Unique client identifier.
1345    pub client_id: String,
1346    /// Client secret supplied for registration (absent for public clients).
1347    ///
1348    /// This field is intentionally private and cannot be cloned or recovered.
1349    /// [`OAuthServer::register_client`] consumes it and retains only a verifier.
1350    client_secret: Option<ClientSecret>,
1351    /// Client type.
1352    pub client_type: ClientType,
1353    /// Allowed redirect URIs.
1354    pub redirect_uris: Vec<String>,
1355    /// Allowed scopes.
1356    pub allowed_scopes: HashSet<String>,
1357    /// Client name (for display).
1358    pub name: Option<String>,
1359    /// Client description.
1360    pub description: Option<String>,
1361    /// When the client was registered.
1362    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/// Non-reusable identity for one registration of an OAuth client ID.
1418///
1419/// A client ID may be registered again only after unregistration. Keeping this
1420/// epoch on codes and token families prevents an old authorization decision
1421/// from being transferred to the new registration through that ABA sequence.
1422#[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/// Public metadata for a registered OAuth client.
1445///
1446/// This is the administrative read model returned by [`OAuthServer::get_client`]
1447/// and [`OAuthServer::list_clients`]. It deliberately has no client-credential
1448/// field: a confidential client's secret is available only to the registration
1449/// flow that creates or receives the [`OAuthClient`].
1450#[derive(Clone, PartialEq, Eq)]
1451pub struct OAuthClientMetadata {
1452    /// Unique client identifier.
1453    pub client_id: String,
1454    /// Client credential classification.
1455    pub client_type: ClientType,
1456    /// Allowed redirect URIs.
1457    pub redirect_uris: Vec<String>,
1458    /// Allowed scopes.
1459    pub allowed_scopes: HashSet<String>,
1460    /// Client name (for display).
1461    pub name: Option<String>,
1462    /// Client description.
1463    pub description: Option<String>,
1464    /// When the client was registered.
1465    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    /// Validates that a redirect URI is allowed for this client.
1503    #[must_use]
1504    pub fn validate_redirect_uri(&self, uri: &str) -> bool {
1505        validate_registered_redirect_uri(&self.redirect_uris, uri)
1506    }
1507
1508    /// Validates that the requested scopes are allowed for this client.
1509    #[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    /// Creates a new client builder.
1530    #[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    /// Validates that a redirect URI is allowed for this client.
1619    #[must_use]
1620    pub fn validate_redirect_uri(&self, uri: &str) -> bool {
1621        validate_registered_redirect_uri(&self.redirect_uris, uri)
1622    }
1623
1624    /// Validates that the requested scopes are allowed for this client.
1625    #[must_use]
1626    pub fn validate_scopes(&self, scopes: &[String]) -> bool {
1627        validate_registered_scopes(&self.allowed_scopes, scopes)
1628    }
1629
1630    /// Authenticates a confidential client.
1631    #[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    /// Returns whether this registration input carries a confidential-client
1643    /// credential.
1644    #[must_use]
1645    pub fn has_client_secret(&self) -> bool {
1646        self.client_secret.is_some()
1647    }
1648}
1649
1650/// Builder for OAuth clients.
1651pub 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    /// Creates a new client builder.
1678    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    /// Sets the client secret (makes this a confidential client).
1690    #[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    /// Adds a redirect URI.
1697    #[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    /// Adds multiple redirect URIs.
1704    #[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    /// Adds an allowed scope.
1715    #[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    /// Adds multiple allowed scopes.
1722    #[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    /// Sets the client name.
1734    #[must_use]
1735    pub fn name(mut self, name: impl Into<String>) -> Self {
1736        self.name = Some(name.into());
1737        self
1738    }
1739
1740    /// Sets the client description.
1741    #[must_use]
1742    pub fn description(mut self, description: impl Into<String>) -> Self {
1743        self.description = Some(description.into());
1744        self
1745    }
1746
1747    /// Builds the OAuth client.
1748    ///
1749    /// # Errors
1750    ///
1751    /// Returns an error if the client metadata is empty, inconsistent, or
1752    /// exceeds a retained-value or retained-count bound.
1753    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// =============================================================================
1776// Authorization Code
1777// =============================================================================
1778
1779/// PKCE code challenge method.
1780#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1781pub enum CodeChallengeMethod {
1782    /// Legacy plain-text method.
1783    ///
1784    /// This value can be parsed so callers can return a precise protocol
1785    /// error, but [`OAuthServer`] rejects it for authorization-code grants.
1786    Plain,
1787    /// SHA-256 hash (required by this OAuth 2.1 authorization-code path).
1788    S256,
1789}
1790
1791impl CodeChallengeMethod {
1792    /// Parses a code challenge method from a string.
1793    #[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    /// Returns the string representation.
1803    #[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/// Metadata retained for an authorization code issued during the flow.
1813///
1814/// The raw code is returned once by [`OAuthServer::authorize`]; server state
1815/// indexes this metadata by a domain-separated digest and never retains the
1816/// raw credential.
1817#[derive(Clone)]
1818pub struct AuthorizationCode {
1819    /// Client ID this code was issued to.
1820    pub client_id: String,
1821    /// Redirect URI used in the authorization request.
1822    pub redirect_uri: String,
1823    /// Approved scopes.
1824    pub scopes: Vec<String>,
1825    /// Approved RFC 8707 resource indicator, if one was requested.
1826    pub resource: Option<String>,
1827    /// PKCE code challenge.
1828    pub code_challenge: String,
1829    /// PKCE code challenge method.
1830    pub code_challenge_method: CodeChallengeMethod,
1831    /// When the code was issued.
1832    pub issued_at: Instant,
1833    /// When the code expires.
1834    pub expires_at: Instant,
1835    /// Subject (user) this code was issued for.
1836    pub subject: Option<String>,
1837    /// State parameter from the authorization request.
1838    pub state: Option<String>,
1839    /// Exact client registration that received the authorization decision.
1840    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    /// Checks if this code has expired.
1859    #[must_use]
1860    pub fn is_expired(&self) -> bool {
1861        Instant::now() >= self.expires_at
1862    }
1863
1864    /// Validates the PKCE code verifier against the stored challenge.
1865    #[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// =============================================================================
1880// OAuth Tokens
1881// =============================================================================
1882
1883/// Token type.
1884#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1885pub enum TokenType {
1886    /// Bearer token.
1887    Bearer,
1888}
1889
1890impl TokenType {
1891    /// Returns the string representation.
1892    #[must_use]
1893    pub fn as_str(&self) -> &'static str {
1894        match self {
1895            Self::Bearer => "bearer",
1896        }
1897    }
1898}
1899
1900/// Fixed-width, non-secret identifier for one authorization grant family.
1901///
1902/// Rotation preserves this identifier so replay or explicit refresh-token
1903/// revocation can invalidate every descendant without retaining raw bearer
1904/// credentials.
1905#[repr(transparent)]
1906#[derive(Clone, Copy, PartialEq, Eq, Hash)]
1907pub struct OAuthGrantId([u8; 32]);
1908
1909impl OAuthGrantId {
1910    /// Constructs a non-secret grant-family identifier from its fixed-width
1911    /// representation.
1912    #[must_use]
1913    pub const fn from_bytes(bytes: [u8; 32]) -> Self {
1914        Self(bytes)
1915    }
1916
1917    /// Borrows the fixed-width grant-family identifier.
1918    #[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/// OAuth token (access or refresh).
1931///
1932/// This value contains token metadata only. Raw bearer credentials are never
1933/// retained in server state and therefore cannot be recovered through token
1934/// introspection.
1935#[derive(Clone)]
1936pub struct OAuthToken {
1937    /// Auxiliary token text supplied by callers constructing standalone token
1938    /// metadata.
1939    ///
1940    /// Server-issued and introspected values always leave this empty; raw
1941    /// bearer credentials are retained only by the one-shot [`TokenResponse`].
1942    pub token: String,
1943    /// Token type.
1944    pub token_type: TokenType,
1945    /// Client ID this token was issued to.
1946    pub client_id: String,
1947    /// Approved scopes.
1948    pub scopes: Vec<String>,
1949    /// Exact RFC 8707 resource/audience binding, if the grant specified one.
1950    pub resource: Option<String>,
1951    /// When the token was issued.
1952    pub issued_at: Instant,
1953    /// When the token expires.
1954    pub expires_at: Instant,
1955    /// Subject (user) this token was issued for.
1956    pub subject: Option<String>,
1957    /// Whether this is a refresh token.
1958    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    /// Checks if this token has expired.
1974    #[must_use]
1975    pub fn is_expired(&self) -> bool {
1976        Instant::now() >= self.expires_at
1977    }
1978
1979    /// Returns the remaining lifetime in seconds.
1980    #[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/// Server-retained token metadata augmented with its revocation family.
1989///
1990/// `OAuthToken` remains the secret-free public introspection model; the grant
1991/// identifier is an internal authorization-server correlation key.
1992#[derive(Clone)]
1993pub(crate) struct StoredOAuthToken {
1994    metadata: OAuthToken,
1995    grant_id: OAuthGrantId,
1996    registration_epoch: OAuthRegistrationEpoch,
1997    /// Absolute, non-sliding deadline for the complete refresh-token family.
1998    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/// Token response for successful token issuance.
2010///
2011/// The response deliberately is not `Clone`: it is the sole post-commit owner
2012/// of the raw access and refresh credentials returned to the caller.
2013#[derive(serde::Serialize)]
2014pub struct TokenResponse {
2015    /// The access token.
2016    pub access_token: String,
2017    /// Token type (always "bearer").
2018    pub token_type: String,
2019    /// Token lifetime in seconds.
2020    pub expires_in: u64,
2021    /// Refresh token (if issued).
2022    #[serde(skip_serializing_if = "Option::is_none")]
2023    pub refresh_token: Option<String>,
2024    /// Granted scopes (space-separated).
2025    #[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// =============================================================================
2047// Authorization Request
2048// =============================================================================
2049
2050/// Immutable generation of the authorization-approval policy installed in an
2051/// [`OAuthServer`].
2052///
2053/// A backend must return this exact generation in an approved decision. It is
2054/// deliberately an opaque value: callers can compare generations but cannot
2055/// inspect policy material through it.
2056#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2057pub struct AuthorizationApprovalGeneration([u8; 32]);
2058
2059impl AuthorizationApprovalGeneration {
2060    /// Creates an opaque approval-policy generation from fixed-width bytes.
2061    #[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
2079/// Redacted, immutable request presented to the authorization/consent
2080/// backend after OAuth request and client validation succeeds.
2081///
2082/// It never contains a client secret, authorization code, access token, or
2083/// refresh token. The fields are bounded and canonicalized before this value
2084/// is created.
2085pub struct AuthorizationApprovalRequest {
2086    binding: AuthorizationApprovalBinding,
2087}
2088
2089impl AuthorizationApprovalRequest {
2090    /// Validated client identifier.
2091    #[must_use]
2092    pub fn client_id(&self) -> &str {
2093        &self.binding.client_id
2094    }
2095
2096    /// Validated redirect URI.
2097    #[must_use]
2098    pub fn redirect_uri(&self) -> &str {
2099        &self.binding.redirect_uri
2100    }
2101
2102    /// Canonical requested scopes.
2103    #[must_use]
2104    pub fn scopes(&self) -> &[String] {
2105        &self.binding.scopes
2106    }
2107
2108    /// Validated RFC 8707 resource indicator, if one was requested.
2109    #[must_use]
2110    pub fn resource(&self) -> Option<&str> {
2111        self.binding.resource.as_deref()
2112    }
2113
2114    /// Caller-supplied state, if present.
2115    #[must_use]
2116    pub fn state(&self) -> Option<&str> {
2117        self.binding.state.as_deref()
2118    }
2119
2120    /// S256 PKCE challenge identifier.
2121    #[must_use]
2122    pub fn code_challenge(&self) -> &str {
2123        &self.binding.code_challenge
2124    }
2125
2126    /// Validated PKCE method (always S256 on this server).
2127    #[must_use]
2128    pub const fn code_challenge_method(&self) -> CodeChallengeMethod {
2129        self.binding.code_challenge_method
2130    }
2131
2132    /// Produces a non-forgeable approval decision bound to this exact request.
2133    ///
2134    /// The decision is intentionally neither cloneable nor serializable. A
2135    /// backend may approve only the exact canonical scopes and resource it was
2136    /// shown; any mismatch is rejected by [`OAuthServer::authorize`] before a
2137    /// code is drawn or state is changed.
2138    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
2169/// One-shot approval decision produced only by an
2170/// [`AuthorizationApprovalBackend`].
2171///
2172/// Its fields are private and it deliberately implements neither `Clone` nor
2173/// serialization, preventing reuse or network transport of an approval
2174/// receipt.
2175pub 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/// Result of a synchronous authorization/consent backend invocation.
2194#[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    /// The backend approved the exact redacted request.
2200    Approved(AuthorizationApprovalDecision),
2201    /// The resource owner denied the request.
2202    Denied,
2203    /// The backend could not reach a decision.
2204    Error,
2205    /// The interaction was cancelled before approval completed.
2206    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
2220/// Synchronous, sealed authorization/consent policy boundary.
2221///
2222/// This API is intentionally synchronous because the existing OAuth server is
2223/// synchronous. An asynchronous backend would require a cancellation-correct
2224/// `Cx` threading change and is not represented by this surface.
2225pub trait AuthorizationApprovalBackend: Send + Sync {
2226    /// Immutable generation of the installed backend configuration.
2227    fn generation(&self) -> AuthorizationApprovalGeneration;
2228
2229    /// Decides one already-validated authorization request.
2230    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/// Authorization request parameters.
2269#[derive(Clone)]
2270pub struct AuthorizationRequest {
2271    /// Response type (must be "code" for authorization code flow).
2272    pub response_type: String,
2273    /// Client ID.
2274    pub client_id: String,
2275    /// Redirect URI.
2276    pub redirect_uri: String,
2277    /// Requested scopes (space-separated in original request).
2278    pub scopes: Vec<String>,
2279    /// RFC 8707 resource indicator requested for this authorization.
2280    pub resource: Option<String>,
2281    /// State parameter (recommended for CSRF protection).
2282    pub state: Option<String>,
2283    /// PKCE code challenge.
2284    pub code_challenge: String,
2285    /// PKCE code challenge method.
2286    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
2303/// Token request parameters.
2304pub struct TokenRequest {
2305    /// Grant type.
2306    pub grant_type: String,
2307    /// Authorization code (for authorization_code grant).
2308    pub code: Option<String>,
2309    /// Redirect URI (for authorization_code grant).
2310    pub redirect_uri: Option<String>,
2311    /// Client ID.
2312    pub client_id: String,
2313    /// Client secret (for confidential clients).
2314    pub client_secret: Option<String>,
2315    /// PKCE code verifier.
2316    pub code_verifier: Option<String>,
2317    /// Refresh token (for refresh_token grant).
2318    pub refresh_token: Option<String>,
2319    /// Requested scopes (for refresh_token grant, subset of original scopes).
2320    pub scopes: Option<Vec<String>>,
2321    /// RFC 8707 resource indicator. It must exactly match the bound
2322    /// authorization-code or refresh-token resource when supplied.
2323    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// =============================================================================
2343// OAuth Errors
2344// =============================================================================
2345
2346/// OAuth error types following RFC 6749.
2347#[derive(Clone)]
2348pub enum OAuthError {
2349    /// The request is missing a required parameter or is otherwise malformed.
2350    InvalidRequest(String),
2351    /// Client authentication failed.
2352    InvalidClient(String),
2353    /// The authorization grant or refresh token is invalid.
2354    InvalidGrant(String),
2355    /// The client is not authorized to use this grant type.
2356    UnauthorizedClient(String),
2357    /// The grant type is not supported.
2358    UnsupportedGrantType(String),
2359    /// The requested scope is invalid or unknown.
2360    InvalidScope(String),
2361    /// The authorization server encountered an unexpected condition.
2362    ServerError(String),
2363    /// The authorization server is temporarily unavailable.
2364    TemporarilyUnavailable(String),
2365    /// Access denied by the resource owner.
2366    AccessDenied(String),
2367    /// The response type is not supported.
2368    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    /// Returns the OAuth error code.
2394    #[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    /// Returns the error description.
2411    #[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// =============================================================================
2451// OAuth Server
2452// =============================================================================
2453
2454#[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/// Expiry-carrying record for a revoked or rotated token.
2481#[derive(Debug, Clone)]
2482pub(crate) struct RevocationTombstone {
2483    /// Client that owned the removed credential.
2484    pub(crate) client_id: String,
2485    /// Grant family that owned the removed credential.
2486    pub(crate) grant_id: OAuthGrantId,
2487    /// Whether this marker must be retained while the refresh chain remains
2488    /// live so reuse can trigger family-wide invalidation.
2489    pub(crate) replay_guard: bool,
2490    /// Retention deadline. Ordinary revocations use the credential expiry;
2491    /// live-chain replay guards use the family's fixed absolute deadline.
2492    pub(crate) expires_at: Instant,
2493}
2494
2495/// Internal state for the OAuth server.
2496pub(crate) struct OAuthServerState {
2497    /// Registered clients by client_id.
2498    clients: HashMap<String, RegisteredOAuthClient>,
2499    /// Pending authorization codes.
2500    pub(crate) authorization_codes: HashMap<CredentialDigest, AuthorizationCode>,
2501    /// Active access tokens.
2502    pub(crate) access_tokens: HashMap<CredentialDigest, StoredOAuthToken>,
2503    /// Active refresh tokens.
2504    pub(crate) refresh_tokens: HashMap<CredentialDigest, StoredOAuthToken>,
2505    /// Revoked tokens retained to their own expiry; refresh replay guards are
2506    /// retained to the fixed absolute deadline of their grant family.
2507    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    /// Atomically removes every active token descended from `grant_id` and
2669    /// leaves bounded, expiry-carrying tombstones for the removed credentials.
2670    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        // Once the complete removal set has been prepared, its replay guards
2691        // can release bounded tombstone capacity before terminal markers are
2692        // inserted. All validation and removal-set collection has completed.
2693        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
2771/// OAuth 2.0/2.1 authorization server.
2772///
2773/// This server contains an OAuth authorization-code path that requires PKCE.
2774/// That implementation policy is not an OAuth profile-conformance claim.
2775pub 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    /// Creates a new OAuth server with the given configuration.
2784    ///
2785    /// Invalid configurations remain fail-closed: mutation methods validate
2786    /// the configuration before changing state. Use [`Self::try_new`] to
2787    /// reject invalid configuration eagerly.
2788    #[must_use]
2789    pub fn new(config: OAuthServerConfig) -> Self {
2790        Self::with_approval_backend(config, Arc::new(DenyAllAuthorizationApprovalBackend))
2791    }
2792
2793    /// Creates an OAuth server with an installed authorization/consent backend.
2794    ///
2795    /// The backend is called exactly once after request and client validation,
2796    /// and before credential generation or state mutation. The default
2797    /// [`Self::new`] constructor installs a fail-closed deny-all backend.
2798    #[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    /// Creates a new OAuth server after validating its configuration.
2813    ///
2814    /// # Errors
2815    ///
2816    /// Returns an error for invalid PKCE bounds, state caps outside the hard
2817    /// per-field or aggregate retention envelope, incoherent state caps, or
2818    /// unsafe token lifetimes.
2819    pub fn try_new(config: OAuthServerConfig) -> Result<Self, OAuthError> {
2820        config.validate()?;
2821        Ok(Self::new(config))
2822    }
2823
2824    /// Creates a validated OAuth server with an installed approval backend.
2825    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    /// Creates a new OAuth server with default configuration.
2834    #[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    /// Returns the server configuration.
2850    #[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        // Capture time only after acquiring the write lock. A caller may have
2864        // waited arbitrarily long behind another mutation.
2865        let now = Instant::now();
2866        state.cleanup_expired_at(now);
2867        Ok((state, now))
2868    }
2869
2870    /// Acquires the write-side mutation gate only after rechecking a resource
2871    /// binding against the uncleaned state. This makes a resource mismatch a
2872    /// strict no-op: opportunistic expiry cleanup cannot run before the
2873    /// mismatch is rejected.
2874    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        // Capture time only after acquiring the write lock. A caller may have
2890        // waited arbitrarily long behind another mutation.
2891        let now = Instant::now();
2892        state.cleanup_expired_at(now);
2893        Ok((state, now))
2894    }
2895
2896    // -------------------------------------------------------------------------
2897    // Client Registration
2898    // -------------------------------------------------------------------------
2899
2900    /// Registers a new OAuth client.
2901    ///
2902    /// # Errors
2903    ///
2904    /// Returns an error if:
2905    /// - Public client fields were mutated into an inconsistent shape
2906    /// - Client metadata exceeds a retained-value or retained-count bound
2907    /// - A client with the same ID already exists
2908    /// - Public clients are not allowed and the client has no secret
2909    /// - The configured client capacity has been reached
2910    pub fn register_client(&self, client: OAuthClient) -> Result<(), OAuthError> {
2911        // OAuthClient fields are public for API ergonomics, so builder-time
2912        // validation is not a retention boundary. Revalidate the complete
2913        // object immediately before it can enter persistent server state.
2914        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    /// Unregisters an OAuth client.
2943    ///
2944    /// This also revokes all tokens issued to the client.
2945    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        // No descendant remains after unregistration. Purging the old
2966        // registration's tombstones prevents a later registration with the
2967        // same public client ID from inheriting replay-guard capacity or state.
2968        state
2969            .revoked_tokens
2970            .retain(|_, tombstone| tombstone.client_id != client_id);
2971
2972        Ok(())
2973    }
2974
2975    /// Gets secret-free metadata for a registered client by ID.
2976    ///
2977    /// Confidential client credentials are never cloned into this
2978    /// administrative read model.
2979    #[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    /// Lists secret-free metadata for all registered clients.
2991    ///
2992    /// Confidential client credentials are never cloned into this
2993    /// administrative read model.
2994    #[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    // -------------------------------------------------------------------------
3006    // Authorization Endpoint
3007    // -------------------------------------------------------------------------
3008
3009    /// Validates an authorization request, obtains one backend approval, and
3010    /// creates an authorization code only for a matching approved decision.
3011    ///
3012    /// # Returns
3013    ///
3014    /// Returns the authorization code and redirect URI on success.
3015    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    /// Builds a safe authorization-error redirect after an authorization
3023    /// request failed.
3024    ///
3025    /// RFC 6749 permits a direct error only when the client or redirect URI
3026    /// cannot be trusted. This helper therefore re-validates exactly those
3027    /// two routing inputs without mutating OAuth state. A caller must use the
3028    /// returned URI only for an authorization-endpoint error response.
3029    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        // Match successful authorization responses: the issuer identifier is
3064        // part of the authorization response, including failures.
3065        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        // Validate response_type
3094        if request.response_type != "code" {
3095            return Err(OAuthError::UnsupportedResponseType(
3096                "only 'code' response_type is supported".to_string(),
3097            ));
3098        }
3099
3100        // Snapshot the exact registration that received this authorization
3101        // decision. The client ID alone is reusable after unregistration.
3102        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        // Validate redirect URI
3117        if !client.validate_redirect_uri(&request.redirect_uri) {
3118            return Err(OAuthError::InvalidRequest(
3119                "invalid redirect_uri".to_string(),
3120            ));
3121        }
3122
3123        // Validate scopes
3124        if !client.validate_scopes(&canonical_scopes) {
3125            return Err(OAuthError::InvalidScope(
3126                "requested scope not allowed".to_string(),
3127            ));
3128        }
3129
3130        // OAuth 2.1 requires PKCE, and this server deliberately supports only
3131        // S256. Accepting `plain` would let an intercepted challenge be used as
3132        // the verifier and would silently downgrade the authorization flow.
3133        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        // The accepted decision is consumed here before the code draw. No
3182        // denial, backend error, cancellation, or binding mismatch reaches
3183        // either random generation or mutable OAuth state.
3184        let subject = approval.subject;
3185
3186        // Generate authorization code
3187        let code_value = generate_token_with_draw(draw)?;
3188        let code_digest = digest_credential(CredentialKind::AuthorizationCode, &code_value)?;
3189        // Store the code
3190        {
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        // Build redirect URI with code
3244        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        // RFC 9207 binds the authorization response to the issuing server.
3254        // Registered redirect URIs containing their own `iss` parameter are
3255        // rejected, so this cannot create an ambiguous duplicate.
3256        redirect.push_str("&iss=");
3257        redirect.push_str(&url_encode(&self.config.issuer));
3258
3259        Ok((code_value, redirect))
3260    }
3261
3262    // -------------------------------------------------------------------------
3263    // Token Endpoint
3264    // -------------------------------------------------------------------------
3265
3266    /// Exchanges an authorization code or refresh token for tokens.
3267    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        // Validate required parameters
3294        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        // Enforce the fixed RFC 7636 syntax and hard bounds before consuming
3322        // the one-use authorization code or performing SHA-256.
3323        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        // Reject a mismatched resource under a read-only snapshot before any
3331        // write-side cleanup can affect otherwise unrelated expired state.
3332        // A missing code remains deferred to the mutation gate so existing
3333        // indistinguishable invalid-grant handling and cleanup semantics hold.
3334        {
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        // Validation, capacity admission, credential generation, one-time code
3349        // consumption, and token insertion share one write-side critical
3350        // section. Failed validation, capacity checks, or either random draw
3351        // leaves the authorization code available for a legitimate retry.
3352        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        // As for authorization-code exchange, resource mismatch must be a
3460        // no-op even when the state contains unrelated expired entries.
3461        // Absence is deliberately deferred: a retained replay marker needs
3462        // the existing write-side family-revocation behavior.
3463        {
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        // Validation, rotation, tombstoning, and insertion are one atomic
3479        // mutation. A successful refresh consumes the presented token exactly
3480        // once. Replaying a retained rotated-token marker revokes every live
3481        // descendant before returning the indistinguishable grant error.
3482        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        // Determine scopes (subset of original if specified)
3525        let scopes = if let Some(requested) = canonical_requested_scopes {
3526            // Validate that requested scopes are a subset of original
3527            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        // Credential generation may block behind the platform RNG. Token and
3617        // family lifetimes begin only after both successful draws.
3618        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    // -------------------------------------------------------------------------
3744    // Token Revocation (RFC 7009)
3745    // -------------------------------------------------------------------------
3746
3747    /// Revokes a token (access or refresh).
3748    ///
3749    /// Per RFC 7009, this always returns success even if the token was not found.
3750    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 and perform the ownership check and deletion under the
3774        // same write lock. In particular, never remove first and discover
3775        // afterward that the token belongs to another client.
3776        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            // RFC 7009 requires an indistinguishable success response for an
3792            // unknown token. Treat a token owned by another client the same way.
3793            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    // -------------------------------------------------------------------------
3821    // Token Introspection
3822    // -------------------------------------------------------------------------
3823
3824    /// Validates an access token and returns its metadata.
3825    ///
3826    /// This is used internally and by the [`OAuthTokenVerifier`].
3827    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        // Check if revoked
3842        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    // -------------------------------------------------------------------------
3861    // MCP Integration
3862    // -------------------------------------------------------------------------
3863
3864    /// Creates a token verifier for use with MCP [`crate::auth::TokenAuthProvider`].
3865    #[must_use]
3866    pub fn token_verifier(self: &Arc<Self>) -> OAuthTokenVerifier {
3867        OAuthTokenVerifier {
3868            server: Arc::clone(self),
3869        }
3870    }
3871
3872    // -------------------------------------------------------------------------
3873    // Maintenance
3874    // -------------------------------------------------------------------------
3875
3876    /// Removes expired tokens, authorization codes, and revocation tombstones.
3877    ///
3878    /// Mutating operations already perform this cleanup opportunistically.
3879    /// Call this during read-only workloads when prompt reclamation matters.
3880    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    /// Returns statistics about the server state.
3889    #[must_use]
3890    pub fn stats(&self) -> OAuthServerStats {
3891        let state = match self.state.read() {
3892            Ok(guard) => guard,
3893            // Preserve observability during partial failure instead of panicking on poison.
3894            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/// Statistics about the OAuth server state.
3907#[derive(Debug, Clone, Default)]
3908pub struct OAuthServerStats {
3909    /// Number of registered clients.
3910    pub clients: usize,
3911    /// Number of pending authorization codes.
3912    pub authorization_codes: usize,
3913    /// Number of active access tokens.
3914    pub access_tokens: usize,
3915    /// Number of active refresh tokens.
3916    pub refresh_tokens: usize,
3917    /// Number of retained revocation tombstones.
3918    pub revoked_tokens: usize,
3919}
3920
3921// =============================================================================
3922// Token Verifier Implementation
3923// =============================================================================
3924
3925/// OAuth token verifier for MCP integration.
3926///
3927/// This implements [`TokenVerifier`] to allow the OAuth server to be used
3928/// with the MCP server's [`crate::auth::TokenAuthProvider`].
3929pub 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        // Only accept Bearer tokens
3941        if !token.scheme.eq_ignore_ascii_case("Bearer") {
3942            return Err(McpError::new(
3943                McpErrorCode::ResourceForbidden,
3944                "unsupported auth scheme",
3945            ));
3946        }
3947
3948        // Validate the token
3949        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
3987// =============================================================================
3988// Helper Functions
3989// =============================================================================
3990
3991fn 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
4353/// Draws token material through the core security-identifier API.
4354fn 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    // Base64url encode (URL-safe, no padding).
4365    Ok(base64url_encode(identifier.as_bytes()))
4366}
4367
4368/// Base64url encodes bytes (URL-safe, no padding).
4369fn 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
4375/// Validates the fixed RFC 7636 verifier grammar and byte bounds.
4376fn 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
4400/// Computes the exact RFC 7636 S256 challenge from an admitted verifier.
4401fn 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
4408/// Validates the canonical, unpadded base64url encoding of a SHA-256 digest.
4409fn validate_s256_code_challenge(challenge: &str) -> Result<(), OAuthError> {
4410    use base64::Engine;
4411    use base64::engine::general_purpose::URL_SAFE_NO_PAD;
4412
4413    // An unpadded base64url encoding of a 32-byte SHA-256 digest is exactly 43
4414    // bytes. Check this before decoding to keep the allocation strictly bounded.
4415    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
4433/// URL-encodes a string.
4434fn 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        // Non-loopback redirects use byte-for-byte registration matching.
4461        // Parsed comparison alone would silently normalize meaningful URI
4462        // spelling differences at a security boundary.
4463        if allowed == uri {
4464            return true;
4465        }
4466
4467        // RFC 8252 permits a native app to choose its loopback listener port at
4468        // launch. Every other component, including the exact IP family/address,
4469        // remains bound to the registration.
4470        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
4506/// Constant-time string comparison.
4507fn 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// =============================================================================
4674// Tests
4675// =============================================================================
4676
4677#[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        // This differs from the matching positive only by a second client_id.
4848        // Rejection occurs before an adapter could authenticate a client or
4849        // create an authorization grant.
4850        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        // The near-identical positive above has one client_secret field. An
4894        // empty first occurrence still reserves that defined name, so a later
4895        // value cannot turn omission into an authentication ambiguity.
4896        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        // This differs from the matching token positive only by a second
4940        // resource field. It must not become an ambiguous resource selector.
4941        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        // The positive above contains one bare `state`. Adding only `state=`
4995        // must be rejected even though both values are omitted downstream.
4996        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        // The parser accepts raw bytes only. It neither receives an OAuth
5015        // server nor has a route, transport, redirect, subject, or mutation
5016        // capability; later HTTP and AUTH-07 layers own those concerns.
5017        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        // Four bounded unknown values reach exactly 16 KiB without exceeding
5151        // the 4 KiB decoded-value cap: 3 * (8 + 4096) + 3 + (8 + 4061).
5152        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        // Exact match
6224        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        // The exact loopback IP may use a different ephemeral port.
6228        assert!(client.validate_redirect_uri("http://127.0.0.1:8080/callback"));
6229
6230        // Invalid
6231        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        // Even an exact registration cannot opt into an authority-confusion
6238        // URI containing userinfo.
6239        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        // Registration cannot opt into fragments either; RFC 6749 forbids a
6247        // redirect endpoint from carrying a fragment component.
6248        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        // Duplicate registration should fail
6350        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        // Verify client exists
6357        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        // Create authorization request
6507        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        // Request without PKCE should fail
6536        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(), // Missing!
6544            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        // Test vectors from RFC 4648
6895        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        // Register a client
6978        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        // Token should be valid
6994        assert!(
6995            server
6996                .validate_access_token(&token_response.access_token)
6997                .is_some()
6998        );
6999
7000        // Revoke the token
7001        server
7002            .revoke(&token_response.access_token, "test-client", None)
7003            .unwrap();
7004
7005        // Token should no longer be valid
7006        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        // Unregistering again should fail
7030        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        // Register a client and create a token
7038        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        // Create verifier
7054        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        // Valid token
7065        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        // Invalid token
7076        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        // Wrong scheme
7084        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    // ========================================
7093    // OAuthServerConfig
7094    // ========================================
7095
7096    #[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    // ========================================
7292    // ClientType
7293    // ========================================
7294
7295    #[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; // Copy
7307        assert_eq!(t, t2);
7308    }
7309
7310    // ========================================
7311    // OAuthClient — additional
7312    // ========================================
7313
7314    #[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        // Public client with no secret provided: should succeed
7335        assert!(client.authenticate(None));
7336        // Public client with secret provided: should fail
7337        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        // Non-loopback redirects require an exact match.
7347        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        // IPv6 loopback with a different port.
7359        assert!(client.validate_redirect_uri("http://[::1]:8080/callback"));
7360        // A hostname or different IP family may not borrow the port exception.
7361        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        // Empty scopes should always be valid
7373        assert!(client.validate_scopes(&[]));
7374    }
7375
7376    // ========================================
7377    // OAuthClientBuilder — additional
7378    // ========================================
7379
7380    #[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    // ========================================
7433    // CodeChallengeMethod — additional
7434    // ========================================
7435
7436    #[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; // Copy
7446        assert_eq!(m, m2);
7447        let m3 = m.clone();
7448        assert_eq!(m, m3);
7449    }
7450
7451    // ========================================
7452    // AuthorizationCode
7453    // ========================================
7454
7455    #[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    // ========================================
7562    // TokenType
7563    // ========================================
7564
7565    #[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; // Copy
7574        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    // ========================================
7582    // OAuthToken
7583    // ========================================
7584
7585    #[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    // ========================================
7644    // TokenResponse
7645    // ========================================
7646
7647    #[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    // ========================================
7676    // AuthorizationRequest / TokenRequest
7677    // ========================================
7678
7679    #[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    // ========================================
7821    // OAuthError — additional
7822    // ========================================
7823
7824    #[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        // InvalidClient and UnauthorizedClient and AccessDenied → ResourceForbidden
7874        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        // Other variants → InvalidRequest
7885        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    // ========================================
7892    // OAuthServer — additional
7893    // ========================================
7894
7895    #[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(), // not "code"
7945            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, // missing
8048            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, // missing
8067            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, // missing
8088            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        // Fail-closed validation now rejects configs below the RFC 7636
8100        // 43-byte floor outright, so the most permissive LEGAL configuration
8101        // is the floor itself; the short verifier below must still bounce.
8102        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        // Authorize first
8111        let issued_verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
8112        let verifier = "short"; // The fixed 43-byte minimum still applies.
8113        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        // PKCE verifier failures on the token endpoint uniformly surface as
8139        // invalid_grant (RFC 7636 section 4.6), keeping length and mismatch
8140        // rejections indistinguishable to a probing client.
8141        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        // First use succeeds
8229        server.token(&token_req).unwrap();
8230        // Second use fails (code is single-use)
8231        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    // ========================================
8304    // OAuthServerStats
8305    // ========================================
8306
8307    #[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    // ========================================
8331    // Helper functions — additional
8332    // ========================================
8333
8334    #[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    // ========================================
8391    // Refresh token flow
8392    // ========================================
8393
8394    #[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        // Use refresh token to get a new access token
8415        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        // New access token should be different
8430        assert_ne!(new_resp.access_token, token_resp.access_token);
8431        assert_eq!(new_resp.token_type, "bearer");
8432        // Successful refresh rotates the credential.
8433        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        // Scopes preserved
8439        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        // Request only a subset of scopes
8463        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        // Request scope not in original grant
8500        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        // Revoking a refresh token invalidates the complete grant family.
8538        server.revoke(&refresh, "c1", None).unwrap();
8539        assert!(server.validate_access_token(&access).is_none());
8540
8541        // Refresh should now fail
8542        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        // Try to use with different client_id
8586        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    // ========================================
8658    // Token exchange edge cases
8659    // ========================================
8660
8661    #[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        // Exchange with different redirect_uri
8687        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        // Exchange with different client_id
8744        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        // Exchange with wrong secret
8796        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    // ========================================
8898    // Authorization edge cases
8899    // ========================================
8900
8901    #[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        // Redirect should contain code, state, and the RFC 9207 issuer binding.
8955        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        // Should use '&' separator since '?' already exists
8994        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    // ========================================
8999    // Error conversions
9000    // ========================================
9001
9002    #[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    // ========================================
9034    // Revocation edge cases
9035    // ========================================
9036
9037    #[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        // Per RFC 7009, revoking an unknown token is not an error
9047        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        // c2 tries to revoke c1's tokens — both calls succeed silently, but
9079        // neither token may be removed or marked revoked.
9080        server.revoke(&token_resp.access_token, "c2", None).unwrap();
9081        server.revoke(&refresh_token, "c2", None).unwrap();
9082
9083        // Token remains active and was not added to the global revocation set.
9084        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    // ========================================
9115    // Unregister edge cases
9116    // ========================================
9117
9118    #[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        // Create an auth code
9129        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        // Verify code exists
9144        {
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        // Unregister client
9154        server.unregister_client("c1").unwrap();
9155
9156        // Auth code should be removed
9157        {
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    // ========================================
9168    // Server misc
9169    // ========================================
9170
9171    #[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        // Claims should include client_id and issuer
9241        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        // Should be > 0 since it expires in the future
9515        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        // Authorization does not authenticate the client; token exchange does.
9530        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        // Token exchange with correct secret
9545        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        // Refresh with wrong secret
9562        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        // Unicode bytes get percent-encoded
9637        let encoded = url_encode("ü");
9638        assert!(encoded.contains('%'));
9639    }
9640
9641    // ========================================
9642    // Additional coverage — uncovered paths
9643    // ========================================
9644
9645    #[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        // Delete client, then try to refresh
9738        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        // Client authentication deliberately precedes the revoked-grant
9755        // lookup, so deleted and never-registered clients are indistinguishable.
9756        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        // Revoke the refresh token specifically
9796        server.revoke(&refresh, "c1", None).unwrap();
9797        assert!(server.validate_access_token(&access).is_none());
9798
9799        // Verify it's in revoked set
9800        {
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            // The infallible constructor is retained for API compatibility,
10266            // but every mutation still validates before changing state.
10267            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        // The previous registration's most recent descendant is invalid too.
10888        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        // Client registration is otherwise unrelated to credential state;
11095        // its write gate must still clean every expiry-bearing collection.
11096        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}