Skip to main content

fastmcp_server/
oidc.rs

1//! OpenID Connect (OIDC) Provider for MCP.
2//!
3//! This module extends the OAuth 2.0/2.1 server with OpenID Connect identity
4//! layer features:
5//!
6//! - **ID Token Issuance**: fail-closed until an FND-09 external signer is admitted
7//! - **UserInfo Endpoint**: Standard endpoint for retrieving user claims
8//! - **Discovery Document**: `.well-known/openid-configuration` metadata
9//! - **Standard Claims**: OpenID Connect standard claim types
10//!
11//! This source inventory is not a promoted production OIDC profile. AUTH and
12//! MCP 2026-07-28 conformance gates remain outstanding; the public protocol
13//! constant is still `2024-11-05`.
14//!
15//! # Architecture
16//!
17//! The OIDC provider builds on top of [`OAuthServer`] by:
18//!
19//! 1. Adding the `openid` scope to enable OIDC flows
20//! 2. Reserving ID-token issuance for an FND-09 externally custodied signer
21//! 3. Providing standard endpoints for identity operations
22//!
23//! # Example
24//!
25//! ```ignore
26//! use std::sync::Arc;
27//!
28//! use fastmcp_rust::oidc::{OidcProvider, OidcProviderConfig, UserClaims};
29//! use fastmcp_rust::oauth::{OAuthServer, OAuthServerConfig};
30//!
31//! // Create OAuth server first
32//! let oauth = Arc::new(OAuthServer::new(OAuthServerConfig::default()));
33//!
34//! // Create OIDC provider on top
35//! let oidc = OidcProvider::new(oauth, OidcProviderConfig::default())
36//!     .expect("oidc provider");
37//!
38//! // Set up user claims provider
39//! oidc.set_claims_fn(|subject| {
40//!     Some(UserClaims::new(subject)
41//!         .with_name("John Doe")
42//!         .with_email("john@example.com"))
43//! });
44//! ```
45
46#[cfg(feature = "builtin-auth-server")]
47use std::collections::BTreeMap;
48use std::collections::HashMap;
49#[cfg(feature = "builtin-auth-server")]
50use std::future::Future;
51#[cfg(feature = "builtin-auth-server")]
52use std::pin::Pin;
53use std::sync::{Arc, RwLock};
54#[cfg(feature = "builtin-auth-server")]
55use std::time::{Instant, SystemTime, UNIX_EPOCH};
56
57#[cfg(feature = "builtin-auth-server")]
58use fastmcp_core::Cx;
59#[cfg(feature = "builtin-auth-server")]
60use fastmcp_protocol::jose::{
61    AdmittedRsaJwks, BoundedJwsClaims, CanonicalRs256PublicJwks, CanonicalRs256PublicJwksSet,
62    ExternalRs256Signer, ExternalRs256SigningDeadline, JwksEndpointReadBack, JwsSigningError,
63    JwsSigningProfile, MAX_JWKS_BYTES, MAX_JWKS_KEYS, MAX_KID_BYTES, Rs256PublicKeyRing,
64    Rs256SigningBinding, SigningActivationProfile, SigningActivationReceipt,
65    verify_compact_jws_rs256,
66};
67#[cfg(feature = "builtin-auth-server")]
68use url::Url;
69
70use crate::oauth::{OAuthError, OAuthServer, OAuthServerConfig, OAuthToken, validate_oauth_issuer};
71
72#[cfg(feature = "builtin-auth-server")]
73const MAX_OIDC_NONCE_BYTES: usize = 1_024;
74
75/// Fixed framework-owned claims for the external signer health canary.
76///
77/// This is not an ID token and is never exposed to a client. Its issuer,
78/// endpoint, canonical public bytes, and signer generations are instead bound
79/// by the sealed activation state that owns the canary result. Keeping this
80/// input fixed lets a KMS/HSM integration provision one independently audited
81/// test vector without putting any private material in this process.
82#[cfg(feature = "builtin-auth-server")]
83const OIDC_SIGNING_CANARY_CLAIMS: &str = r#"{"sub":"fixed-vector","aud":"server-policy-later"}"#;
84
85// =============================================================================
86// Configuration
87// =============================================================================
88
89/// Configuration for the OIDC provider.
90#[derive(Debug, Clone)]
91pub struct OidcProviderConfig {
92    /// Issuer identifier (URL) - must match OAuth server issuer.
93    pub issuer: String,
94    /// Supported claims.
95    pub supported_claims: Vec<String>,
96    /// Supported scopes beyond `openid`.
97    pub supported_scopes: Vec<String>,
98}
99
100impl Default for OidcProviderConfig {
101    fn default() -> Self {
102        Self {
103            issuer: OAuthServerConfig::default().issuer,
104            supported_claims: vec![
105                "sub".to_string(),
106                "name".to_string(),
107                "email".to_string(),
108                "email_verified".to_string(),
109                "preferred_username".to_string(),
110                "picture".to_string(),
111                "updated_at".to_string(),
112            ],
113            supported_scopes: vec![
114                "openid".to_string(),
115                "profile".to_string(),
116                "email".to_string(),
117            ],
118        }
119    }
120}
121
122// =============================================================================
123// User Claims
124// =============================================================================
125
126/// Standard OpenID Connect user claims.
127///
128/// These claims describe the authenticated user and are included in
129/// ID tokens and returned from the userinfo endpoint.
130#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
131pub struct UserClaims {
132    /// Subject identifier (required, unique user ID).
133    pub sub: String,
134
135    // Profile scope claims
136    /// User's full name.
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub name: Option<String>,
139    /// User's given/first name.
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub given_name: Option<String>,
142    /// User's family/last name.
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub family_name: Option<String>,
145    /// User's middle name.
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub middle_name: Option<String>,
148    /// User's nickname/username.
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub nickname: Option<String>,
151    /// User's preferred username.
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub preferred_username: Option<String>,
154    /// URL of user's profile page.
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub profile: Option<String>,
157    /// URL of user's profile picture.
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub picture: Option<String>,
160    /// URL of user's website.
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub website: Option<String>,
163    /// User's gender.
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub gender: Option<String>,
166    /// User's birthday (ISO 8601 date).
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub birthdate: Option<String>,
169    /// User's timezone (IANA timezone string).
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub zoneinfo: Option<String>,
172    /// User's locale (BCP47 language tag).
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub locale: Option<String>,
175    /// Time the user's info was last updated (Unix timestamp).
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub updated_at: Option<i64>,
178
179    // Email scope claims
180    /// User's email address.
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub email: Option<String>,
183    /// Whether the email has been verified.
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub email_verified: Option<bool>,
186
187    // Phone scope claims
188    /// User's phone number.
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub phone_number: Option<String>,
191    /// Whether the phone number has been verified.
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub phone_number_verified: Option<bool>,
194
195    // Address scope claims
196    /// User's address (JSON object).
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub address: Option<AddressClaim>,
199
200    /// Additional custom claims.
201    #[serde(flatten)]
202    pub custom: HashMap<String, serde_json::Value>,
203}
204
205impl std::fmt::Debug for UserClaims {
206    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207        let profile_claim_count = [
208            self.name.is_some(),
209            self.given_name.is_some(),
210            self.family_name.is_some(),
211            self.middle_name.is_some(),
212            self.nickname.is_some(),
213            self.preferred_username.is_some(),
214            self.profile.is_some(),
215            self.picture.is_some(),
216            self.website.is_some(),
217            self.gender.is_some(),
218            self.birthdate.is_some(),
219            self.zoneinfo.is_some(),
220            self.locale.is_some(),
221            self.updated_at.is_some(),
222        ]
223        .into_iter()
224        .filter(|present| *present)
225        .count();
226
227        f.debug_struct("UserClaims")
228            .field("subject_len", &self.sub.len())
229            .field("profile_claim_count", &profile_claim_count)
230            .field("email_present", &self.email.is_some())
231            .field("email_verified_present", &self.email_verified.is_some())
232            .field("phone_number_present", &self.phone_number.is_some())
233            .field(
234                "phone_number_verified_present",
235                &self.phone_number_verified.is_some(),
236            )
237            .field("address_present", &self.address.is_some())
238            .field("custom_claim_count", &self.custom.len())
239            .finish()
240    }
241}
242
243impl UserClaims {
244    /// Creates new user claims with the given subject.
245    #[must_use]
246    pub fn new(sub: impl Into<String>) -> Self {
247        Self {
248            sub: sub.into(),
249            ..Default::default()
250        }
251    }
252
253    /// Sets the user's full name.
254    #[must_use]
255    pub fn with_name(mut self, name: impl Into<String>) -> Self {
256        self.name = Some(name.into());
257        self
258    }
259
260    /// Sets the user's email.
261    #[must_use]
262    pub fn with_email(mut self, email: impl Into<String>) -> Self {
263        self.email = Some(email.into());
264        self
265    }
266
267    /// Sets whether the email is verified.
268    #[must_use]
269    pub fn with_email_verified(mut self, verified: bool) -> Self {
270        self.email_verified = Some(verified);
271        self
272    }
273
274    /// Sets the user's preferred username.
275    #[must_use]
276    pub fn with_preferred_username(mut self, username: impl Into<String>) -> Self {
277        self.preferred_username = Some(username.into());
278        self
279    }
280
281    /// Sets the user's profile picture URL.
282    #[must_use]
283    pub fn with_picture(mut self, url: impl Into<String>) -> Self {
284        self.picture = Some(url.into());
285        self
286    }
287
288    /// Sets the user's given name.
289    #[must_use]
290    pub fn with_given_name(mut self, name: impl Into<String>) -> Self {
291        self.given_name = Some(name.into());
292        self
293    }
294
295    /// Sets the user's family name.
296    #[must_use]
297    pub fn with_family_name(mut self, name: impl Into<String>) -> Self {
298        self.family_name = Some(name.into());
299        self
300    }
301
302    /// Sets the user's phone number.
303    #[must_use]
304    pub fn with_phone_number(mut self, phone: impl Into<String>) -> Self {
305        self.phone_number = Some(phone.into());
306        self
307    }
308
309    /// Sets the updated_at timestamp.
310    #[must_use]
311    pub fn with_updated_at(mut self, timestamp: i64) -> Self {
312        self.updated_at = Some(timestamp);
313        self
314    }
315
316    /// Adds a custom claim.
317    #[must_use]
318    pub fn with_custom(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
319        self.custom.insert(key.into(), value);
320        self
321    }
322
323    /// Filters claims based on requested scopes.
324    ///
325    /// Only returns claims that are allowed by the given scopes.
326    #[must_use]
327    #[allow(clippy::assigning_clones)]
328    pub fn filter_by_scopes(&self, scopes: &[String]) -> UserClaims {
329        let mut filtered = UserClaims::new(&self.sub);
330
331        // Profile scope claims
332        if scopes.iter().any(|s| s == "profile") {
333            filtered.name = self.name.clone();
334            filtered.given_name = self.given_name.clone();
335            filtered.family_name = self.family_name.clone();
336            filtered.middle_name = self.middle_name.clone();
337            filtered.nickname = self.nickname.clone();
338            filtered.preferred_username = self.preferred_username.clone();
339            filtered.profile = self.profile.clone();
340            filtered.picture = self.picture.clone();
341            filtered.website = self.website.clone();
342            filtered.gender = self.gender.clone();
343            filtered.birthdate = self.birthdate.clone();
344            filtered.zoneinfo = self.zoneinfo.clone();
345            filtered.locale = self.locale.clone();
346            filtered.updated_at = self.updated_at;
347        }
348
349        // Email scope claims
350        if scopes.iter().any(|s| s == "email") {
351            filtered.email = self.email.clone();
352            filtered.email_verified = self.email_verified;
353        }
354
355        // Phone scope claims
356        if scopes.iter().any(|s| s == "phone") {
357            filtered.phone_number = self.phone_number.clone();
358            filtered.phone_number_verified = self.phone_number_verified;
359        }
360
361        // Address scope claims
362        if scopes.iter().any(|s| s == "address") {
363            filtered.address = self.address.clone();
364        }
365
366        filtered
367    }
368}
369
370/// Address claim structure per OpenID Connect spec.
371#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
372pub struct AddressClaim {
373    /// Full formatted address.
374    #[serde(skip_serializing_if = "Option::is_none")]
375    pub formatted: Option<String>,
376    /// Street address.
377    #[serde(skip_serializing_if = "Option::is_none")]
378    pub street_address: Option<String>,
379    /// City/locality.
380    #[serde(skip_serializing_if = "Option::is_none")]
381    pub locality: Option<String>,
382    /// State/region.
383    #[serde(skip_serializing_if = "Option::is_none")]
384    pub region: Option<String>,
385    /// Postal/zip code.
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub postal_code: Option<String>,
388    /// Country.
389    #[serde(skip_serializing_if = "Option::is_none")]
390    pub country: Option<String>,
391}
392
393impl std::fmt::Debug for AddressClaim {
394    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
395        let populated_field_count = [
396            self.formatted.is_some(),
397            self.street_address.is_some(),
398            self.locality.is_some(),
399            self.region.is_some(),
400            self.postal_code.is_some(),
401            self.country.is_some(),
402        ]
403        .into_iter()
404        .filter(|present| *present)
405        .count();
406
407        f.debug_struct("AddressClaim")
408            .field("populated_field_count", &populated_field_count)
409            .finish()
410    }
411}
412
413// =============================================================================
414// ID Token
415// =============================================================================
416
417/// ID Token claims (JWT payload).
418#[derive(Clone, serde::Serialize, serde::Deserialize)]
419pub struct IdTokenClaims {
420    /// Issuer identifier.
421    pub iss: String,
422    /// Subject identifier.
423    pub sub: String,
424    /// Audience (client ID).
425    pub aud: String,
426    /// Expiration time (Unix timestamp).
427    pub exp: i64,
428    /// Issued at time (Unix timestamp).
429    pub iat: i64,
430    /// Authentication time (Unix timestamp).
431    #[serde(skip_serializing_if = "Option::is_none")]
432    pub auth_time: Option<i64>,
433    /// Nonce from authorization request.
434    #[serde(skip_serializing_if = "Option::is_none")]
435    pub nonce: Option<String>,
436    /// Authentication Context Class Reference.
437    #[serde(skip_serializing_if = "Option::is_none")]
438    pub acr: Option<String>,
439    /// Authentication Methods References.
440    #[serde(skip_serializing_if = "Option::is_none")]
441    pub amr: Option<Vec<String>>,
442    /// Authorized party (client ID that was issued the token).
443    #[serde(skip_serializing_if = "Option::is_none")]
444    pub azp: Option<String>,
445    /// Access token hash (for hybrid flows).
446    #[serde(skip_serializing_if = "Option::is_none")]
447    pub at_hash: Option<String>,
448    /// Code hash (for hybrid flows).
449    #[serde(skip_serializing_if = "Option::is_none")]
450    pub c_hash: Option<String>,
451    /// Additional user claims.
452    #[serde(flatten)]
453    pub user_claims: UserClaims,
454}
455
456impl std::fmt::Debug for IdTokenClaims {
457    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
458        f.debug_struct("IdTokenClaims")
459            .field("issuer_len", &self.iss.len())
460            .field("subject_len", &self.sub.len())
461            .field("audience_len", &self.aud.len())
462            .field("auth_time_present", &self.auth_time.is_some())
463            .field("nonce_present", &self.nonce.is_some())
464            .field("acr_present", &self.acr.is_some())
465            .field("amr_count", &self.amr.as_ref().map_or(0, Vec::len))
466            .field("authorized_party_present", &self.azp.is_some())
467            .field("access_token_hash_present", &self.at_hash.is_some())
468            .field("code_hash_present", &self.c_hash.is_some())
469            .field("user_claims", &self.user_claims)
470            .finish_non_exhaustive()
471    }
472}
473
474/// A signed ID token.
475#[derive(Clone)]
476pub struct IdToken {
477    /// The raw JWT string.
478    pub raw: String,
479    /// The parsed claims.
480    pub claims: IdTokenClaims,
481}
482
483impl std::fmt::Debug for IdToken {
484    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
485        f.debug_struct("IdToken")
486            .field("raw_len", &self.raw.len())
487            .field("claims", &self.claims)
488            .finish()
489    }
490}
491
492// =============================================================================
493// Discovery Document
494// =============================================================================
495
496/// OpenID Connect Discovery Document.
497///
498/// This is served at `/.well-known/openid-configuration`.
499#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
500pub struct DiscoveryDocument {
501    /// Issuer identifier URL.
502    pub issuer: String,
503    /// Authorization endpoint URL.
504    pub authorization_endpoint: String,
505    /// Token endpoint URL.
506    pub token_endpoint: String,
507    /// UserInfo endpoint URL.
508    #[serde(skip_serializing_if = "Option::is_none")]
509    pub userinfo_endpoint: Option<String>,
510    /// JWKs URI for public key retrieval.
511    #[serde(skip_serializing_if = "Option::is_none")]
512    pub jwks_uri: Option<String>,
513    /// Registration endpoint URL.
514    #[serde(skip_serializing_if = "Option::is_none")]
515    pub registration_endpoint: Option<String>,
516    /// Revocation endpoint URL.
517    #[serde(skip_serializing_if = "Option::is_none")]
518    pub revocation_endpoint: Option<String>,
519    /// Supported scopes.
520    pub scopes_supported: Vec<String>,
521    /// Supported response types.
522    pub response_types_supported: Vec<String>,
523    /// Supported response modes.
524    #[serde(skip_serializing_if = "Option::is_none")]
525    pub response_modes_supported: Option<Vec<String>>,
526    /// Supported grant types.
527    pub grant_types_supported: Vec<String>,
528    /// Supported subject types.
529    pub subject_types_supported: Vec<String>,
530    /// Supported ID token signing algorithms.
531    pub id_token_signing_alg_values_supported: Vec<String>,
532    /// Supported token endpoint auth methods.
533    pub token_endpoint_auth_methods_supported: Vec<String>,
534    /// Supported claims.
535    #[serde(skip_serializing_if = "Option::is_none")]
536    pub claims_supported: Option<Vec<String>>,
537    /// Supported code challenge methods.
538    #[serde(skip_serializing_if = "Option::is_none")]
539    pub code_challenge_methods_supported: Option<Vec<String>>,
540}
541
542impl DiscoveryDocument {
543    /// Creates a new discovery document with the given issuer and base URL.
544    #[must_use]
545    pub fn new(issuer: impl Into<String>, base_url: impl Into<String>) -> Self {
546        let issuer = issuer.into();
547        let base = base_url.into();
548
549        Self {
550            issuer: issuer.clone(),
551            authorization_endpoint: format!("{}/authorize", base),
552            token_endpoint: format!("{}/token", base),
553            userinfo_endpoint: Some(format!("{}/userinfo", base)),
554            jwks_uri: None,
555            registration_endpoint: None,
556            revocation_endpoint: Some(format!("{}/revoke", base)),
557            scopes_supported: vec![
558                "openid".to_string(),
559                "profile".to_string(),
560                "email".to_string(),
561            ],
562            response_types_supported: vec!["code".to_string()],
563            response_modes_supported: Some(vec!["query".to_string()]),
564            grant_types_supported: vec![
565                "authorization_code".to_string(),
566                "refresh_token".to_string(),
567            ],
568            subject_types_supported: vec!["public".to_string()],
569            id_token_signing_alg_values_supported: Vec::new(),
570            token_endpoint_auth_methods_supported: vec![
571                "client_secret_post".to_string(),
572                "client_secret_basic".to_string(),
573            ],
574            claims_supported: Some(vec![
575                "sub".to_string(),
576                "iss".to_string(),
577                "aud".to_string(),
578                "exp".to_string(),
579                "iat".to_string(),
580                "name".to_string(),
581                "email".to_string(),
582                "email_verified".to_string(),
583                "preferred_username".to_string(),
584                "picture".to_string(),
585            ]),
586            code_challenge_methods_supported: Some(vec!["S256".to_string()]),
587        }
588    }
589}
590
591// =============================================================================
592// Claims Provider
593// =============================================================================
594
595/// Trait for providing user claims.
596pub trait ClaimsProvider: Send + Sync {
597    /// Retrieves claims for a user by subject identifier.
598    ///
599    /// Returns `None` if the user is not found.
600    fn get_claims(&self, subject: &str) -> Option<UserClaims>;
601}
602
603/// Simple in-memory claims provider.
604#[derive(Default)]
605pub struct InMemoryClaimsProvider {
606    claims: RwLock<HashMap<String, UserClaims>>,
607}
608
609impl std::fmt::Debug for InMemoryClaimsProvider {
610    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
611        let claim_count = self.claims.try_read().ok().map(|claims| claims.len());
612        f.debug_struct("InMemoryClaimsProvider")
613            .field("claim_count", &claim_count)
614            .finish()
615    }
616}
617
618impl InMemoryClaimsProvider {
619    /// Creates a new empty claims provider.
620    #[must_use]
621    pub fn new() -> Self {
622        Self::default()
623    }
624
625    /// Adds or updates claims for a user.
626    pub fn set_claims(&self, claims: UserClaims) {
627        if let Ok(mut guard) = self.claims.write() {
628            guard.insert(claims.sub.clone(), claims);
629        }
630    }
631
632    /// Removes claims for a user.
633    pub fn remove_claims(&self, subject: &str) {
634        if let Ok(mut guard) = self.claims.write() {
635            guard.remove(subject);
636        }
637    }
638}
639
640impl ClaimsProvider for InMemoryClaimsProvider {
641    fn get_claims(&self, subject: &str) -> Option<UserClaims> {
642        self.claims
643            .read()
644            .ok()
645            .and_then(|guard| guard.get(subject).cloned())
646    }
647}
648
649/// Function-based claims provider.
650pub struct FnClaimsProvider<F>
651where
652    F: Fn(&str) -> Option<UserClaims> + Send + Sync,
653{
654    func: F,
655}
656
657impl<F> FnClaimsProvider<F>
658where
659    F: Fn(&str) -> Option<UserClaims> + Send + Sync,
660{
661    /// Creates a new function-based claims provider.
662    #[must_use]
663    pub fn new(func: F) -> Self {
664        Self { func }
665    }
666}
667
668impl<F> ClaimsProvider for FnClaimsProvider<F>
669where
670    F: Fn(&str) -> Option<UserClaims> + Send + Sync,
671{
672    fn get_claims(&self, subject: &str) -> Option<UserClaims> {
673        (self.func)(subject)
674    }
675}
676
677impl ClaimsProvider for Arc<dyn ClaimsProvider> {
678    fn get_claims(&self, subject: &str) -> Option<UserClaims> {
679        (**self).get_claims(subject)
680    }
681}
682
683// =============================================================================
684// OIDC Errors
685// =============================================================================
686
687/// OIDC-specific errors.
688pub enum OidcError {
689    /// Underlying OAuth error.
690    OAuth(OAuthError),
691    /// Missing openid scope.
692    MissingOpenIdScope,
693    /// User claims not found.
694    ClaimsNotFound(String),
695    /// Claims provider returned an identity other than the requested subject.
696    ClaimsSubjectMismatch,
697    /// Token signing failed.
698    SigningError(String),
699    /// An external signer outcome, including its dispatch knowledge.
700    #[cfg(feature = "builtin-auth-server")]
701    ExternalSigning(JwsSigningError),
702    /// Invalid ID token.
703    InvalidIdToken(String),
704}
705
706impl std::fmt::Debug for OidcError {
707    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
708        match self {
709            Self::OAuth(error) => f.debug_tuple("OAuth").field(error).finish(),
710            Self::MissingOpenIdScope => f.write_str("MissingOpenIdScope"),
711            Self::ClaimsNotFound(subject) => f
712                .debug_struct("ClaimsNotFound")
713                .field("subject_len", &subject.len())
714                .finish(),
715            Self::ClaimsSubjectMismatch => f.write_str("ClaimsSubjectMismatch"),
716            Self::SigningError(description) => f
717                .debug_struct("SigningError")
718                .field("description_len", &description.len())
719                .finish(),
720            #[cfg(feature = "builtin-auth-server")]
721            Self::ExternalSigning(error) => f.debug_tuple("ExternalSigning").field(error).finish(),
722            Self::InvalidIdToken(description) => f
723                .debug_struct("InvalidIdToken")
724                .field("description_len", &description.len())
725                .finish(),
726        }
727    }
728}
729
730impl std::fmt::Display for OidcError {
731    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
732        match self {
733            Self::OAuth(e) => write!(f, "OAuth error: {}", e),
734            Self::MissingOpenIdScope => write!(f, "missing 'openid' scope"),
735            Self::ClaimsNotFound(_) => f.write_str("claims not found for requested subject"),
736            Self::ClaimsSubjectMismatch => {
737                write!(f, "claims provider returned a mismatched subject")
738            }
739            Self::SigningError(_) => f.write_str("ID token signing failed"),
740            #[cfg(feature = "builtin-auth-server")]
741            Self::ExternalSigning(error) => write!(f, "external ID token signing failed: {error}"),
742            Self::InvalidIdToken(_) => f.write_str("invalid ID token"),
743        }
744    }
745}
746
747impl std::error::Error for OidcError {}
748
749impl From<OAuthError> for OidcError {
750    fn from(err: OAuthError) -> Self {
751        Self::OAuth(err)
752    }
753}
754
755// =============================================================================
756// OIDC Provider
757// =============================================================================
758
759/// OpenID Connect Provider.
760///
761/// This extends the OAuth server with OIDC identity features.
762pub struct OidcProvider {
763    /// Underlying OAuth server.
764    oauth: Arc<OAuthServer>,
765    /// OIDC configuration.
766    config: OidcProviderConfig,
767    /// Claims provider.
768    claims_provider: RwLock<Option<Arc<dyn ClaimsProvider>>>,
769    /// A non-forgeable external-signer activation. It remains absent until a
770    /// caller supplies an exact publication/read-back of the selected public
771    /// JWKS under the same immutable signer binding.
772    #[cfg(feature = "builtin-auth-server")]
773    signing_activation: RwLock<OidcIdTokenSigningState>,
774    /// The embedding-owned verifier and durable CAS store.  There is no
775    /// default: a missing dependency leaves activation fail-closed.
776    #[cfg(feature = "builtin-auth-server")]
777    signing_activation_dependencies: RwLock<Option<OidcSigningActivationDependencies>>,
778}
779
780/// Bounded, embedding-owned fetcher for every public JWKS endpoint bound to an
781/// OIDC signer activation.
782///
783/// This is deliberately an external effect boundary.  Native route handlers
784/// only serve bytes; they cannot mint a read-back record by parsing their own
785/// in-memory publication buffer.
786#[cfg(feature = "builtin-auth-server")]
787pub trait OidcJwksReadBackVerifier: Send + Sync + 'static {
788    /// Fetches every supplied exact URI and returns the observed URI, origin,
789    /// response bytes, and monotonic publication generation.
790    fn read_back<'a>(
791        &'a self,
792        cx: &'a Cx,
793        endpoints: &'a [String],
794    ) -> Pin<Box<dyn Future<Output = Result<Vec<JwksEndpointReadBack>, OidcError>> + Send + 'a>>;
795}
796
797/// Persistent compare-and-set record for an OIDC activation generation.
798///
799/// A store implementation must retain this record across process replacement.
800/// The provider always reconstructs the cryptographic receipt through a fresh
801/// external read-back; a stored generation alone never restores `Active`.
802#[cfg(feature = "builtin-auth-server")]
803#[derive(Debug, Clone, PartialEq, Eq)]
804pub struct OidcSigningActivationStoreRecord {
805    issuer: String,
806    key_ring_generation: u64,
807    activation_generation: u64,
808    status: OidcSigningActivationStatus,
809    maximum_id_token_expires_at: i64,
810    key_id_maximum_id_token_expires_at: BTreeMap<String, OidcSigningKeyExpiry>,
811}
812
813/// One bounded, canonical public-key identity and its greatest issued
814/// ID-token expiry. The identity contains public JWKS bytes only; it never
815/// carries private key material or a signer backend.
816#[cfg(feature = "builtin-auth-server")]
817#[derive(Debug, Clone, PartialEq, Eq)]
818pub struct OidcSigningKeyExpiry {
819    expires_at: i64,
820    canonical_public_key_identity: Vec<u8>,
821}
822
823#[cfg(feature = "builtin-auth-server")]
824impl OidcSigningKeyExpiry {
825    /// Reconstructs a bounded public-key expiry watermark for an
826    /// embedding-owned durable store.
827    pub fn new(expires_at: i64, canonical_public_key_identity: Vec<u8>) -> Result<Self, OidcError> {
828        if expires_at < 0
829            || canonical_public_key_identity.is_empty()
830            || canonical_public_key_identity.len() > MAX_JWKS_BYTES
831        {
832            return Err(OidcError::SigningError(
833                "OIDC durable key expiry watermark is outside bounded admission".to_string(),
834            ));
835        }
836        Ok(Self {
837            expires_at,
838            canonical_public_key_identity,
839        })
840    }
841
842    /// Greatest ID-token expiry bound to this exact public key.
843    #[must_use]
844    pub const fn expires_at(&self) -> i64 {
845        self.expires_at
846    }
847
848    /// Exact canonical public JWKS bytes for this one key.
849    #[must_use]
850    pub fn canonical_public_key_identity(&self) -> &[u8] {
851        &self.canonical_public_key_identity
852    }
853}
854
855/// Durable status for one profile-specific OIDC signing activation.
856///
857/// Any status or generation transition must advance the durable activation
858/// generation. Issuers use this to fence stale process-local activation state
859/// before and after external signing.
860#[cfg(feature = "builtin-auth-server")]
861#[derive(Debug, Clone, Copy, PartialEq, Eq)]
862pub enum OidcSigningActivationStatus {
863    /// The record authorizes issuance for its exact key-ring generation.
864    Active,
865    /// The record has closed issuance while retaining public verification
866    /// material until its durable maximum ID-token expiry.
867    Retiring,
868    /// The record has been explicitly revoked; no local activation may issue.
869    Revoked,
870}
871
872#[cfg(feature = "builtin-auth-server")]
873impl OidcSigningActivationStoreRecord {
874    /// Reconstructs one bounded durable record supplied by an embedding-owned
875    /// store. This is the only public constructor; field mutation is not
876    /// possible after validation.
877    pub fn new(
878        issuer: impl Into<String>,
879        key_ring_generation: u64,
880        activation_generation: u64,
881        status: OidcSigningActivationStatus,
882        maximum_id_token_expires_at: i64,
883        key_id_maximum_id_token_expires_at: BTreeMap<String, OidcSigningKeyExpiry>,
884    ) -> Result<Self, OidcError> {
885        let issuer = issuer.into();
886        if issuer.is_empty()
887            || issuer.len() > crate::oauth::MAX_OAUTH_ISSUER_BYTES
888            || issuer.bytes().any(|byte| byte.is_ascii_control())
889            || key_ring_generation == 0
890            || activation_generation == 0
891            || maximum_id_token_expires_at < 0
892            || key_id_maximum_id_token_expires_at.len() > MAX_JWKS_KEYS
893            || key_id_maximum_id_token_expires_at
894                .iter()
895                .any(|(key_id, watermark)| {
896                    key_id.is_empty()
897                        || key_id.len() > MAX_KID_BYTES
898                        || watermark.expires_at() > maximum_id_token_expires_at
899                        || !canonical_oidc_key_identity_matches(
900                            key_id,
901                            watermark.canonical_public_key_identity(),
902                        )
903                })
904            || key_id_maximum_id_token_expires_at
905                .values()
906                .try_fold(0usize, |total, watermark| {
907                    total.checked_add(watermark.canonical_public_key_identity().len())
908                })
909                .is_none_or(|total| total > MAX_JWKS_BYTES)
910        {
911            return Err(OidcError::SigningError(
912                "OIDC activation store record is outside bounded admission".to_string(),
913            ));
914        }
915        Ok(Self {
916            issuer,
917            key_ring_generation,
918            activation_generation,
919            status,
920            maximum_id_token_expires_at,
921            key_id_maximum_id_token_expires_at,
922        })
923    }
924
925    /// Exact issuer bound to the CAS record.
926    #[must_use]
927    pub fn issuer(&self) -> &str {
928        &self.issuer
929    }
930
931    /// Key-ring generation admitted by the record.
932    #[must_use]
933    pub const fn key_ring_generation(&self) -> u64 {
934        self.key_ring_generation
935    }
936
937    /// Monotonic CAS generation assigned by the store.
938    #[must_use]
939    pub const fn activation_generation(&self) -> u64 {
940        self.activation_generation
941    }
942
943    /// Durable status used to fence issuance across provider instances.
944    #[must_use]
945    pub const fn status(&self) -> OidcSigningActivationStatus {
946        self.status
947    }
948
949    /// Greatest ID-token expiry durably committed for this key-ring generation.
950    #[must_use]
951    pub const fn maximum_id_token_expires_at(&self) -> i64 {
952        self.maximum_id_token_expires_at
953    }
954
955    /// Ordered per-key expiry watermarks durably carried across process
956    /// replacement. Each entry is bounded and cannot exceed the aggregate
957    /// maximum recorded above.
958    #[must_use]
959    pub fn key_id_maximum_id_token_expires_at(&self) -> &BTreeMap<String, OidcSigningKeyExpiry> {
960        &self.key_id_maximum_id_token_expires_at
961    }
962}
963
964/// Bounded durable CAS boundary for profile-specific OIDC signer activation.
965///
966/// Absence, a failed read, or a lost race is a fail-closed activation failure.
967/// Implementations own durability, restore fencing, and process-instance
968/// coordination; FastMCP deliberately supplies no implicit local persistence.
969#[cfg(feature = "builtin-auth-server")]
970pub trait OidcSigningActivationStore: Send + Sync + 'static {
971    /// Reads the current record for the exact issuer.
972    fn load(
973        &self,
974        cx: &Cx,
975        issuer: &str,
976    ) -> Result<Option<OidcSigningActivationStoreRecord>, OidcError>;
977
978    /// Atomically writes `next` only if the currently stored activation
979    /// generation equals `expected_generation` (`None` means no record).
980    fn compare_and_set(
981        &self,
982        cx: &Cx,
983        expected_generation: Option<u64>,
984        next: OidcSigningActivationStoreRecord,
985    ) -> Result<OidcSigningActivationStoreRecord, OidcError>;
986}
987
988#[cfg(feature = "builtin-auth-server")]
989#[derive(Clone)]
990struct OidcSigningActivationDependencies {
991    verifier: Arc<dyn OidcJwksReadBackVerifier>,
992    store: Arc<dyn OidcSigningActivationStore>,
993}
994
995/// Fail-closed state of the current process's ID-token signer activation.
996///
997/// Process restart deliberately returns to [`Self::Inactive`]; this server
998/// does not deserialize an `Active` state from unverified storage. An embedding
999/// must repeat publication and the public-endpoint read-back before signing can
1000/// resume.
1001#[cfg(feature = "builtin-auth-server")]
1002enum OidcIdTokenSigningState {
1003    /// No signer has been admitted for this issuer.
1004    Inactive,
1005    /// A signer and exact public endpoint are bound, but no bytes are public.
1006    Pending(OidcIdTokenSigningPending),
1007    /// Canonical public bytes are ready at the configured endpoint, but an
1008    /// endpoint read-back has not yet been observed and verified.
1009    Published(OidcIdTokenSigningPublished),
1010    /// The signer completed a canary that verified against endpoint read-back.
1011    Active(OidcIdTokenSigningActivation),
1012    /// An Active signer continues issuing while a successor ring progresses
1013    /// through Pending then Published read-back. The successor JWKS must
1014    /// retain the active key before it can replace this activation.
1015    Rotating {
1016        active: OidcIdTokenSigningActivation,
1017        successor: OidcIdTokenSigningSuccessor,
1018    },
1019    /// The published verification key remains visible, but issuance is closed
1020    /// after the issuer's recorded maximum token lifetime.
1021    Retiring(OidcIdTokenSigningActivation),
1022}
1023
1024/// A signer/key/endpoint binding that cannot yet issue ID tokens.
1025#[cfg(feature = "builtin-auth-server")]
1026struct OidcIdTokenSigningPending {
1027    key_ring: Rs256PublicKeyRing,
1028    binding: Rs256SigningBinding,
1029    issuer: String,
1030    advertised_jwks_uris: Vec<String>,
1031    advertised_jwks_origins: Vec<String>,
1032    canonical_jwks: CanonicalRs256PublicJwksSet,
1033    dependencies: Option<OidcSigningActivationDependencies>,
1034}
1035
1036/// A pending signer whose exact canonical public JWKS is available to the
1037/// configured public endpoint.
1038#[cfg(feature = "builtin-auth-server")]
1039struct OidcIdTokenSigningPublished {
1040    pending: OidcIdTokenSigningPending,
1041    published_jwks: Vec<u8>,
1042    publication_generation: u64,
1043}
1044
1045/// A server-held active signer for one externally custodied ID-token key.
1046///
1047/// Its fields are private and it has no public constructor. It can only be
1048/// created after the selected signer's canonical public JWKS was served by the
1049/// exact public endpoint and a signer-produced canary verified through those
1050/// read-back public keys. This type neither implements a KMS/HSM adapter nor
1051/// contains private material.
1052#[cfg(feature = "builtin-auth-server")]
1053struct OidcIdTokenSigningActivation {
1054    published: OidcIdTokenSigningPublished,
1055    receipt: SigningActivationReceipt,
1056    activation_generation: u64,
1057    maximum_id_token_expires_at: i64,
1058    /// Per-key expiry watermarks let a successor retain every verification
1059    /// key that still protects an issued ID token while permitting keys whose
1060    /// artifacts have all expired to leave a later ring.
1061    live_key_expiries: BTreeMap<String, OidcSigningKeyExpiry>,
1062}
1063
1064#[cfg(feature = "builtin-auth-server")]
1065enum OidcIdTokenSigningSuccessor {
1066    Pending(OidcIdTokenSigningPending),
1067    Published(OidcIdTokenSigningPublished),
1068}
1069
1070#[cfg(feature = "builtin-auth-server")]
1071impl OidcIdTokenSigningState {
1072    fn published(&self) -> Option<&OidcIdTokenSigningPublished> {
1073        match self {
1074            Self::Published(published) => Some(published),
1075            Self::Active(active) => Some(&active.published),
1076            Self::Rotating {
1077                active,
1078                successor: OidcIdTokenSigningSuccessor::Pending(_),
1079            } => Some(&active.published),
1080            Self::Rotating {
1081                successor: OidcIdTokenSigningSuccessor::Published(published),
1082                ..
1083            } => Some(published),
1084            Self::Retiring(retiring) => Some(&retiring.published),
1085            Self::Inactive | Self::Pending(_) => None,
1086        }
1087    }
1088
1089    fn active(&self) -> Option<&OidcIdTokenSigningActivation> {
1090        match self {
1091            Self::Active(active) => Some(active),
1092            Self::Rotating { active, .. } => Some(active),
1093            Self::Inactive | Self::Pending(_) | Self::Published(_) | Self::Retiring(_) => None,
1094        }
1095    }
1096}
1097
1098#[cfg(feature = "builtin-auth-server")]
1099impl std::fmt::Debug for OidcIdTokenSigningState {
1100    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1101        match self {
1102            Self::Inactive => formatter.write_str("OidcIdTokenSigningState::Inactive"),
1103            Self::Pending(_) => formatter.write_str("OidcIdTokenSigningState::Pending"),
1104            Self::Published(_) => formatter.write_str("OidcIdTokenSigningState::Published"),
1105            Self::Active(_) => formatter.write_str("OidcIdTokenSigningState::Active"),
1106            Self::Rotating { .. } => formatter.write_str("OidcIdTokenSigningState::Rotating"),
1107            Self::Retiring(_) => formatter.write_str("OidcIdTokenSigningState::Retiring"),
1108        }
1109    }
1110}
1111
1112#[cfg(feature = "builtin-auth-server")]
1113impl std::fmt::Debug for OidcIdTokenSigningActivation {
1114    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1115        let published = &self.published;
1116        formatter
1117            .debug_struct("OidcIdTokenSigningActivation")
1118            .field("binding", &published.pending.binding)
1119            .field("issuer_bytes", &published.pending.issuer.len())
1120            .field(
1121                "advertised_jwks_uri_bytes",
1122                &published
1123                    .pending
1124                    .advertised_jwks_uris
1125                    .iter()
1126                    .map(String::len)
1127                    .sum::<usize>(),
1128            )
1129            .field(
1130                "advertised_jwks_origin_bytes",
1131                &published
1132                    .pending
1133                    .advertised_jwks_origins
1134                    .iter()
1135                    .map(String::len)
1136                    .sum::<usize>(),
1137            )
1138            .field("published_jwks_bytes", &published.published_jwks.len())
1139            .field(
1140                "key_ring_generation",
1141                &published.pending.key_ring.generation(),
1142            )
1143            .field("publication_generation", &published.publication_generation)
1144            .field("activation_generation", &self.activation_generation)
1145            .finish()
1146    }
1147}
1148
1149fn validate_oidc_config(config: &OidcProviderConfig) -> Result<(), OidcError> {
1150    // Reuse the OAuth issuer admission policy so OIDC discovery cannot publish
1151    // a non-HTTPS, non-canonical, or otherwise unsafe issuer spelling.
1152    validate_oauth_issuer(&config.issuer).map_err(OidcError::from)
1153}
1154
1155impl OidcProvider {
1156    /// Creates a new OIDC provider with the given OAuth server.
1157    ///
1158    /// # Errors
1159    ///
1160    /// Returns an error when the OIDC issuer is unsafe or does not exactly
1161    /// match the underlying OAuth server issuer.
1162    pub fn new(oauth: Arc<OAuthServer>, config: OidcProviderConfig) -> Result<Self, OidcError> {
1163        validate_oidc_config(&config)?;
1164        if config.issuer != oauth.config().issuer {
1165            return Err(OidcError::OAuth(OAuthError::ServerError(
1166                "OIDC issuer must exactly match the OAuth server issuer".to_string(),
1167            )));
1168        }
1169        Ok(Self {
1170            oauth,
1171            config,
1172            claims_provider: RwLock::new(None),
1173            #[cfg(feature = "builtin-auth-server")]
1174            signing_activation: RwLock::new(OidcIdTokenSigningState::Inactive),
1175            #[cfg(feature = "builtin-auth-server")]
1176            signing_activation_dependencies: RwLock::new(None),
1177        })
1178    }
1179
1180    /// Creates a new OIDC provider with default configuration.
1181    ///
1182    /// # Errors
1183    ///
1184    /// Returns an error when the underlying OAuth server has an unsafe issuer.
1185    pub fn with_defaults(oauth: Arc<OAuthServer>) -> Result<Self, OidcError> {
1186        let config = OidcProviderConfig {
1187            issuer: oauth.config().issuer.clone(),
1188            ..OidcProviderConfig::default()
1189        };
1190        Self::new(oauth, config)
1191    }
1192
1193    /// Returns the OIDC configuration.
1194    #[must_use]
1195    pub fn config(&self) -> &OidcProviderConfig {
1196        &self.config
1197    }
1198
1199    /// Returns a reference to the underlying OAuth server.
1200    #[must_use]
1201    pub fn oauth(&self) -> &Arc<OAuthServer> {
1202        &self.oauth
1203    }
1204
1205    /// Sets the claims provider.
1206    pub fn set_claims_provider<P: ClaimsProvider + 'static>(&self, provider: P) {
1207        if let Ok(mut guard) = self.claims_provider.write() {
1208            *guard = Some(Arc::new(provider));
1209        }
1210    }
1211
1212    /// Sets a function-based claims provider.
1213    pub fn set_claims_fn<F>(&self, func: F)
1214    where
1215        F: Fn(&str) -> Option<UserClaims> + Send + Sync + 'static,
1216    {
1217        self.set_claims_provider(FnClaimsProvider::new(func));
1218    }
1219
1220    /// Configures the explicit external read-back and durable-CAS boundaries
1221    /// required before this provider can activate an ID-token signer.
1222    #[cfg(feature = "builtin-auth-server")]
1223    pub fn set_id_token_signing_activation_dependencies(
1224        &self,
1225        verifier: Arc<dyn OidcJwksReadBackVerifier>,
1226        store: Arc<dyn OidcSigningActivationStore>,
1227    ) -> Result<(), OidcError> {
1228        let mut dependencies = self.signing_activation_dependencies.write().map_err(|_| {
1229            OidcError::SigningError(
1230                "OIDC signing activation dependencies are unavailable".to_string(),
1231            )
1232        })?;
1233        if dependencies.is_some() {
1234            return Err(OidcError::SigningError(
1235                "OIDC signing activation dependencies are already configured".to_string(),
1236            ));
1237        }
1238        *dependencies = Some(OidcSigningActivationDependencies { verifier, store });
1239        Ok(())
1240    }
1241
1242    /// Starts a sealed Pending activation for one externally custodied signer.
1243    ///
1244    /// This is intentionally not enough to issue tokens. The caller must next
1245    /// publish the exact canonical bytes through the configured public route,
1246    /// let that route observe a read-back, and then activate its canary.
1247    #[cfg(feature = "builtin-auth-server")]
1248    pub fn begin_id_token_signing_activation(
1249        &self,
1250        signer: Arc<ExternalRs256Signer>,
1251        advertised_jwks_uri: &str,
1252    ) -> Result<(), OidcError> {
1253        let signer_ring_generation = signer.binding().ring_generation();
1254        let key_ring = Rs256PublicKeyRing::new(signer, Vec::new(), signer_ring_generation)
1255            .map_err(|_| {
1256                OidcError::SigningError("unable to admit OIDC signer key ring".to_string())
1257            })?;
1258        self.begin_id_token_signing_key_ring_activation(
1259            key_ring,
1260            vec![advertised_jwks_uri.to_string()],
1261        )
1262    }
1263
1264    /// Starts Pending activation for a selected active signer plus retained
1265    /// verification overlap.  Every endpoint is independently bound and must
1266    /// later appear in the external verifier's receipt.
1267    #[cfg(feature = "builtin-auth-server")]
1268    pub fn begin_id_token_signing_key_ring_activation(
1269        &self,
1270        key_ring: Rs256PublicKeyRing,
1271        advertised_jwks_uris: Vec<String>,
1272    ) -> Result<(), OidcError> {
1273        if advertised_jwks_uris.is_empty() {
1274            return Err(OidcError::SigningError(
1275                "OIDC signer activation requires at least one advertised JWKS endpoint".to_string(),
1276            ));
1277        }
1278        let mut admitted_uris = Vec::with_capacity(advertised_jwks_uris.len());
1279        let mut admitted_origins = Vec::with_capacity(advertised_jwks_uris.len());
1280        for uri in advertised_jwks_uris {
1281            let (uri, origin) = validate_advertised_jwks_uri(&self.config.issuer, &uri)?;
1282            if admitted_uris.iter().any(|existing| existing == &uri) {
1283                return Err(OidcError::SigningError(
1284                    "OIDC signer activation has duplicate advertised JWKS endpoints".to_string(),
1285                ));
1286            }
1287            admitted_uris.push(uri);
1288            admitted_origins.push(origin);
1289        }
1290        let canonical_jwks = key_ring.canonical_public_jwks().map_err(|_| {
1291            OidcError::SigningError("unable to canonicalize signing JWKS".to_string())
1292        })?;
1293        let dependencies = self
1294            .signing_activation_dependencies
1295            .read()
1296            .map_err(|_| {
1297                OidcError::SigningError(
1298                    "OIDC signing activation dependencies are unavailable".to_string(),
1299                )
1300            })?
1301            .clone();
1302        let mut slot = self.signing_activation.write().map_err(|_| {
1303            OidcError::SigningError("OIDC signing activation state is unavailable".to_string())
1304        })?;
1305        if !matches!(*slot, OidcIdTokenSigningState::Inactive) {
1306            return Err(OidcError::SigningError(
1307                "OIDC signing activation is already in progress or active".to_string(),
1308            ));
1309        }
1310        *slot = OidcIdTokenSigningState::Pending(OidcIdTokenSigningPending {
1311            binding: key_ring.active_signer().binding(),
1312            key_ring,
1313            issuer: self.config.issuer.clone(),
1314            advertised_jwks_uris: admitted_uris,
1315            advertised_jwks_origins: admitted_origins,
1316            canonical_jwks,
1317            dependencies,
1318        });
1319        Ok(())
1320    }
1321
1322    /// Begins a live successor transition without withdrawing the currently
1323    /// Active signer. The successor ring must retain the exact active signer
1324    /// as a public verification key and advance the key-ring generation.
1325    #[cfg(feature = "builtin-auth-server")]
1326    pub fn begin_id_token_signing_key_ring_rotation(
1327        &self,
1328        key_ring: Rs256PublicKeyRing,
1329        advertised_jwks_uris: Vec<String>,
1330    ) -> Result<(), OidcError> {
1331        if advertised_jwks_uris.is_empty() {
1332            return Err(OidcError::SigningError(
1333                "OIDC signer rotation requires at least one advertised JWKS endpoint".to_string(),
1334            ));
1335        }
1336        let mut admitted_uris = Vec::with_capacity(advertised_jwks_uris.len());
1337        let mut admitted_origins = Vec::with_capacity(advertised_jwks_uris.len());
1338        for uri in advertised_jwks_uris {
1339            let (uri, origin) = validate_advertised_jwks_uri(&self.config.issuer, &uri)?;
1340            if admitted_uris.iter().any(|existing| existing == &uri) {
1341                return Err(OidcError::SigningError(
1342                    "OIDC signer rotation has duplicate advertised JWKS endpoints".to_string(),
1343                ));
1344            }
1345            admitted_uris.push(uri);
1346            admitted_origins.push(origin);
1347        }
1348        let canonical_jwks = key_ring.canonical_public_jwks().map_err(|_| {
1349            OidcError::SigningError("unable to canonicalize successor signing JWKS".to_string())
1350        })?;
1351        let successor_identities = oidc_key_ring_public_identities(&key_ring)?;
1352        let now = oidc_unix_timestamp()?;
1353        let dependencies = self
1354            .signing_activation_dependencies
1355            .read()
1356            .map_err(|_| {
1357                OidcError::SigningError(
1358                    "OIDC signing activation dependencies are unavailable".to_string(),
1359                )
1360            })?
1361            .clone();
1362        let mut slot = self.signing_activation.write().map_err(|_| {
1363            OidcError::SigningError("OIDC signing activation state is unavailable".to_string())
1364        })?;
1365        let state = std::mem::replace(&mut *slot, OidcIdTokenSigningState::Inactive);
1366        let OidcIdTokenSigningState::Active(mut active) = state else {
1367            *slot = state;
1368            return Err(OidcError::SigningError(
1369                "OIDC signer rotation requires an Active generation".to_string(),
1370            ));
1371        };
1372        let current = active.published.pending.key_ring.active_signer();
1373        let drops_live_key = active.live_key_expiries.iter().any(|(key_id, watermark)| {
1374            (key_id == current.key_id() || watermark.expires_at() > now)
1375                && (!key_ring.retains_key_from(&active.published.pending.key_ring, key_id)
1376                    || successor_identities.get(key_id).is_none_or(|identity| {
1377                        identity.as_slice() != watermark.canonical_public_key_identity()
1378                    }))
1379        });
1380        if key_ring.generation() <= active.published.pending.key_ring.generation()
1381            || key_ring.active_signer().binding() == current.binding()
1382            || drops_live_key
1383        {
1384            *slot = OidcIdTokenSigningState::Active(active);
1385            return Err(OidcError::SigningError(
1386                "OIDC successor ring must advance and retain every live public key".to_string(),
1387            ));
1388        }
1389        let pending = OidcIdTokenSigningPending {
1390            binding: key_ring.active_signer().binding(),
1391            key_ring,
1392            issuer: self.config.issuer.clone(),
1393            advertised_jwks_uris: admitted_uris,
1394            advertised_jwks_origins: admitted_origins,
1395            canonical_jwks,
1396            dependencies,
1397        };
1398        *slot = OidcIdTokenSigningState::Rotating {
1399            active,
1400            successor: OidcIdTokenSigningSuccessor::Pending(pending),
1401        };
1402        Ok(())
1403    }
1404
1405    /// Publishes the exact signer-bound canonical JWKS to this provider's
1406    /// public endpoint state. Copied or stale bytes leave the state Pending.
1407    #[cfg(feature = "builtin-auth-server")]
1408    pub fn publish_id_token_signing_jwks(
1409        &self,
1410        published_jwks: CanonicalRs256PublicJwks,
1411    ) -> Result<(), OidcError> {
1412        let mut slot = self.signing_activation.write().map_err(|_| {
1413            OidcError::SigningError("OIDC signing activation state is unavailable".to_string())
1414        })?;
1415        let state = std::mem::replace(&mut *slot, OidcIdTokenSigningState::Inactive);
1416        let OidcIdTokenSigningState::Pending(pending) = state else {
1417            *slot = state;
1418            return Err(OidcError::SigningError(
1419                "OIDC signing JWKS publication requires a Pending activation".to_string(),
1420            ));
1421        };
1422        if published_jwks.binding() != pending.binding
1423            || published_jwks.as_bytes() != pending.canonical_jwks.as_bytes()
1424        {
1425            *slot = OidcIdTokenSigningState::Pending(pending);
1426            return Err(OidcError::SigningError(
1427                "OIDC signing JWKS publication does not match the pending signer binding"
1428                    .to_string(),
1429            ));
1430        }
1431        *slot = OidcIdTokenSigningState::Published(OidcIdTokenSigningPublished {
1432            published_jwks: published_jwks.as_bytes().to_vec(),
1433            pending,
1434            publication_generation: published_jwks.binding().ring_generation(),
1435        });
1436        Ok(())
1437    }
1438
1439    /// Publishes the exact canonical active-plus-retained JWKS for a key-ring
1440    /// activation.  This is the rotation-capable publication entry point.
1441    #[cfg(feature = "builtin-auth-server")]
1442    pub fn publish_id_token_signing_key_ring_jwks(
1443        &self,
1444        published_jwks: CanonicalRs256PublicJwksSet,
1445    ) -> Result<(), OidcError> {
1446        let mut slot = self.signing_activation.write().map_err(|_| {
1447            OidcError::SigningError("OIDC signing activation state is unavailable".to_string())
1448        })?;
1449        let state = std::mem::replace(&mut *slot, OidcIdTokenSigningState::Inactive);
1450        let (pending, active) = match state {
1451            OidcIdTokenSigningState::Pending(pending) => (pending, None),
1452            OidcIdTokenSigningState::Rotating {
1453                active,
1454                successor: OidcIdTokenSigningSuccessor::Pending(pending),
1455            } => (pending, Some(active)),
1456            state => {
1457                *slot = state;
1458                return Err(OidcError::SigningError(
1459                    "OIDC signing JWKS publication requires a Pending activation".to_string(),
1460                ));
1461            }
1462        };
1463        if published_jwks.generation() != pending.key_ring.generation()
1464            || published_jwks.as_bytes() != pending.canonical_jwks.as_bytes()
1465        {
1466            *slot = match active {
1467                Some(active) => OidcIdTokenSigningState::Rotating {
1468                    active,
1469                    successor: OidcIdTokenSigningSuccessor::Pending(pending),
1470                },
1471                None => OidcIdTokenSigningState::Pending(pending),
1472            };
1473            return Err(OidcError::SigningError(
1474                "OIDC signing JWKS publication does not match the pending key ring".to_string(),
1475            ));
1476        }
1477        let published = OidcIdTokenSigningPublished {
1478            published_jwks: published_jwks.as_bytes().to_vec(),
1479            publication_generation: published_jwks.generation(),
1480            pending,
1481        };
1482        *slot = match active {
1483            Some(active) => OidcIdTokenSigningState::Rotating {
1484                active,
1485                successor: OidcIdTokenSigningSuccessor::Published(published),
1486            },
1487            None => OidcIdTokenSigningState::Published(published),
1488        };
1489        Ok(())
1490    }
1491
1492    /// Returns the exact public JWKS URI bound while this activation is
1493    /// Pending, Published, or Active. Native HTTP configuration uses this to
1494    /// install one non-inferred route; callers cannot substitute a Host header
1495    /// or copied endpoint spelling.
1496    #[cfg(feature = "builtin-auth-server")]
1497    pub(crate) fn advertised_id_token_jwks_uri(&self) -> Result<String, OidcError> {
1498        let slot = self.signing_activation.read().map_err(|_| {
1499            OidcError::SigningError("OIDC signing activation state is unavailable".to_string())
1500        })?;
1501        match &*slot {
1502            OidcIdTokenSigningState::Pending(pending) => {
1503                Ok(pending.advertised_jwks_uris[0].clone())
1504            }
1505            OidcIdTokenSigningState::Published(published) => {
1506                Ok(published.pending.advertised_jwks_uris[0].clone())
1507            }
1508            OidcIdTokenSigningState::Active(active) => {
1509                Ok(active.published.pending.advertised_jwks_uris[0].clone())
1510            }
1511            OidcIdTokenSigningState::Rotating {
1512                active,
1513                successor: OidcIdTokenSigningSuccessor::Pending(_),
1514            } => Ok(active.published.pending.advertised_jwks_uris[0].clone()),
1515            OidcIdTokenSigningState::Rotating {
1516                successor: OidcIdTokenSigningSuccessor::Published(published),
1517                ..
1518            } => Ok(published.pending.advertised_jwks_uris[0].clone()),
1519            OidcIdTokenSigningState::Retiring(retiring) => {
1520                Ok(retiring.published.pending.advertised_jwks_uris[0].clone())
1521            }
1522            OidcIdTokenSigningState::Inactive => Err(OidcError::SigningError(
1523                "OIDC signing activation has no advertised JWKS endpoint".to_string(),
1524            )),
1525        }
1526    }
1527
1528    /// Returns exact canonical bytes for the bound public JWKS route.
1529    ///
1530    /// This is intentionally side-effect free.  Serving a response is not an
1531    /// endpoint read-back and can never mint activation evidence; only the
1532    /// embedding-owned [`OidcJwksReadBackVerifier`] may do that.
1533    #[cfg(feature = "builtin-auth-server")]
1534    pub(crate) fn published_jwks_document(
1535        &self,
1536        advertised_jwks_uri: &str,
1537    ) -> Result<Vec<u8>, OidcError> {
1538        let slot = self.signing_activation.read().map_err(|_| {
1539            OidcError::SigningError("OIDC signing activation state is unavailable".to_string())
1540        })?;
1541        let Some(published) = slot.published() else {
1542            return Err(OidcError::SigningError(
1543                "OIDC signing JWKS is not published".to_string(),
1544            ));
1545        };
1546        if !published
1547            .pending
1548            .advertised_jwks_uris
1549            .iter()
1550            .any(|uri| uri == advertised_jwks_uri)
1551        {
1552            return Err(OidcError::SigningError(
1553                "OIDC public JWKS request did not use the activated endpoint".to_string(),
1554            ));
1555        }
1556        Ok(published.published_jwks.clone())
1557    }
1558
1559    /// Completes Active admission through an external endpoint read-back and
1560    /// durable compare-and-set.  A route handler's local buffer is never
1561    /// sufficient evidence.
1562    #[cfg(feature = "builtin-auth-server")]
1563    pub async fn activate_id_token_signing(
1564        &self,
1565        cx: &Cx,
1566        deadline: ExternalRs256SigningDeadline,
1567    ) -> Result<(), OidcError> {
1568        let (
1569            key_ring,
1570            binding,
1571            expected,
1572            endpoints,
1573            origins,
1574            issuer,
1575            dependencies,
1576            publication_generation,
1577            rotating,
1578        ) = {
1579            let slot = self.signing_activation.read().map_err(|_| {
1580                OidcError::SigningError("OIDC signing activation state is unavailable".to_string())
1581            })?;
1582            let (published, rotating) = match &*slot {
1583                OidcIdTokenSigningState::Published(published) => (published, false),
1584                OidcIdTokenSigningState::Rotating {
1585                    successor: OidcIdTokenSigningSuccessor::Published(published),
1586                    ..
1587                } => (published, true),
1588                _ => {
1589                    return Err(OidcError::SigningError(
1590                        "OIDC signing activation requires published JWKS read-back".to_string(),
1591                    ));
1592                }
1593            };
1594            (
1595                published.pending.key_ring.clone(),
1596                published.pending.binding,
1597                published.pending.canonical_jwks.clone(),
1598                published.pending.advertised_jwks_uris.clone(),
1599                published.pending.advertised_jwks_origins.clone(),
1600                published.pending.issuer.clone(),
1601                published.pending.dependencies.clone(),
1602                published.publication_generation,
1603                rotating,
1604            )
1605        };
1606        let dependencies = dependencies.ok_or_else(|| {
1607            OidcError::SigningError(
1608                "OIDC signer activation requires an external read-back verifier and durable store"
1609                    .to_string(),
1610            )
1611        })?;
1612        let canary = oidc_signing_canary_claims()?;
1613        let signed = key_ring
1614            .active_signer()
1615            .sign(cx, JwsSigningProfile::OidcIdToken, canary, deadline)
1616            .await
1617            .map_err(OidcError::ExternalSigning)?;
1618        if signed.binding() != binding {
1619            return Err(OidcError::SigningError(
1620                "OIDC signing canary returned a stale signer binding".to_string(),
1621            ));
1622        }
1623        cx.checkpoint().map_err(|_| {
1624            OidcError::SigningError(
1625                "OIDC signing activation cancelled after canary dispatch".to_string(),
1626            )
1627        })?;
1628        let canary = signed.into_compact_jws();
1629        let observed = dependencies.verifier.read_back(cx, &endpoints).await?;
1630        cx.checkpoint().map_err(|_| {
1631            OidcError::SigningError("OIDC activation cancelled after JWKS read-back".to_string())
1632        })?;
1633        let receipt = SigningActivationReceipt::verify(
1634            SigningActivationProfile::OidcIdToken,
1635            issuer.clone(),
1636            &expected,
1637            &endpoints,
1638            &origins,
1639            observed,
1640            canary,
1641        )
1642        .map_err(|_| {
1643            OidcError::SigningError(
1644                "OIDC signing canary failed public JWKS verification".to_string(),
1645            )
1646        })?;
1647
1648        cx.checkpoint().map_err(|_| {
1649            OidcError::SigningError("OIDC activation cancelled before durable CAS".to_string())
1650        })?;
1651        let prior = dependencies.store.load(cx, &issuer)?;
1652        if prior.as_ref().is_some_and(|record| {
1653            record.issuer() != issuer
1654                || record.status() != OidcSigningActivationStatus::Active
1655                || record.key_ring_generation() > expected.generation()
1656        }) {
1657            return Err(OidcError::SigningError(
1658                "OIDC activation store rejected a stale or rollback key-ring generation"
1659                    .to_string(),
1660            ));
1661        }
1662        let now = oidc_unix_timestamp()?;
1663        let successor_key_identities = oidc_key_ring_public_identities(&key_ring)?;
1664        let mut key_id_maximum_id_token_expires_at =
1665            prior.as_ref().map_or_else(BTreeMap::new, |record| {
1666                record.key_id_maximum_id_token_expires_at().clone()
1667            });
1668        if key_id_maximum_id_token_expires_at
1669            .iter()
1670            .any(|(key_id, watermark)| {
1671                watermark.expires_at() > now
1672                    && successor_key_identities.get(key_id).is_none_or(|identity| {
1673                        identity.as_slice() != watermark.canonical_public_key_identity()
1674                    })
1675            })
1676        {
1677            return Err(OidcError::SigningError(
1678                "OIDC activation key ring omits or substitutes a durably live verification key"
1679                    .to_string(),
1680            ));
1681        }
1682        key_id_maximum_id_token_expires_at.retain(|key_id, watermark| {
1683            watermark.expires_at() > now || successor_key_identities.contains_key(key_id)
1684        });
1685        for (key_id, identity) in successor_key_identities {
1686            let replaces_expired_identity = key_id_maximum_id_token_expires_at
1687                .get(&key_id)
1688                .is_some_and(|watermark| {
1689                    watermark.expires_at() <= now
1690                        && watermark.canonical_public_key_identity() != identity.as_slice()
1691                });
1692            if replaces_expired_identity
1693                || !key_id_maximum_id_token_expires_at.contains_key(&key_id)
1694            {
1695                key_id_maximum_id_token_expires_at
1696                    .insert(key_id, OidcSigningKeyExpiry::new(0, identity)?);
1697            }
1698        }
1699        let expected_activation_generation = prior
1700            .as_ref()
1701            .map(OidcSigningActivationStoreRecord::activation_generation);
1702        let next_generation = expected_activation_generation
1703            .unwrap_or(0)
1704            .checked_add(1)
1705            .ok_or_else(|| {
1706                OidcError::SigningError("OIDC activation generation is exhausted".to_string())
1707            })?;
1708        let next = OidcSigningActivationStoreRecord::new(
1709            issuer.clone(),
1710            expected.generation(),
1711            next_generation,
1712            OidcSigningActivationStatus::Active,
1713            prior.as_ref().map_or(
1714                0,
1715                OidcSigningActivationStoreRecord::maximum_id_token_expires_at,
1716            ),
1717            key_id_maximum_id_token_expires_at,
1718        )?;
1719        let committed =
1720            dependencies
1721                .store
1722                .compare_and_set(cx, expected_activation_generation, next.clone())?;
1723        if committed != next {
1724            return Err(OidcError::SigningError(
1725                "OIDC activation store returned a mismatched CAS record".to_string(),
1726            ));
1727        }
1728
1729        let mut slot = self.signing_activation.write().map_err(|_| {
1730            OidcError::SigningError("OIDC signing activation state is unavailable".to_string())
1731        })?;
1732        let state = std::mem::replace(&mut *slot, OidcIdTokenSigningState::Inactive);
1733        let (published, previous_active) = match state {
1734            OidcIdTokenSigningState::Published(published) if !rotating => (published, None),
1735            OidcIdTokenSigningState::Rotating {
1736                active,
1737                successor: OidcIdTokenSigningSuccessor::Published(published),
1738            } if rotating => (published, Some(active)),
1739            state => {
1740                *slot = state;
1741                return Err(OidcError::SigningError(
1742                    "OIDC signing activation changed while its canary was in flight".to_string(),
1743                ));
1744            }
1745        };
1746        if published.pending.binding != binding
1747            || published.publication_generation != publication_generation
1748            || published.pending.key_ring.generation() != expected.generation()
1749        {
1750            *slot = match previous_active {
1751                Some(active) => OidcIdTokenSigningState::Rotating {
1752                    active,
1753                    successor: OidcIdTokenSigningSuccessor::Published(published),
1754                },
1755                None => OidcIdTokenSigningState::Published(published),
1756            };
1757            return Err(OidcError::SigningError(
1758                "OIDC signing activation publication changed while its canary was in flight"
1759                    .to_string(),
1760            ));
1761        }
1762        *slot = OidcIdTokenSigningState::Active(OidcIdTokenSigningActivation {
1763            published,
1764            receipt,
1765            activation_generation: committed.activation_generation,
1766            maximum_id_token_expires_at: committed.maximum_id_token_expires_at,
1767            live_key_expiries: committed.key_id_maximum_id_token_expires_at().clone(),
1768        });
1769        Ok(())
1770    }
1771
1772    /// Closes issuance and enters `Retiring` only after the recorded maximum
1773    /// lifetime of every ID token signed by this generation.  Verification
1774    /// material remains available to the public JWKS route while retirement is
1775    /// observed; callers must install a successor generation separately.
1776    #[cfg(feature = "builtin-auth-server")]
1777    pub fn retire_id_token_signing_generation(
1778        &self,
1779        cx: &Cx,
1780        now_unix_seconds: i64,
1781    ) -> Result<(), OidcError> {
1782        let (dependencies, issuer, key_ring_generation, activation_generation) = {
1783            let slot = self.signing_activation.read().map_err(|_| {
1784                OidcError::SigningError("OIDC signing activation state is unavailable".to_string())
1785            })?;
1786            let OidcIdTokenSigningState::Active(active) = &*slot else {
1787                return Err(OidcError::SigningError(
1788                    "OIDC signing retirement requires an unrotated Active generation".to_string(),
1789                ));
1790            };
1791            (
1792                active
1793                    .published
1794                    .pending
1795                    .dependencies
1796                    .clone()
1797                    .ok_or_else(|| {
1798                        OidcError::SigningError(
1799                            "OIDC active signer lost its durable activation dependencies"
1800                                .to_string(),
1801                        )
1802                    })?,
1803                active.published.pending.issuer.clone(),
1804                active.published.pending.key_ring.generation(),
1805                active.activation_generation,
1806            )
1807        };
1808        let durable = fence_active_oidc_activation_store(
1809            cx,
1810            &dependencies,
1811            &issuer,
1812            key_ring_generation,
1813            activation_generation,
1814        )?;
1815        if durable.maximum_id_token_expires_at() > now_unix_seconds {
1816            return Err(OidcError::SigningError(
1817                "OIDC signing key cannot retire before the durable maximum ID-token expiry"
1818                    .to_string(),
1819            ));
1820        }
1821        let next_generation = durable
1822            .activation_generation()
1823            .checked_add(1)
1824            .ok_or_else(|| {
1825                OidcError::SigningError(
1826                    "OIDC durable activation generation is exhausted".to_string(),
1827                )
1828            })?;
1829        let next = OidcSigningActivationStoreRecord::new(
1830            issuer,
1831            key_ring_generation,
1832            next_generation,
1833            OidcSigningActivationStatus::Retiring,
1834            durable.maximum_id_token_expires_at(),
1835            durable.key_id_maximum_id_token_expires_at().clone(),
1836        )?;
1837        let committed = dependencies.store.compare_and_set(
1838            cx,
1839            Some(durable.activation_generation()),
1840            next.clone(),
1841        )?;
1842        if committed != next {
1843            return Err(OidcError::SigningError(
1844                "OIDC durable retirement CAS fence was lost".to_string(),
1845            ));
1846        }
1847        let mut slot = self.signing_activation.write().map_err(|_| {
1848            OidcError::SigningError("OIDC signing activation state is unavailable".to_string())
1849        })?;
1850        let state = std::mem::replace(&mut *slot, OidcIdTokenSigningState::Inactive);
1851        let OidcIdTokenSigningState::Active(mut active) = state else {
1852            *slot = state;
1853            return Err(OidcError::SigningError(
1854                "OIDC signing retirement requires an Active generation".to_string(),
1855            ));
1856        };
1857        if active.activation_generation != durable.activation_generation()
1858            || active.published.pending.key_ring.generation() != key_ring_generation
1859        {
1860            *slot = OidcIdTokenSigningState::Active(active);
1861            return Err(OidcError::SigningError(
1862                "OIDC signing activation changed before retirement".to_string(),
1863            ));
1864        }
1865        active.activation_generation = committed.activation_generation();
1866        active.maximum_id_token_expires_at = committed.maximum_id_token_expires_at();
1867        *slot = OidcIdTokenSigningState::Retiring(active);
1868        Ok(())
1869    }
1870
1871    /// Returns exact public JWKS bytes only after their matching activation is
1872    /// Active. A pending/publication read-back never exposes a usable signer.
1873    #[cfg(feature = "builtin-auth-server")]
1874    pub fn activated_jwks_document(&self) -> Result<Vec<u8>, OidcError> {
1875        let slot = self.signing_activation.read().map_err(|_| {
1876            OidcError::SigningError("OIDC signing activation state is unavailable".to_string())
1877        })?;
1878        let active = slot.active().ok_or_else(|| {
1879            OidcError::SigningError("OIDC signing activation is not Active".to_string())
1880        })?;
1881        Ok(active.published.published_jwks.clone())
1882    }
1883
1884    /// Generates the discovery document.
1885    #[must_use]
1886    pub fn discovery_document(&self, base_url: impl Into<String>) -> DiscoveryDocument {
1887        let base_url = base_url.into();
1888        let mut doc = DiscoveryDocument::new(&self.config.issuer, base_url);
1889        doc.scopes_supported = self.config.supported_scopes.clone();
1890        doc.claims_supported = Some(self.config.supported_claims.clone());
1891        #[cfg(feature = "builtin-auth-server")]
1892        if let Ok(slot) = self.signing_activation.read() {
1893            if let (Some(_), Some(published)) = (slot.active(), slot.published()) {
1894                doc.id_token_signing_alg_values_supported = vec!["RS256".to_string()];
1895                doc.jwks_uri = Some(published.pending.advertised_jwks_uris[0].clone());
1896            }
1897        }
1898        doc
1899    }
1900
1901    // -------------------------------------------------------------------------
1902    // ID Token Issuance
1903    // -------------------------------------------------------------------------
1904
1905    /// Issues one OIDC ID-token candidate through the active external RS256
1906    /// signer. The returned compact JWS is self-verified by the signer facade
1907    /// and then verified again against the exact JWKS publication read-back.
1908    ///
1909    /// This is intentionally asynchronous and accepts the caller's [`Cx`];
1910    /// it never creates a runtime or substitutes an in-process signing key.
1911    #[cfg(feature = "builtin-auth-server")]
1912    pub async fn issue_id_token(
1913        &self,
1914        cx: &Cx,
1915        access_token: &str,
1916        nonce: Option<&str>,
1917        deadline: ExternalRs256SigningDeadline,
1918    ) -> Result<IdToken, OidcError> {
1919        let access_token_credential = access_token;
1920        let access_token = self.validated_oidc_access_token(access_token)?;
1921        let subject = access_token
1922            .subject
1923            .as_deref()
1924            .ok_or_else(|| OidcError::ClaimsNotFound("no subject in access token".to_string()))?;
1925        validate_oidc_nonce(nonce)?;
1926        let user_claims = self.get_user_claims(subject, &access_token.scopes)?;
1927        let now = oidc_unix_timestamp()?;
1928        let expires_in = access_token
1929            .expires_at
1930            .saturating_duration_since(Instant::now())
1931            .as_secs();
1932        let expires_in = i64::try_from(expires_in).map_err(|_| {
1933            OidcError::InvalidIdToken("access-token lifetime exceeds ID-token range".to_string())
1934        })?;
1935        let exp = now.checked_add(expires_in).ok_or_else(|| {
1936            OidcError::InvalidIdToken("ID-token expiry exceeds timestamp range".to_string())
1937        })?;
1938        if exp <= now {
1939            return Err(OidcError::InvalidIdToken(
1940                "access token is expired before ID-token issuance".to_string(),
1941            ));
1942        }
1943        let claims = IdTokenClaims {
1944            iss: self.config.issuer.clone(),
1945            sub: subject.to_string(),
1946            aud: access_token.client_id.clone(),
1947            exp,
1948            iat: now,
1949            auth_time: None,
1950            nonce: nonce.map(str::to_string),
1951            acr: None,
1952            amr: None,
1953            azp: None,
1954            at_hash: None,
1955            c_hash: None,
1956            user_claims,
1957        };
1958        let signing_claims = id_token_signing_claims(&claims)?;
1959        let (
1960            signer,
1961            binding,
1962            read_back_keys,
1963            dependencies,
1964            activation_generation,
1965            key_ring_generation,
1966        ) = {
1967            let slot = self.signing_activation.read().map_err(|_| {
1968                OidcError::SigningError("OIDC signing activation state is unavailable".to_string())
1969            })?;
1970            let activation = slot.active().ok_or_else(|| {
1971                OidcError::SigningError("OIDC signing activation is required".to_string())
1972            })?;
1973            if activation.published.pending.issuer != self.config.issuer
1974                || activation.published.pending.binding
1975                    != activation
1976                        .published
1977                        .pending
1978                        .key_ring
1979                        .active_signer()
1980                        .binding()
1981                || !activation.receipt.applies_to(
1982                    SigningActivationProfile::OidcIdToken,
1983                    &self.config.issuer,
1984                    &activation.published.pending.canonical_jwks,
1985                    &activation.published.pending.advertised_jwks_uris,
1986                    &activation.published.pending.advertised_jwks_origins,
1987                )
1988            {
1989                return Err(OidcError::SigningError(
1990                    "OIDC signing activation no longer matches the selected signer".to_string(),
1991                ));
1992            }
1993            (
1994                Arc::clone(activation.published.pending.key_ring.active_signer()),
1995                activation.published.pending.binding,
1996                AdmittedRsaJwks::from_json(&activation.published.published_jwks).map_err(|_| {
1997                    OidcError::SigningError(
1998                        "OIDC signing activation lost its public JWKS read-back".to_string(),
1999                    )
2000                })?,
2001                activation
2002                    .published
2003                    .pending
2004                    .dependencies
2005                    .clone()
2006                    .ok_or_else(|| {
2007                        OidcError::SigningError(
2008                            "OIDC active signer lost its durable activation dependencies"
2009                                .to_string(),
2010                        )
2011                    })?,
2012                activation.activation_generation,
2013                activation.published.pending.key_ring.generation(),
2014            )
2015        };
2016        let durable_before_sign = fence_active_oidc_activation_store(
2017            cx,
2018            &dependencies,
2019            &self.config.issuer,
2020            key_ring_generation,
2021            activation_generation,
2022        )?;
2023        let signer_key_id = signer.key_id().to_string();
2024        let signer_key_identity = signer
2025            .canonical_public_jwks()
2026            .map_err(|_| {
2027                OidcError::SigningError(
2028                    "OIDC active signer has no canonical public key identity".to_string(),
2029                )
2030            })?
2031            .as_bytes()
2032            .to_vec();
2033        let signed = signer
2034            .sign(cx, JwsSigningProfile::OidcIdToken, signing_claims, deadline)
2035            .await
2036            .map_err(OidcError::ExternalSigning)?;
2037        if signed.binding() != binding {
2038            return Err(OidcError::SigningError(
2039                "external ID-token signing binding changed before exposure".to_string(),
2040            ));
2041        }
2042        cx.checkpoint().map_err(|_| {
2043            OidcError::SigningError("OIDC request cancelled after external signing".to_string())
2044        })?;
2045        let revalidated = self.validated_oidc_access_token(access_token_credential)?;
2046        if !same_id_token_authorization(&access_token, &revalidated) {
2047            return Err(OidcError::SigningError(
2048                "OIDC access-token authorization changed during external signing".to_string(),
2049            ));
2050        }
2051        let next_activation_generation = durable_before_sign
2052            .activation_generation()
2053            .checked_add(1)
2054            .ok_or_else(|| {
2055                OidcError::SigningError(
2056                    "OIDC durable activation generation is exhausted".to_string(),
2057                )
2058            })?;
2059        let maximum_id_token_expires_at =
2060            durable_before_sign.maximum_id_token_expires_at().max(exp);
2061        let mut key_id_maximum_id_token_expires_at = durable_before_sign
2062            .key_id_maximum_id_token_expires_at()
2063            .clone();
2064        if key_id_maximum_id_token_expires_at
2065            .get(&signer_key_id)
2066            .is_some_and(|watermark| {
2067                watermark.canonical_public_key_identity() != signer_key_identity.as_slice()
2068            })
2069        {
2070            return Err(OidcError::SigningError(
2071                "OIDC durable key identity changed before ID-token exposure".to_string(),
2072            ));
2073        }
2074        let signer_expiry = key_id_maximum_id_token_expires_at
2075            .get(&signer_key_id)
2076            .map_or(exp, |watermark| watermark.expires_at().max(exp));
2077        key_id_maximum_id_token_expires_at.insert(
2078            signer_key_id,
2079            OidcSigningKeyExpiry::new(signer_expiry, signer_key_identity)?,
2080        );
2081        let committed = OidcSigningActivationStoreRecord::new(
2082            self.config.issuer.clone(),
2083            key_ring_generation,
2084            next_activation_generation,
2085            OidcSigningActivationStatus::Active,
2086            maximum_id_token_expires_at,
2087            key_id_maximum_id_token_expires_at.clone(),
2088        )?;
2089        let committed = dependencies.store.compare_and_set(
2090            cx,
2091            Some(durable_before_sign.activation_generation()),
2092            committed.clone(),
2093        )?;
2094        if committed
2095            != OidcSigningActivationStoreRecord::new(
2096                self.config.issuer.clone(),
2097                key_ring_generation,
2098                next_activation_generation,
2099                OidcSigningActivationStatus::Active,
2100                maximum_id_token_expires_at,
2101                key_id_maximum_id_token_expires_at,
2102            )?
2103        {
2104            return Err(OidcError::SigningError(
2105                "OIDC durable activation fence changed while signing".to_string(),
2106            ));
2107        }
2108        let mut slot = self.signing_activation.write().map_err(|_| {
2109            OidcError::SigningError("OIDC signing activation state is unavailable".to_string())
2110        })?;
2111        let active = match &mut *slot {
2112            OidcIdTokenSigningState::Active(active) => active,
2113            OidcIdTokenSigningState::Rotating { active, .. } => active,
2114            _ => {
2115                return Err(OidcError::SigningError(
2116                    "OIDC signing activation changed before ID-token exposure".to_string(),
2117                ));
2118            }
2119        };
2120        if active.activation_generation != durable_before_sign.activation_generation()
2121            || active.published.pending.binding != binding
2122            || active.published.pending.key_ring.generation() != key_ring_generation
2123        {
2124            return Err(OidcError::SigningError(
2125                "OIDC signing activation was superseded before ID-token exposure".to_string(),
2126            ));
2127        }
2128        active.activation_generation = committed.activation_generation();
2129        active.maximum_id_token_expires_at = committed.maximum_id_token_expires_at();
2130        active.live_key_expiries = committed.key_id_maximum_id_token_expires_at().clone();
2131        drop(slot);
2132        let raw = signed.into_compact_jws();
2133        verify_compact_jws_rs256(&raw, &read_back_keys).map_err(|_| {
2134            OidcError::SigningError("ID-token failed published-JWKS verification".to_string())
2135        })?;
2136        Ok(IdToken { raw, claims })
2137    }
2138
2139    // -------------------------------------------------------------------------
2140    // UserInfo Endpoint
2141    // -------------------------------------------------------------------------
2142
2143    /// Handles a userinfo request.
2144    ///
2145    /// Returns the user's claims filtered by the access token's scopes.
2146    pub fn userinfo(&self, access_token: &str) -> Result<UserClaims, OidcError> {
2147        let validated = self.validated_oidc_access_token(access_token)?;
2148
2149        let subject = validated
2150            .subject
2151            .as_ref()
2152            .ok_or_else(|| OidcError::ClaimsNotFound("no subject in access token".to_string()))?;
2153
2154        self.get_user_claims(subject, &validated.scopes)
2155    }
2156
2157    fn validated_oidc_access_token(&self, access_token: &str) -> Result<OAuthToken, OidcError> {
2158        let validated = self
2159            .oauth
2160            .validate_access_token(access_token)
2161            .ok_or_else(|| {
2162                OidcError::OAuth(OAuthError::InvalidGrant(
2163                    "invalid, revoked, or expired OAuth access token".to_string(),
2164                ))
2165            })?;
2166        if validated.is_refresh_token || validated.is_expired() {
2167            return Err(OidcError::OAuth(OAuthError::InvalidGrant(
2168                "invalid, revoked, or expired OAuth access token".to_string(),
2169            )));
2170        }
2171        if !validated.scopes.iter().any(|scope| scope == "openid") {
2172            return Err(OidcError::MissingOpenIdScope);
2173        }
2174        Ok(validated)
2175    }
2176
2177    // -------------------------------------------------------------------------
2178    // Helper Methods
2179    // -------------------------------------------------------------------------
2180
2181    fn get_user_claims(&self, subject: &str, scopes: &[String]) -> Result<UserClaims, OidcError> {
2182        let provider = self
2183            .claims_provider
2184            .read()
2185            .ok()
2186            .and_then(|guard| guard.clone());
2187
2188        let claims = match provider {
2189            Some(p) => p
2190                .get_claims(subject)
2191                .ok_or_else(|| OidcError::ClaimsNotFound(subject.to_string()))?,
2192            None => {
2193                // Default: just return subject
2194                UserClaims::new(subject)
2195            }
2196        };
2197
2198        // The validated access token is the authority for user identity. A
2199        // custom provider must never be able to substitute another subject's
2200        // claims, even when it was queried with the correct key.
2201        if claims.sub != subject {
2202            return Err(OidcError::ClaimsSubjectMismatch);
2203        }
2204
2205        Ok(claims.filter_by_scopes(scopes))
2206    }
2207}
2208
2209// =============================================================================
2210// Helper Functions
2211// =============================================================================
2212
2213#[cfg(feature = "builtin-auth-server")]
2214fn validate_advertised_jwks_uri(
2215    issuer: &str,
2216    advertised_jwks_uri: &str,
2217) -> Result<(String, String), OidcError> {
2218    let issuer = Url::parse(issuer).map_err(|_| {
2219        OidcError::SigningError("OIDC issuer cannot bind an advertised JWKS endpoint".to_string())
2220    })?;
2221    let endpoint = Url::parse(advertised_jwks_uri).map_err(|_| {
2222        OidcError::SigningError("advertised JWKS endpoint is not an absolute HTTPS URL".to_string())
2223    })?;
2224    if endpoint.scheme() != "https"
2225        || endpoint.host_str().is_none()
2226        || !endpoint.username().is_empty()
2227        || endpoint.password().is_some()
2228        || endpoint.query().is_some()
2229        || endpoint.fragment().is_some()
2230    {
2231        return Err(OidcError::SigningError(
2232            "advertised JWKS endpoint is not a canonical HTTPS origin path".to_string(),
2233        ));
2234    }
2235    let canonical_endpoint = endpoint.to_string();
2236    if advertised_jwks_uri != canonical_endpoint {
2237        return Err(OidcError::SigningError(
2238            "advertised JWKS endpoint must use its exact canonical URI spelling".to_string(),
2239        ));
2240    }
2241    let issuer_origin = issuer.origin().ascii_serialization();
2242    let endpoint_origin = endpoint.origin().ascii_serialization();
2243    if endpoint_origin != issuer_origin {
2244        return Err(OidcError::SigningError(
2245            "advertised JWKS endpoint origin does not match the OIDC issuer".to_string(),
2246        ));
2247    }
2248    Ok((canonical_endpoint, endpoint_origin))
2249}
2250
2251#[cfg(feature = "builtin-auth-server")]
2252fn same_id_token_authorization(left: &OAuthToken, right: &OAuthToken) -> bool {
2253    left.client_id == right.client_id
2254        && left.scopes == right.scopes
2255        && left.resource == right.resource
2256        && left.issued_at == right.issued_at
2257        && left.expires_at == right.expires_at
2258        && left.subject == right.subject
2259        && left.token_type == right.token_type
2260        && !left.is_refresh_token
2261        && !right.is_refresh_token
2262}
2263
2264/// Loads and atomically reasserts the exact durable Active record before an
2265/// external signing dispatch. Status transitions and all generation advances
2266/// must change the CAS generation, so a stale process cannot overwrite a
2267/// retire/revoke record with its old local activation.
2268#[cfg(feature = "builtin-auth-server")]
2269fn fence_active_oidc_activation_store(
2270    cx: &Cx,
2271    dependencies: &OidcSigningActivationDependencies,
2272    issuer: &str,
2273    key_ring_generation: u64,
2274    activation_generation: u64,
2275) -> Result<OidcSigningActivationStoreRecord, OidcError> {
2276    let record = dependencies.store.load(cx, issuer)?.ok_or_else(|| {
2277        OidcError::SigningError("OIDC durable activation record is absent".to_string())
2278    })?;
2279    if record.issuer() != issuer
2280        || record.key_ring_generation() != key_ring_generation
2281        || record.activation_generation() != activation_generation
2282        || record.status() != OidcSigningActivationStatus::Active
2283    {
2284        return Err(OidcError::SigningError(
2285            "OIDC durable activation record no longer authorizes this signer".to_string(),
2286        ));
2287    }
2288    let fenced = dependencies.store.compare_and_set(
2289        cx,
2290        Some(record.activation_generation()),
2291        record.clone(),
2292    )?;
2293    if fenced != record {
2294        return Err(OidcError::SigningError(
2295            "OIDC durable activation CAS fence was lost".to_string(),
2296        ));
2297    }
2298    Ok(record)
2299}
2300
2301#[cfg(feature = "builtin-auth-server")]
2302fn canonical_oidc_key_identity_matches(key_id: &str, identity: &[u8]) -> bool {
2303    let canonical = serde_json::from_slice::<serde_json::Value>(identity)
2304        .ok()
2305        .and_then(|value| serde_json::to_vec(&value).ok());
2306    canonical.as_deref() == Some(identity)
2307        && AdmittedRsaJwks::from_json(identity)
2308            .is_ok_and(|keys| keys.len() == 1 && keys.contains_kid(key_id))
2309}
2310
2311#[cfg(feature = "builtin-auth-server")]
2312fn oidc_key_ring_public_identities(
2313    key_ring: &Rs256PublicKeyRing,
2314) -> Result<BTreeMap<String, Vec<u8>>, OidcError> {
2315    let mut identities = BTreeMap::new();
2316    for key_id in key_ring.key_ids() {
2317        let identity = key_ring
2318            .canonical_public_key_identity(&key_id)
2319            .ok_or_else(|| {
2320                OidcError::SigningError(
2321                    "OIDC key ring has no canonical public key identity".to_string(),
2322                )
2323            })?
2324            .as_bytes()
2325            .to_vec();
2326        if identities.insert(key_id, identity).is_some() {
2327            return Err(OidcError::SigningError(
2328                "OIDC key ring contains duplicate public key identifiers".to_string(),
2329            ));
2330        }
2331    }
2332    Ok(identities)
2333}
2334
2335#[cfg(feature = "builtin-auth-server")]
2336fn validate_oidc_nonce(nonce: Option<&str>) -> Result<(), OidcError> {
2337    if nonce.is_some_and(|nonce| {
2338        nonce.is_empty()
2339            || nonce.len() > MAX_OIDC_NONCE_BYTES
2340            || nonce.bytes().any(|byte| byte.is_ascii_control())
2341    }) {
2342        return Err(OidcError::InvalidIdToken(
2343            "OIDC nonce is outside admitted bounds".to_string(),
2344        ));
2345    }
2346    Ok(())
2347}
2348
2349#[cfg(feature = "builtin-auth-server")]
2350fn oidc_signing_canary_claims() -> Result<BoundedJwsClaims, OidcError> {
2351    BoundedJwsClaims::from_json_bytes(OIDC_SIGNING_CANARY_CLAIMS.as_bytes()).map_err(|_| {
2352        OidcError::SigningError("OIDC signing canary claims are outside signer bounds".to_string())
2353    })
2354}
2355
2356#[cfg(feature = "builtin-auth-server")]
2357fn oidc_unix_timestamp() -> Result<i64, OidcError> {
2358    let seconds = SystemTime::now()
2359        .duration_since(UNIX_EPOCH)
2360        .map_err(|_| OidcError::InvalidIdToken("system clock predates Unix epoch".to_string()))?
2361        .as_secs();
2362    i64::try_from(seconds)
2363        .map_err(|_| OidcError::InvalidIdToken("system clock exceeds ID-token range".to_string()))
2364}
2365
2366#[cfg(feature = "builtin-auth-server")]
2367fn id_token_signing_claims(claims: &IdTokenClaims) -> Result<BoundedJwsClaims, OidcError> {
2368    let mut value = serde_json::to_value(&claims.user_claims)
2369        .map_err(|_| OidcError::InvalidIdToken("user claims cannot be serialized".to_string()))?;
2370    let object = value.as_object_mut().ok_or_else(|| {
2371        OidcError::InvalidIdToken("user claims must serialize as an object".to_string())
2372    })?;
2373    // A claims provider may supply arbitrary public profile claims, but it
2374    // cannot overwrite the identity and lifetime facts that this server owns.
2375    for name in [
2376        "iss",
2377        "sub",
2378        "aud",
2379        "exp",
2380        "iat",
2381        "auth_time",
2382        "nonce",
2383        "acr",
2384        "amr",
2385        "azp",
2386        "at_hash",
2387        "c_hash",
2388    ] {
2389        object.remove(name);
2390    }
2391    object.insert(
2392        "iss".to_string(),
2393        serde_json::Value::String(claims.iss.clone()),
2394    );
2395    object.insert(
2396        "sub".to_string(),
2397        serde_json::Value::String(claims.sub.clone()),
2398    );
2399    object.insert(
2400        "aud".to_string(),
2401        serde_json::Value::String(claims.aud.clone()),
2402    );
2403    object.insert("exp".to_string(), serde_json::Value::from(claims.exp));
2404    object.insert("iat".to_string(), serde_json::Value::from(claims.iat));
2405    if let Some(auth_time) = claims.auth_time {
2406        object.insert("auth_time".to_string(), serde_json::Value::from(auth_time));
2407    }
2408    if let Some(nonce) = &claims.nonce {
2409        object.insert(
2410            "nonce".to_string(),
2411            serde_json::Value::String(nonce.clone()),
2412        );
2413    }
2414    if let Some(acr) = &claims.acr {
2415        object.insert("acr".to_string(), serde_json::Value::String(acr.clone()));
2416    }
2417    if let Some(amr) = &claims.amr {
2418        object.insert(
2419            "amr".to_string(),
2420            serde_json::to_value(amr).map_err(|_| {
2421                OidcError::InvalidIdToken("authentication methods cannot be serialized".to_string())
2422            })?,
2423        );
2424    }
2425    if let Some(azp) = &claims.azp {
2426        object.insert("azp".to_string(), serde_json::Value::String(azp.clone()));
2427    }
2428    if let Some(at_hash) = &claims.at_hash {
2429        object.insert(
2430            "at_hash".to_string(),
2431            serde_json::Value::String(at_hash.clone()),
2432        );
2433    }
2434    if let Some(c_hash) = &claims.c_hash {
2435        object.insert(
2436            "c_hash".to_string(),
2437            serde_json::Value::String(c_hash.clone()),
2438        );
2439    }
2440    BoundedJwsClaims::from_value(&value)
2441        .map_err(|_| OidcError::InvalidIdToken("ID-token claims exceed signing bounds".to_string()))
2442}
2443
2444// =============================================================================
2445// Tests
2446// =============================================================================
2447
2448#[cfg(test)]
2449mod non_signing_tests {
2450    use super::*;
2451
2452    #[test]
2453    fn default_oidc_and_oauth_issuers_match() {
2454        assert_eq!(
2455            OidcProviderConfig::default().issuer,
2456            OAuthServerConfig::default().issuer
2457        );
2458    }
2459
2460    #[test]
2461    fn provider_requires_exact_safe_oauth_issuer_and_defaults_follow_custom_oauth() {
2462        let oauth = Arc::new(
2463            OAuthServer::try_new(OAuthServerConfig {
2464                issuer: "https://issuer.example/tenant".to_string(),
2465                ..OAuthServerConfig::default()
2466            })
2467            .unwrap(),
2468        );
2469        let provider = OidcProvider::with_defaults(Arc::clone(&oauth)).unwrap();
2470        assert_eq!(provider.config().issuer, oauth.config().issuer);
2471        assert_eq!(
2472            provider.discovery_document("https://issuer.example").issuer,
2473            oauth.config().issuer
2474        );
2475
2476        assert!(matches!(
2477            OidcProvider::new(Arc::clone(&oauth), OidcProviderConfig::default()),
2478            Err(OidcError::OAuth(OAuthError::ServerError(_)))
2479        ));
2480
2481        let unsafe_config = OidcProviderConfig {
2482            issuer: "http://issuer.example".to_string(),
2483            ..OidcProviderConfig::default()
2484        };
2485        assert!(matches!(
2486            OidcProvider::new(oauth, unsafe_config),
2487            Err(OidcError::OAuth(OAuthError::ServerError(_)))
2488        ));
2489    }
2490
2491    #[test]
2492    fn discovery_does_not_advertise_signing() {
2493        let doc = DiscoveryDocument::new("https://issuer.example", "https://issuer.example");
2494        assert!(doc.id_token_signing_alg_values_supported.is_empty());
2495        assert!(doc.jwks_uri.is_none());
2496        assert_eq!(
2497            doc.code_challenge_methods_supported,
2498            Some(vec!["S256".to_string()])
2499        );
2500    }
2501
2502    #[test]
2503    fn user_claims_filter_by_scope() {
2504        let claims = UserClaims::new("subject")
2505            .with_name("Alice")
2506            .with_email("alice@example.test")
2507            .with_email_verified(true);
2508        let filtered = claims.filter_by_scopes(&["openid".to_string()]);
2509        assert_eq!(filtered.sub, "subject");
2510        assert!(filtered.name.is_none());
2511        assert!(filtered.email.is_none());
2512    }
2513
2514    #[test]
2515    fn claims_provider_cannot_substitute_a_different_subject() {
2516        let oauth = Arc::new(OAuthServer::new(OAuthServerConfig::default()));
2517        let provider = OidcProvider::with_defaults(oauth).expect("default provider");
2518        provider.set_claims_fn(|requested_subject| {
2519            assert_eq!(requested_subject, "authenticated-subject");
2520            Some(UserClaims::new("different-subject").with_email("different-subject@example.test"))
2521        });
2522
2523        let error = provider
2524            .get_user_claims(
2525                "authenticated-subject",
2526                &["openid".to_string(), "email".to_string()],
2527            )
2528            .expect_err("a claims provider must not substitute another identity");
2529
2530        assert!(matches!(error, OidcError::ClaimsSubjectMismatch));
2531        assert!(!error.to_string().contains("authenticated-subject"));
2532        assert!(!error.to_string().contains("different-subject"));
2533    }
2534
2535    #[test]
2536    fn oidc_debug_surfaces_redact_token_and_pii_canaries_without_changing_wire_data() {
2537        const CANARY: &str = "oidc-debug-token-pii-canary";
2538        let address = AddressClaim {
2539            formatted: Some(CANARY.to_owned()),
2540            street_address: Some(CANARY.to_owned()),
2541            locality: Some(CANARY.to_owned()),
2542            region: Some(CANARY.to_owned()),
2543            postal_code: Some(CANARY.to_owned()),
2544            country: Some(CANARY.to_owned()),
2545        };
2546        let mut user_claims = UserClaims::new(CANARY)
2547            .with_name(CANARY)
2548            .with_email(CANARY)
2549            .with_email_verified(true)
2550            .with_phone_number(CANARY)
2551            .with_custom(CANARY, serde_json::json!(CANARY));
2552        user_claims.preferred_username = Some(CANARY.to_owned());
2553        user_claims.address = Some(address.clone());
2554        let id_token_claims = IdTokenClaims {
2555            iss: CANARY.to_owned(),
2556            sub: CANARY.to_owned(),
2557            aud: CANARY.to_owned(),
2558            exp: 2,
2559            iat: 1,
2560            auth_time: Some(1),
2561            nonce: Some(CANARY.to_owned()),
2562            acr: Some(CANARY.to_owned()),
2563            amr: Some(vec![CANARY.to_owned()]),
2564            azp: Some(CANARY.to_owned()),
2565            at_hash: Some(CANARY.to_owned()),
2566            c_hash: Some(CANARY.to_owned()),
2567            user_claims: user_claims.clone(),
2568        };
2569
2570        let wire = serde_json::to_value(&id_token_claims).unwrap();
2571        assert_eq!(wire["nonce"], CANARY);
2572        assert_eq!(wire["email"], CANARY);
2573        assert_eq!(wire["phone_number"], CANARY);
2574        assert_eq!(wire["address"]["formatted"], CANARY);
2575        assert_eq!(wire[CANARY], CANARY);
2576
2577        let id_token = IdToken {
2578            raw: CANARY.to_owned(),
2579            claims: id_token_claims.clone(),
2580        };
2581        let provider = InMemoryClaimsProvider::new();
2582        provider.set_claims(user_claims.clone());
2583        let errors = [
2584            OidcError::ClaimsNotFound(CANARY.to_owned()),
2585            OidcError::SigningError(CANARY.to_owned()),
2586            OidcError::InvalidIdToken(CANARY.to_owned()),
2587        ];
2588        let debug_outputs = [
2589            format!("{address:?}"),
2590            format!("{user_claims:?}"),
2591            format!("{id_token_claims:?}"),
2592            format!("{id_token:?}"),
2593            format!("{provider:?}"),
2594            format!("{:?}", errors[0]),
2595            format!("{:?}", errors[1]),
2596            format!("{:?}", errors[2]),
2597        ];
2598
2599        for debug in debug_outputs {
2600            assert!(
2601                !debug.contains(CANARY),
2602                "sensitive canary leaked through Debug: {debug}"
2603            );
2604            assert!(
2605                debug.contains("_len") || debug.contains("_count") || debug.contains("_present"),
2606                "Debug output lacked safe structural metadata: {debug}"
2607            );
2608        }
2609
2610        for display in errors.map(|error| error.to_string()) {
2611            assert!(
2612                !display.contains(CANARY),
2613                "sensitive canary leaked through Display: {display}"
2614            );
2615        }
2616    }
2617}
2618
2619#[cfg(all(test, feature = "builtin-auth-server"))]
2620mod signer_activation_tests {
2621    use std::collections::BTreeMap;
2622    use std::future::Future;
2623    use std::pin::Pin;
2624    use std::sync::atomic::{AtomicUsize, Ordering};
2625    use std::sync::{Mutex, Weak};
2626
2627    use base64::Engine as _;
2628    use fastmcp_protocol::jose::{
2629        AttestedRs256PublicKey, ExternalRs256OperationReceipt, ExternalRs256SignDisposition,
2630        ExternalRs256SignerBackend, ExternalRs256SigningRequest, RawRs256Signature,
2631        RedactedSignerProvenance,
2632    };
2633    use ring::rand::SystemRandom;
2634    use ring::signature::{RSA_PKCS1_SHA256, RsaKeyPair};
2635
2636    use super::*;
2637    use crate::oauth::{AuthorizationRequest, CodeChallengeMethod, OAuthClient, TokenRequest};
2638
2639    const TEST_CLIENT_ID: &str = "oidc-signing-test-client";
2640    const TEST_REDIRECT_URI: &str = "http://127.0.0.1/oidc-callback";
2641    const TEST_CODE_VERIFIER: &str = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
2642    const TEST_CODE_CHALLENGE: &str = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
2643    // This is a retained public RS256 verification vector; no private key is
2644    // present here. Negative tests never ask their backend to produce a JWS.
2645    const TEST_PUBLIC_MODULUS: &str = "jlHZ9nzuIuM4aiAQSAgEJMBaYS7qm7Z_3mtGYDdzReIkzxPHHr21oeXQyUJI89eQG13fsUdyoodcuh5kmndPCrODJekfr_zgor6sNspcB88iQEqEc9yf9YAf5v-cNH1Evh82KABuWb26LMaNAzZFR3BMhMEQ1FD6fLFGAbX76Drd5_UZ-1xcU07IXEc_9zvQvOwXckhO7P5Yil1fVzLTrHye_6zTbGWvdqi45095bKPnSqjrLBCTVrUW8o02Gi6mt7Ls9pZeWx2DXV8SqV06DdlqiovtKWRooQ1zV-v7BGsLsVk6T6d-8mNMGNrh0fpNb_5kdaHphAt_Ji6eE1wQPw";
2646    const TEST_CANARY_COMPACT_JWS: &str = concat!(
2647        "eyJhbGciOiJSUzI1NiIsImtpZCI6ImZpeGVkLXJzMjU2In0.",
2648        "eyJzdWIiOiJmaXhlZC12ZWN0b3IiLCJhdWQiOiJzZXJ2ZXItcG9saWN5LWxhdGVyIn0.",
2649        "Oak9UDEtrL-pNcPIFw31uzuCoCTyXywF5i3jxDixd0gHonZYPFfSlyPwhNSTrqmlzPsL-wNFcDn1zFlug6Ae1vK_QaL-bZBSxq-lOrMDUI_5_3P_HUrngtZaNk8ru88-wdGByGm1jRZa-LfeoSkESHVKPIcQ_WT7wqhq1RX3ZrPiq9QkHFE8nWIgiIesu8DFOXsdN05rmOxHheCbDGRpf8cQAG0ZENpJvYugD-SX9Sg9Kds5HOlOt6csIQBexCeKM2rIrN0r7qCp6jx_0aevqU6rNr6oxCxCGoH3UZGJa5xRh2KeJ6NVBE9BpPW3Kdi3dEfKlKldjzlUW-zEREdeEw"
2650    );
2651    // Test-only PKCS#8 fixture for a simulated externally-custodied signer.
2652    // This module alone is cfg(test); production accepts only an embedding
2653    // supplied `ExternalRs256SignerBackend` and contains no private key path.
2654    const TEST_DYNAMIC_PRIVATE_KEY_PKCS8_B64: &str = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC2H073wfexUwbdJyzIgK9KMxPV7ocei0R/RIRJNKyENR9JlymRa1MJtEuHmR5gQaVB5UkHZTlumKxFvt5ahOWKVQYCi8Qmu9bb0MjcWGP7kasqmapZqIa0eZQX7ax4wk0i2sljuQHrIdZpfBwS2ZMpx9GoKlvLSyuC+XtpapLf3Rfz5o0PAT0Iq9QGtoKFeFGV048OTTVN+Kianun2MHy3qqAS+PXMS6Ld5DRHpGbbyvGNXXtVC6/QyR0nf5hCbKFd7i6oEYPUzaxXMkaSvAIQJ6hvcH7mkXJ9EDewTdhuVVnf6IxpRX+3w4mP0hwVXM62PHSgOjxYJoy1F1c1zBHpAgMBAAECggEAQsfCLgka6RO3vZoiyradEAkgqd5X/3QzvrMCCtHcvHG6MkLShDclcLaCx4r233bSwRHxMFwGri4fZUeywuBeRtYcaQyU9VsFUv8A2AM1TkbAy9Mi6tNn6X93NTr6diFRJYmyNPXe5Tg4Jd/Tb3oGg1h44T/+/tFmeBVBEot4pcsOqk/IeKaaEsbodSBH3bAHSuq/Pp6vwWSuzEAQoCa/SSvOJ8vwNX6xw0QDvR6PlMkfR1J5JM0zdhgZFBeWL1lTu5kJsH49ytAceCMlR/EodDSVIpYEiPJBggVFMoc3rkqeu6J0nS/TxBwyXDc/GVJn1uPna+53uV+s5Ljw39GErQKBgQDpfHnTE3tb8ph27dJ3OF1h2VjqVxB85OpXhOxhSzIo6qyZuqvt7hBVZTKo8BHWFkjK9/NzoC1MPcgUJLiBdqoI+YSnYOKkEDscRWFWQu4gSj4tmxYrvpF+HEaNw/khvcKtn8uIYKTa5Vp/5eMKT04fjthyuToIufX+6IEhZsjcdwKBgQDHru6m0u9Cv3/qDl5W7+oq7KP0I9KwHx690dcABVN+ICiXH0ZfY0QkTGP1ihtR794VqX9fofd+vGqlcl7WNULVh7J0hfYffjNP54yuW9DnM8OwuIPhamFcJST9IpphpGS6AuOaxrElS/BmM4J7IrCNqyt600MbBaV87m7Mear8nwKBgQDKXOE1aSA0rAkqoqsUO1zsLrWavYUDyl+1JPa+yK6bufGId7sFx5yOdtw2gYPj+oJyr/5ny38XIkDj/IORaairiJ9JdnZYXdztftCDNBUxFUfYvR61IUD2fUlFG4I0lURCuUltVN3s/nW2fieOSvfZ8DN3E0TSRWKI4TjyGyShtwKBgAjO+7IaPfm4zuC5T4oQPUk1dSoQ5ntkdAu1lQFoOr2ml4PLGmSc0WW0hPhQ5lGf/jEAcCD82RkbIK05tVtHsDIRMVsYibnr7EGLGlaasEVysCA8k3y/H5pb/Ry5iQvjn5nhBL9QIoJdrjYj8Y6TAizNrzZU2XH4tssjDXoxp8xLAoGARgFG1+rEzvAN+cBcYqBahxte52q6oZIB/haRnVx47O3lqBoKMsE5KQq4N1kiP14Ge+WmEs2MRXe9mZ1PNBFRZS/0+raba3yuz78SjQPo9l4MSxV53bAK73QFzaWicHPSeONcxyq8AXvSebcuIfZWGrmhImRnAtc3LZH/5aVWIrk=";
2655    const TEST_DYNAMIC_PUBLIC_MODULUS: &str = "th9O98H3sVMG3ScsyICvSjMT1e6HHotEf0SESTSshDUfSZcpkWtTCbRLh5keYEGlQeVJB2U5bpisRb7eWoTlilUGAovEJrvW29DI3Fhj-5GrKpmqWaiGtHmUF-2seMJNItrJY7kB6yHWaXwcEtmTKcfRqCpby0srgvl7aWqS390X8-aNDwE9CKvUBraChXhRldOPDk01Tfiomp7p9jB8t6qgEvj1zEui3eQ0R6Rm28rxjV17VQuv0MkdJ3-YQmyhXe4uqBGD1M2sVzJGkrwCECeob3B-5pFyfRA3sE3YblVZ3-iMaUV_t8OJj9IcFVzOtjx0oDo8WCaMtRdXNcwR6Q";
2656
2657    struct TestOnlyDynamicRs256Backend {
2658        calls: Arc<AtomicUsize>,
2659        signing_inputs: Mutex<Vec<Vec<u8>>>,
2660    }
2661
2662    impl ExternalRs256SignerBackend for TestOnlyDynamicRs256Backend {
2663        fn sign<'a>(
2664            &'a self,
2665            _: &'a Cx,
2666            request: ExternalRs256SigningRequest,
2667        ) -> Pin<Box<dyn Future<Output = ExternalRs256SignDisposition> + Send + 'a>> {
2668            Box::pin(async move {
2669                let signing_input = request.input().with_bytes(|input| input.to_vec());
2670                self.signing_inputs
2671                    .lock()
2672                    .expect("dynamic signer input capture lock")
2673                    .push(signing_input.clone());
2674                let private_key = base64::engine::general_purpose::STANDARD
2675                    .decode(TEST_DYNAMIC_PRIVATE_KEY_PKCS8_B64)
2676                    .expect("bounded test-only PKCS#8 fixture");
2677                let key_pair = RsaKeyPair::from_pkcs8(&private_key)
2678                    .expect("valid test-only external-custody key");
2679                let mut signature = vec![0_u8; key_pair.public_modulus_len()];
2680                key_pair
2681                    .sign(
2682                        &RSA_PKCS1_SHA256,
2683                        &SystemRandom::new(),
2684                        &signing_input,
2685                        &mut signature,
2686                    )
2687                    .expect("real RS256 test signing succeeds");
2688                let operation = self.calls.fetch_add(1, Ordering::AcqRel) + 1;
2689                let receipt = ExternalRs256OperationReceipt::new(
2690                    request.binding(),
2691                    u64::try_from(operation).expect("bounded test operation count"),
2692                    RedactedSignerProvenance::new("oidc-test-external-custody")
2693                        .expect("bounded redacted test provenance"),
2694                )
2695                .expect("valid dynamic test signing receipt");
2696                ExternalRs256SignDisposition::Dispatched(
2697                    RawRs256Signature::from_bytes(signature)
2698                        .expect("real RS256 fixture width is admitted"),
2699                    receipt,
2700                )
2701            })
2702        }
2703    }
2704
2705    struct FixedCanaryBackend {
2706        calls: Arc<AtomicUsize>,
2707    }
2708
2709    fn fixed_canary_disposition(
2710        request: ExternalRs256SigningRequest,
2711    ) -> ExternalRs256SignDisposition {
2712        let (input, signature) = TEST_CANARY_COMPACT_JWS
2713            .rsplit_once('.')
2714            .expect("retained test canary has signature");
2715        assert!(
2716            request
2717                .input()
2718                .with_bytes(|bytes| bytes == input.as_bytes())
2719        );
2720        let signature = base64::engine::general_purpose::URL_SAFE_NO_PAD
2721            .decode(signature)
2722            .expect("retained public canary signature");
2723        let receipt = ExternalRs256OperationReceipt::new(
2724            request.binding(),
2725            1,
2726            RedactedSignerProvenance::new("oidc-public-canary-test")
2727                .expect("bounded redacted test provenance"),
2728        )
2729        .expect("valid dispatched-operation receipt");
2730        ExternalRs256SignDisposition::Dispatched(
2731            RawRs256Signature::from_bytes(signature).expect("retained RS256 signature length"),
2732            receipt,
2733        )
2734    }
2735
2736    impl ExternalRs256SignerBackend for FixedCanaryBackend {
2737        fn sign<'a>(
2738            &'a self,
2739            _: &'a Cx,
2740            request: ExternalRs256SigningRequest,
2741        ) -> Pin<Box<dyn Future<Output = ExternalRs256SignDisposition> + Send + 'a>> {
2742            let calls = Arc::clone(&self.calls);
2743            Box::pin(async move {
2744                calls.fetch_add(1, Ordering::AcqRel);
2745                fixed_canary_disposition(request)
2746            })
2747        }
2748    }
2749
2750    struct StaleReadBackBackend {
2751        calls: Arc<AtomicUsize>,
2752        provider: Arc<Mutex<Option<Weak<OidcProvider>>>>,
2753    }
2754
2755    impl ExternalRs256SignerBackend for StaleReadBackBackend {
2756        fn sign<'a>(
2757            &'a self,
2758            _: &'a Cx,
2759            request: ExternalRs256SigningRequest,
2760        ) -> Pin<Box<dyn Future<Output = ExternalRs256SignDisposition> + Send + 'a>> {
2761            let calls = Arc::clone(&self.calls);
2762            let provider = Arc::clone(&self.provider);
2763            Box::pin(async move {
2764                calls.fetch_add(1, Ordering::AcqRel);
2765                let provider = provider
2766                    .lock()
2767                    .expect("stale read-back test provider lock")
2768                    .as_ref()
2769                    .and_then(Weak::upgrade)
2770                    .expect("activation retains stale read-back test provider");
2771                assert!(
2772                    provider
2773                        .published_jwks_document("https://fastmcp.invalid/oidc/jwks")
2774                        .is_ok()
2775                );
2776                fixed_canary_disposition(request)
2777            })
2778        }
2779    }
2780
2781    struct UnexpectedBackend {
2782        calls: Arc<AtomicUsize>,
2783    }
2784
2785    /// Test-only external verifier.  It models an embedding-owned observation
2786    /// boundary; production native HTTP tests use a loopback implementation.
2787    struct FixedReadBackVerifier {
2788        bytes: Vec<u8>,
2789        generation: u64,
2790        origin: &'static str,
2791    }
2792
2793    impl OidcJwksReadBackVerifier for FixedReadBackVerifier {
2794        fn read_back<'a>(
2795            &'a self,
2796            _: &'a Cx,
2797            endpoints: &'a [String],
2798        ) -> Pin<Box<dyn Future<Output = Result<Vec<JwksEndpointReadBack>, OidcError>> + Send + 'a>>
2799        {
2800            let bytes = self.bytes.clone();
2801            let generation = self.generation;
2802            Box::pin(async move {
2803                endpoints
2804                    .iter()
2805                    .map(|uri| {
2806                        JwksEndpointReadBack::new(
2807                            uri.clone(),
2808                            self.origin,
2809                            bytes.clone(),
2810                            generation,
2811                        )
2812                        .map_err(|_| {
2813                            OidcError::SigningError("test verifier evidence failed".to_string())
2814                        })
2815                    })
2816                    .collect()
2817            })
2818        }
2819    }
2820
2821    struct MutableReadBackVerifier {
2822        evidence: Mutex<(Vec<u8>, u64, &'static str)>,
2823    }
2824
2825    impl MutableReadBackVerifier {
2826        fn set(&self, bytes: Vec<u8>, generation: u64) {
2827            *self
2828                .evidence
2829                .lock()
2830                .expect("mutable verifier evidence lock") =
2831                (bytes, generation, "https://fastmcp.invalid");
2832        }
2833    }
2834
2835    impl OidcJwksReadBackVerifier for MutableReadBackVerifier {
2836        fn read_back<'a>(
2837            &'a self,
2838            _: &'a Cx,
2839            endpoints: &'a [String],
2840        ) -> Pin<Box<dyn Future<Output = Result<Vec<JwksEndpointReadBack>, OidcError>> + Send + 'a>>
2841        {
2842            let (bytes, generation, origin) = self
2843                .evidence
2844                .lock()
2845                .expect("mutable verifier evidence lock")
2846                .clone();
2847            Box::pin(async move {
2848                endpoints
2849                    .iter()
2850                    .map(|uri| {
2851                        JwksEndpointReadBack::new(uri.clone(), origin, bytes.clone(), generation)
2852                            .map_err(|_| {
2853                                OidcError::SigningError(
2854                                    "mutable verifier evidence failed".to_string(),
2855                                )
2856                            })
2857                    })
2858                    .collect()
2859            })
2860        }
2861    }
2862
2863    /// In-repo durable-store contract fixture: two providers can share one
2864    /// store across simulated process replacement and exercise CAS fencing.
2865    #[derive(Default)]
2866    struct TestDurableActivationStore {
2867        record: Mutex<Option<OidcSigningActivationStoreRecord>>,
2868    }
2869
2870    impl OidcSigningActivationStore for TestDurableActivationStore {
2871        fn load(
2872            &self,
2873            cx: &Cx,
2874            issuer: &str,
2875        ) -> Result<Option<OidcSigningActivationStoreRecord>, OidcError> {
2876            cx.checkpoint().map_err(|_| {
2877                OidcError::SigningError("test activation store cancelled".to_string())
2878            })?;
2879            Ok(self
2880                .record
2881                .lock()
2882                .map_err(|_| {
2883                    OidcError::SigningError("test activation store unavailable".to_string())
2884                })?
2885                .clone()
2886                .filter(|record| record.issuer() == issuer))
2887        }
2888
2889        fn compare_and_set(
2890            &self,
2891            cx: &Cx,
2892            expected_generation: Option<u64>,
2893            next: OidcSigningActivationStoreRecord,
2894        ) -> Result<OidcSigningActivationStoreRecord, OidcError> {
2895            cx.checkpoint().map_err(|_| {
2896                OidcError::SigningError("test activation store cancelled".to_string())
2897            })?;
2898            let mut record = self.record.lock().map_err(|_| {
2899                OidcError::SigningError("test activation store unavailable".to_string())
2900            })?;
2901            if record
2902                .as_ref()
2903                .map(OidcSigningActivationStoreRecord::activation_generation)
2904                != expected_generation
2905            {
2906                return Err(OidcError::SigningError(
2907                    "test activation CAS lost".to_string(),
2908                ));
2909            }
2910            *record = Some(next.clone());
2911            Ok(next)
2912        }
2913    }
2914
2915    impl ExternalRs256SignerBackend for UnexpectedBackend {
2916        fn sign<'a>(
2917            &'a self,
2918            _: &'a Cx,
2919            _: ExternalRs256SigningRequest,
2920        ) -> Pin<Box<dyn Future<Output = ExternalRs256SignDisposition> + Send + 'a>> {
2921            let calls = Arc::clone(&self.calls);
2922            Box::pin(async move {
2923                calls.fetch_add(1, Ordering::AcqRel);
2924                panic!("a rejected OIDC issuance must not dispatch external signing")
2925            })
2926        }
2927    }
2928
2929    struct CancellationAfterDispatchBackend {
2930        calls: Arc<AtomicUsize>,
2931    }
2932
2933    impl ExternalRs256SignerBackend for CancellationAfterDispatchBackend {
2934        fn sign<'a>(
2935            &'a self,
2936            cx: &'a Cx,
2937            request: ExternalRs256SigningRequest,
2938        ) -> Pin<Box<dyn Future<Output = ExternalRs256SignDisposition> + Send + 'a>> {
2939            let calls = Arc::clone(&self.calls);
2940            Box::pin(async move {
2941                calls.fetch_add(1, Ordering::AcqRel);
2942                cx.set_cancel_requested(true);
2943                let receipt = ExternalRs256OperationReceipt::new(
2944                    request.binding(),
2945                    1,
2946                    RedactedSignerProvenance::new("oidc-cancellation-test")
2947                        .expect("bounded redacted test provenance"),
2948                )
2949                .expect("valid dispatched-operation receipt");
2950                // This is intentionally not a signature: cancellation must
2951                // consume the dispatched attempt before any JWS can exist.
2952                ExternalRs256SignDisposition::Dispatched(
2953                    RawRs256Signature::from_bytes(vec![0_u8; 256])
2954                        .expect("bounded cancellation-path bytes"),
2955                    receipt,
2956                )
2957            })
2958        }
2959    }
2960
2961    struct CancelIdTokenAfterSigningBackend {
2962        calls: Arc<AtomicUsize>,
2963    }
2964
2965    impl ExternalRs256SignerBackend for CancelIdTokenAfterSigningBackend {
2966        fn sign<'a>(
2967            &'a self,
2968            cx: &'a Cx,
2969            request: ExternalRs256SigningRequest,
2970        ) -> Pin<Box<dyn Future<Output = ExternalRs256SignDisposition> + Send + 'a>> {
2971            let calls = Arc::clone(&self.calls);
2972            Box::pin(async move {
2973                calls.fetch_add(1, Ordering::AcqRel);
2974                let is_canary = request.input().with_bytes(|input| {
2975                    TEST_CANARY_COMPACT_JWS
2976                        .rsplit_once('.')
2977                        .is_some_and(|(canary_input, _)| input == canary_input.as_bytes())
2978                });
2979                if is_canary {
2980                    return fixed_canary_disposition(request);
2981                }
2982                cx.set_cancel_requested(true);
2983                let receipt = ExternalRs256OperationReceipt::new(
2984                    request.binding(),
2985                    2,
2986                    RedactedSignerProvenance::new("oidc-id-token-cancel-test")
2987                        .expect("bounded redacted test provenance"),
2988                )
2989                .expect("valid dispatched-operation receipt");
2990                ExternalRs256SignDisposition::Dispatched(
2991                    RawRs256Signature::from_bytes(vec![0_u8; 256])
2992                        .expect("bounded cancellation-path bytes"),
2993                    receipt,
2994                )
2995            })
2996        }
2997    }
2998
2999    fn test_signer_with_binding(
3000        backend: Arc<dyn ExternalRs256SignerBackend>,
3001        binding: Rs256SigningBinding,
3002    ) -> Arc<ExternalRs256Signer> {
3003        test_signer_with_kid_and_binding(backend, "fixed-rs256", binding)
3004    }
3005
3006    fn test_signer_with_kid_and_binding(
3007        backend: Arc<dyn ExternalRs256SignerBackend>,
3008        kid: &str,
3009        binding: Rs256SigningBinding,
3010    ) -> Arc<ExternalRs256Signer> {
3011        let modulus = base64::engine::general_purpose::URL_SAFE_NO_PAD
3012            .decode(TEST_PUBLIC_MODULUS)
3013            .expect("retained public verification modulus");
3014        let key = AttestedRs256PublicKey::admit(
3015            kid,
3016            modulus,
3017            binding,
3018            RedactedSignerProvenance::new("oidc-test-adapter")
3019                .expect("bounded redacted test provenance"),
3020        )
3021        .expect("retained public verification key admits");
3022        Arc::new(ExternalRs256Signer::new(backend, key))
3023    }
3024
3025    fn test_signer(backend: Arc<dyn ExternalRs256SignerBackend>) -> Arc<ExternalRs256Signer> {
3026        let binding =
3027            Rs256SigningBinding::new(11, 12, 13, 14).expect("nonzero external signer generations");
3028        test_signer_with_binding(backend, binding)
3029    }
3030
3031    fn dynamic_test_signer(
3032        backend: Arc<dyn ExternalRs256SignerBackend>,
3033    ) -> Arc<ExternalRs256Signer> {
3034        dynamic_test_signer_with_kid(
3035            backend,
3036            "dynamic-external-rs256",
3037            Rs256SigningBinding::new(21, 22, 23, 24).expect("nonzero dynamic signer generations"),
3038        )
3039    }
3040
3041    fn dynamic_test_signer_with_kid(
3042        backend: Arc<dyn ExternalRs256SignerBackend>,
3043        kid: &str,
3044        binding: Rs256SigningBinding,
3045    ) -> Arc<ExternalRs256Signer> {
3046        let modulus = base64::engine::general_purpose::URL_SAFE_NO_PAD
3047            .decode(TEST_DYNAMIC_PUBLIC_MODULUS)
3048            .expect("test-only external-custody public modulus");
3049        let key = AttestedRs256PublicKey::admit(
3050            kid,
3051            modulus,
3052            binding,
3053            RedactedSignerProvenance::new("oidc-dynamic-test-adapter")
3054                .expect("bounded redacted test provenance"),
3055        )
3056        .expect("test-only external-custody key admits");
3057        Arc::new(ExternalRs256Signer::new(backend, key))
3058    }
3059
3060    fn issue_access_token(scopes: &[&str]) -> (Arc<OAuthServer>, crate::oauth::TokenResponse) {
3061        let oauth = Arc::new(OAuthServer::with_defaults());
3062        oauth
3063            .register_client(
3064                OAuthClient::builder(TEST_CLIENT_ID)
3065                    .redirect_uri(TEST_REDIRECT_URI)
3066                    .scopes(scopes.iter().copied())
3067                    .build()
3068                    .expect("valid test client"),
3069            )
3070            .expect("register test client");
3071        let (code, _) = oauth
3072            .authorize(&AuthorizationRequest {
3073                response_type: "code".to_string(),
3074                client_id: TEST_CLIENT_ID.to_string(),
3075                redirect_uri: TEST_REDIRECT_URI.to_string(),
3076                scopes: scopes.iter().map(|scope| (*scope).to_string()).collect(),
3077                resource: None,
3078                state: Some("oidc-test-state".to_string()),
3079                code_challenge: TEST_CODE_CHALLENGE.to_string(),
3080                code_challenge_method: CodeChallengeMethod::S256,
3081            })
3082            .expect("authorize test access token");
3083        let response = oauth
3084            .token(&TokenRequest {
3085                grant_type: "authorization_code".to_string(),
3086                code: Some(code),
3087                redirect_uri: Some(TEST_REDIRECT_URI.to_string()),
3088                client_id: TEST_CLIENT_ID.to_string(),
3089                client_secret: None,
3090                code_verifier: Some(TEST_CODE_VERIFIER.to_string()),
3091                refresh_token: None,
3092                scopes: None,
3093                resource: None,
3094            })
3095            .expect("exchange test access token");
3096        (oauth, response)
3097    }
3098
3099    #[test]
3100    fn durable_key_expiry_record_is_publicly_constructible_and_bounded() {
3101        let signer = test_signer(Arc::new(UnexpectedBackend {
3102            calls: Arc::new(AtomicUsize::new(0)),
3103        }));
3104        let watermark = OidcSigningKeyExpiry::new(
3105            99,
3106            signer
3107                .canonical_public_jwks()
3108                .expect("canonical public identity")
3109                .as_bytes()
3110                .to_vec(),
3111        )
3112        .expect("bounded public watermark");
3113        let record = OidcSigningActivationStoreRecord::new(
3114            "https://fastmcp.invalid/",
3115            1,
3116            1,
3117            OidcSigningActivationStatus::Active,
3118            99,
3119            BTreeMap::from([("fixed-rs256".to_string(), watermark.clone())]),
3120        )
3121        .expect("embedding-owned durable store can reconstruct a record");
3122        assert_eq!(
3123            record
3124                .key_id_maximum_id_token_expires_at()
3125                .get("fixed-rs256")
3126                .map(OidcSigningKeyExpiry::expires_at),
3127            Some(99),
3128        );
3129
3130        let too_many = (0..=fastmcp_protocol::jose::MAX_JWKS_KEYS)
3131            .map(|index| (format!("key-{index}"), watermark.clone()))
3132            .collect();
3133        assert!(
3134            OidcSigningActivationStoreRecord::new(
3135                "https://fastmcp.invalid/",
3136                1,
3137                1,
3138                OidcSigningActivationStatus::Active,
3139                99,
3140                too_many,
3141            )
3142            .is_err()
3143        );
3144    }
3145
3146    fn provider_with_published_jwks(
3147        oauth: Arc<OAuthServer>,
3148        signer: Arc<ExternalRs256Signer>,
3149    ) -> OidcProvider {
3150        provider_with_published_jwks_observed_generation(oauth, signer, None)
3151    }
3152
3153    fn provider_with_published_jwks_observed_generation(
3154        oauth: Arc<OAuthServer>,
3155        signer: Arc<ExternalRs256Signer>,
3156        observed_generation: Option<u64>,
3157    ) -> OidcProvider {
3158        let signer_ring_generation = signer.binding().ring_generation();
3159        let key_ring = Rs256PublicKeyRing::new(signer, Vec::new(), signer_ring_generation)
3160            .expect("single signer test key ring");
3161        provider_with_published_key_ring_observed_generation(oauth, key_ring, observed_generation)
3162    }
3163
3164    fn provider_with_published_key_ring_observed_generation(
3165        oauth: Arc<OAuthServer>,
3166        key_ring: Rs256PublicKeyRing,
3167        observed_generation: Option<u64>,
3168    ) -> OidcProvider {
3169        let provider = OidcProvider::with_defaults(oauth).expect("OIDC provider");
3170        let canonical = key_ring
3171            .canonical_public_jwks()
3172            .expect("canonical external public JWKS");
3173        provider
3174            .set_id_token_signing_activation_dependencies(
3175                Arc::new(FixedReadBackVerifier {
3176                    bytes: canonical.as_bytes().to_vec(),
3177                    generation: observed_generation.unwrap_or(key_ring.generation()),
3178                    origin: "https://fastmcp.invalid",
3179                }),
3180                Arc::new(TestDurableActivationStore::default()),
3181            )
3182            .expect("install external verifier and durable activation store");
3183        provider
3184            .begin_id_token_signing_key_ring_activation(
3185                key_ring,
3186                vec!["https://fastmcp.invalid/oidc/jwks".to_string()],
3187            )
3188            .expect("begin OIDC signer publication");
3189        provider
3190            .publish_id_token_signing_key_ring_jwks(canonical)
3191            .expect("publish exact canonical JWKS");
3192        provider
3193    }
3194
3195    fn activate_provider(provider: &OidcProvider) -> Result<(), OidcError> {
3196        assert!(
3197            provider
3198                .published_jwks_document("https://fastmcp.invalid/oidc/jwks")
3199                .is_ok()
3200        );
3201        let cx = Cx::for_testing();
3202        fastmcp_core::block_on(provider.activate_id_token_signing(&cx, signing_deadline())).0
3203    }
3204
3205    fn signing_deadline() -> ExternalRs256SigningDeadline {
3206        ExternalRs256SigningDeadline::new(std::time::Duration::from_secs(1))
3207            .expect("bounded test deadline")
3208    }
3209
3210    #[test]
3211    fn forged_revoked_refresh_and_non_openid_credentials_never_dispatch_signing() {
3212        let calls = Arc::new(AtomicUsize::new(0));
3213        let signer = test_signer(Arc::new(UnexpectedBackend {
3214            calls: Arc::clone(&calls),
3215        }));
3216        let (oauth, issued) = issue_access_token(&["openid"]);
3217        let refresh = issued
3218            .refresh_token
3219            .as_deref()
3220            .expect("refresh credential")
3221            .to_string();
3222        let provider = provider_with_published_jwks(Arc::clone(&oauth), signer);
3223        let cx = Cx::for_testing();
3224        let refresh_result = fastmcp_core::block_on(provider.issue_id_token(
3225            &cx,
3226            &refresh,
3227            None,
3228            signing_deadline(),
3229        ))
3230        .0;
3231        assert!(matches!(
3232            refresh_result,
3233            Err(OidcError::OAuth(OAuthError::InvalidGrant(_)))
3234        ));
3235        oauth
3236            .revoke(&issued.access_token, TEST_CLIENT_ID, None)
3237            .expect("revoke owned access credential");
3238
3239        for credential in ["forged-opaque-credential".to_string(), issued.access_token] {
3240            let cx = Cx::for_testing();
3241            let result = fastmcp_core::block_on(provider.issue_id_token(
3242                &cx,
3243                &credential,
3244                None,
3245                signing_deadline(),
3246            ))
3247            .0;
3248            assert!(matches!(
3249                result,
3250                Err(OidcError::OAuth(OAuthError::InvalidGrant(_)))
3251            ));
3252        }
3253        assert_eq!(calls.load(Ordering::Acquire), 0);
3254
3255        let calls = Arc::new(AtomicUsize::new(0));
3256        let signer = test_signer(Arc::new(UnexpectedBackend {
3257            calls: Arc::clone(&calls),
3258        }));
3259        let (oauth, issued) = issue_access_token(&[]);
3260        let provider = provider_with_published_jwks(oauth, signer);
3261        let cx = Cx::for_testing();
3262        let result = fastmcp_core::block_on(provider.issue_id_token(
3263            &cx,
3264            &issued.access_token,
3265            None,
3266            signing_deadline(),
3267        ))
3268        .0;
3269        assert!(matches!(result, Err(OidcError::MissingOpenIdScope)));
3270        assert_eq!(calls.load(Ordering::Acquire), 0);
3271    }
3272
3273    #[test]
3274    fn rh5_one_field_wrong_read_back_origin_cannot_commit_activation() {
3275        let calls = Arc::new(AtomicUsize::new(0));
3276        let signer = test_signer(Arc::new(FixedCanaryBackend {
3277            calls: Arc::clone(&calls),
3278        }));
3279        let store = Arc::new(TestDurableActivationStore::default());
3280        let provider = OidcProvider::with_defaults(Arc::new(OAuthServer::with_defaults()))
3281            .expect("OIDC provider");
3282        let canonical = signer
3283            .canonical_public_jwks()
3284            .expect("canonical external public JWKS");
3285        provider
3286            .set_id_token_signing_activation_dependencies(
3287                Arc::new(FixedReadBackVerifier {
3288                    bytes: canonical.as_bytes().to_vec(),
3289                    generation: signer.binding().ring_generation(),
3290                    // Only this receipt field differs from the configured
3291                    // endpoint: the verifier attests a sibling origin.
3292                    origin: "https://fastmcp.invalid.evil",
3293                }),
3294                Arc::clone(&store),
3295            )
3296            .expect("install test verifier and durable store");
3297        provider
3298            .begin_id_token_signing_activation(signer, "https://fastmcp.invalid/oidc/jwks")
3299            .expect("begin pending activation");
3300        provider
3301            .publish_id_token_signing_jwks(canonical)
3302            .expect("publish exact canonical JWKS");
3303        assert!(matches!(
3304            fastmcp_core::block_on(
3305                provider.activate_id_token_signing(&Cx::for_testing(), signing_deadline(),)
3306            )
3307            .0,
3308            Err(OidcError::SigningError(_))
3309        ));
3310        assert!(provider.activated_jwks_document().is_err());
3311        assert!(
3312            store
3313                .load(&Cx::for_testing(), "https://fastmcp.invalid/")
3314                .expect("durable store remains readable")
3315                .is_none()
3316        );
3317        assert_eq!(calls.load(Ordering::Acquire), 1);
3318    }
3319
3320    #[test]
3321    fn rh5_no_publish_never_serves_or_activates_a_pending_signer() {
3322        let calls = Arc::new(AtomicUsize::new(0));
3323        let signer = test_signer(Arc::new(UnexpectedBackend {
3324            calls: Arc::clone(&calls),
3325        }));
3326        let provider = OidcProvider::with_defaults(Arc::new(OAuthServer::with_defaults()))
3327            .expect("OIDC provider");
3328        provider
3329            .begin_id_token_signing_activation(signer, "https://fastmcp.invalid/oidc/jwks")
3330            .expect("begin Pending activation");
3331
3332        assert!(
3333            provider
3334                .published_jwks_document("https://fastmcp.invalid/oidc/jwks")
3335                .is_err()
3336        );
3337        assert!(matches!(
3338            fastmcp_core::block_on(
3339                provider.activate_id_token_signing(&Cx::for_testing(), signing_deadline(),)
3340            )
3341            .0,
3342            Err(OidcError::SigningError(_))
3343        ));
3344        assert!(provider.activated_jwks_document().is_err());
3345        assert_eq!(calls.load(Ordering::Acquire), 0);
3346    }
3347
3348    #[test]
3349    fn rh5_stale_signer_generation_cannot_publish_or_activate() {
3350        let calls = Arc::new(AtomicUsize::new(0));
3351        let pending_signer = test_signer(Arc::new(UnexpectedBackend {
3352            calls: Arc::clone(&calls),
3353        }));
3354        let stale_signer = test_signer_with_binding(
3355            Arc::new(UnexpectedBackend {
3356                calls: Arc::clone(&calls),
3357            }),
3358            Rs256SigningBinding::new(11, 12, 14, 14).expect("different well-formed key generation"),
3359        );
3360        let provider = OidcProvider::with_defaults(Arc::new(OAuthServer::with_defaults()))
3361            .expect("OIDC provider");
3362        provider
3363            .begin_id_token_signing_activation(pending_signer, "https://fastmcp.invalid/oidc/jwks")
3364            .expect("begin pending activation");
3365        let stale_canonical = stale_signer
3366            .canonical_public_jwks()
3367            .expect("stale signer canonical JWKS");
3368        assert!(matches!(
3369            provider.publish_id_token_signing_jwks(stale_canonical),
3370            Err(OidcError::SigningError(_))
3371        ));
3372        assert!(
3373            provider
3374                .published_jwks_document("https://fastmcp.invalid/oidc/jwks")
3375                .is_err()
3376        );
3377        assert!(provider.activated_jwks_document().is_err());
3378        assert_eq!(calls.load(Ordering::Acquire), 0);
3379    }
3380
3381    #[test]
3382    fn rh5_one_field_stale_read_back_generation_cannot_commit_durable_activation() {
3383        let calls = Arc::new(AtomicUsize::new(0));
3384        let signer = test_signer(Arc::new(FixedCanaryBackend {
3385            calls: Arc::clone(&calls),
3386        }));
3387        let canonical = signer
3388            .canonical_public_jwks()
3389            .expect("canonical public JWKS");
3390        let store = Arc::new(TestDurableActivationStore::default());
3391        let provider = OidcProvider::with_defaults(Arc::new(OAuthServer::with_defaults()))
3392            .expect("OIDC provider");
3393        provider
3394            .set_id_token_signing_activation_dependencies(
3395                Arc::new(FixedReadBackVerifier {
3396                    bytes: canonical.as_bytes().to_vec(),
3397                    // This is the only changed receipt field: the advertised
3398                    // key-ring generation is 14, while read-back says 12.
3399                    generation: 12,
3400                    origin: "https://fastmcp.invalid",
3401                }),
3402                Arc::clone(&store),
3403            )
3404            .expect("install verifier and durable activation store");
3405        provider
3406            .begin_id_token_signing_activation(signer, "https://fastmcp.invalid/oidc/jwks")
3407            .expect("begin pending activation");
3408        provider
3409            .publish_id_token_signing_jwks(canonical)
3410            .expect("publish exact public JWKS");
3411
3412        let cx = Cx::for_testing();
3413        assert!(matches!(
3414            fastmcp_core::block_on(provider.activate_id_token_signing(&cx, signing_deadline())).0,
3415            Err(OidcError::SigningError(_))
3416        ));
3417        assert!(provider.activated_jwks_document().is_err());
3418        assert!(
3419            store
3420                .load(&Cx::for_testing(), "https://fastmcp.invalid/")
3421                .expect("durable store remains readable")
3422                .is_none()
3423        );
3424        assert_eq!(calls.load(Ordering::Acquire), 1);
3425    }
3426
3427    #[test]
3428    fn external_read_back_receipt_and_canary_verification_activate_exact_signer() {
3429        let calls = Arc::new(AtomicUsize::new(0));
3430        let signer = test_signer(Arc::new(FixedCanaryBackend {
3431            calls: Arc::clone(&calls),
3432        }));
3433        let canonical = signer
3434            .canonical_public_jwks()
3435            .expect("canonical public JWKS");
3436        let provider = provider_with_published_jwks(
3437            Arc::new(OAuthServer::with_defaults()),
3438            Arc::clone(&signer),
3439        );
3440
3441        assert_eq!(
3442            provider
3443                .published_jwks_document("https://fastmcp.invalid/oidc/jwks")
3444                .expect("bound public JWKS read-back"),
3445            canonical.as_bytes(),
3446        );
3447        let cx = Cx::for_testing();
3448        assert!(
3449            fastmcp_core::block_on(provider.activate_id_token_signing(&cx, signing_deadline()))
3450                .0
3451                .is_ok()
3452        );
3453        assert_eq!(
3454            provider.activated_jwks_document().expect("active JWKS"),
3455            canonical.as_bytes()
3456        );
3457        assert_eq!(calls.load(Ordering::Acquire), 1);
3458    }
3459
3460    #[test]
3461    fn durable_revocation_fences_id_token_before_external_signing() {
3462        let calls = Arc::new(AtomicUsize::new(0));
3463        let signer = test_signer(Arc::new(FixedCanaryBackend {
3464            calls: Arc::clone(&calls),
3465        }));
3466        let canonical = signer
3467            .canonical_public_jwks()
3468            .expect("canonical public JWKS");
3469        let store = Arc::new(TestDurableActivationStore::default());
3470        let (oauth, issued) = issue_access_token(&["openid"]);
3471        let provider = OidcProvider::with_defaults(Arc::clone(&oauth)).expect("OIDC provider");
3472        provider
3473            .set_id_token_signing_activation_dependencies(
3474                Arc::new(FixedReadBackVerifier {
3475                    bytes: canonical.as_bytes().to_vec(),
3476                    generation: signer.binding().ring_generation(),
3477                    origin: "https://fastmcp.invalid",
3478                }),
3479                Arc::clone(&store),
3480            )
3481            .expect("install activation dependencies");
3482        provider
3483            .begin_id_token_signing_activation(signer, "https://fastmcp.invalid/oidc/jwks")
3484            .expect("begin pending activation");
3485        provider
3486            .publish_id_token_signing_jwks(canonical)
3487            .expect("publish canonical JWKS");
3488        activate_provider(&provider).expect("activate signer before durable revocation");
3489        let prior = store
3490            .load(&Cx::for_testing(), "https://fastmcp.invalid/")
3491            .expect("load active durable record")
3492            .expect("active durable record");
3493        let revoked = OidcSigningActivationStoreRecord::new(
3494            prior.issuer().to_string(),
3495            prior.key_ring_generation(),
3496            prior
3497                .activation_generation()
3498                .checked_add(1)
3499                .expect("bounded test generation"),
3500            OidcSigningActivationStatus::Revoked,
3501            prior.maximum_id_token_expires_at(),
3502            prior.key_id_maximum_id_token_expires_at().clone(),
3503        )
3504        .expect("bounded revoked durable record");
3505        *store.record.lock().expect("test durable store lock") = Some(revoked);
3506
3507        let result = fastmcp_core::block_on(provider.issue_id_token(
3508            &Cx::for_testing(),
3509            &issued.access_token,
3510            None,
3511            signing_deadline(),
3512        ))
3513        .0;
3514        assert!(matches!(result, Err(OidcError::SigningError(_))));
3515        assert_eq!(
3516            calls.load(Ordering::Acquire),
3517            1,
3518            "only activation canary dispatched"
3519        );
3520    }
3521
3522    #[test]
3523    fn activated_external_custody_backend_issues_dynamic_verified_id_token() {
3524        let calls = Arc::new(AtomicUsize::new(0));
3525        let backend = Arc::new(TestOnlyDynamicRs256Backend {
3526            calls: Arc::clone(&calls),
3527            signing_inputs: Mutex::new(Vec::new()),
3528        });
3529        let signer =
3530            dynamic_test_signer(Arc::clone(&backend) as Arc<dyn ExternalRs256SignerBackend>);
3531        let retained = test_signer(Arc::new(UnexpectedBackend {
3532            calls: Arc::new(AtomicUsize::new(0)),
3533        }));
3534        let key_ring = Rs256PublicKeyRing::new(signer, vec![retained], 24)
3535            .expect("dynamic active signer retains one published verification key");
3536        let (oauth, issued) = issue_access_token(&["openid"]);
3537        let expected_subject = oauth
3538            .validate_access_token(&issued.access_token)
3539            .expect("test access token remains open")
3540            .subject
3541            .expect("OIDC access token carries a subject");
3542        let provider = provider_with_published_key_ring_observed_generation(
3543            Arc::clone(&oauth),
3544            key_ring,
3545            None,
3546        );
3547        activate_provider(&provider).expect("dynamic external signer activates through read-back");
3548
3549        let token = fastmcp_core::block_on(provider.issue_id_token(
3550            &Cx::for_testing(),
3551            &issued.access_token,
3552            Some("dynamic-nonce"),
3553            signing_deadline(),
3554        ))
3555        .0
3556        .expect("active externally-custodied signer issues a compact ID token");
3557        let public_jwks = provider
3558            .activated_jwks_document()
3559            .expect("published overlapping JWKS remains active");
3560        let read_back_keys =
3561            AdmittedRsaJwks::from_json(&public_jwks).expect("published JWKS admits");
3562        assert_eq!(
3563            read_back_keys.len(),
3564            2,
3565            "active plus retained public key overlap"
3566        );
3567        let verified = verify_compact_jws_rs256(&token.raw, &read_back_keys)
3568            .expect("dynamic compact ID token verifies against published JWKS");
3569        let payload = verified.claims();
3570
3571        assert_eq!(token.claims.iss, provider.config().issuer);
3572        assert_eq!(token.claims.sub, expected_subject);
3573        assert_eq!(token.claims.aud, TEST_CLIENT_ID);
3574        assert_eq!(
3575            payload.get("iss"),
3576            Some(&serde_json::json!(token.claims.iss))
3577        );
3578        assert_eq!(
3579            payload.get("sub"),
3580            Some(&serde_json::json!(token.claims.sub))
3581        );
3582        assert_eq!(payload.get("aud"), Some(&serde_json::json!(TEST_CLIENT_ID)));
3583        assert_eq!(
3584            payload.get("iat"),
3585            Some(&serde_json::json!(token.claims.iat))
3586        );
3587        assert_eq!(
3588            payload.get("exp"),
3589            Some(&serde_json::json!(token.claims.exp))
3590        );
3591        assert_eq!(
3592            payload.get("nonce"),
3593            Some(&serde_json::json!("dynamic-nonce"))
3594        );
3595        assert!(token.claims.exp > token.claims.iat);
3596
3597        let signing_input = token
3598            .raw
3599            .rsplit_once('.')
3600            .expect("compact ID token has a signature")
3601            .0;
3602        let captured_inputs = backend
3603            .signing_inputs
3604            .lock()
3605            .expect("dynamic signer input capture lock");
3606        assert_eq!(
3607            captured_inputs.len(),
3608            2,
3609            "one canary plus one ID-token operation"
3610        );
3611        assert_eq!(captured_inputs[1], signing_input.as_bytes());
3612        assert_ne!(captured_inputs[0], captured_inputs[1]);
3613        assert_eq!(calls.load(Ordering::Acquire), 2);
3614    }
3615
3616    #[test]
3617    fn active_generation_rotates_through_published_successor_with_public_overlap() {
3618        let active_calls = Arc::new(AtomicUsize::new(0));
3619        let active = test_signer(Arc::new(FixedCanaryBackend {
3620            calls: Arc::clone(&active_calls),
3621        }));
3622        let initial_jwks = active
3623            .canonical_public_jwks()
3624            .expect("initial canonical JWKS");
3625        let verifier = Arc::new(MutableReadBackVerifier {
3626            evidence: Mutex::new((
3627                initial_jwks.as_bytes().to_vec(),
3628                active.binding().ring_generation(),
3629                "https://fastmcp.invalid",
3630            )),
3631        });
3632        let provider = OidcProvider::with_defaults(Arc::new(OAuthServer::with_defaults()))
3633            .expect("OIDC provider");
3634        provider
3635            .set_id_token_signing_activation_dependencies(
3636                Arc::clone(&verifier),
3637                Arc::new(TestDurableActivationStore::default()),
3638            )
3639            .expect("install mutable read-back verifier");
3640        provider
3641            .begin_id_token_signing_activation(
3642                Arc::clone(&active),
3643                "https://fastmcp.invalid/oidc/jwks",
3644            )
3645            .expect("begin initial Pending activation");
3646        provider
3647            .publish_id_token_signing_jwks(initial_jwks)
3648            .expect("publish initial public JWKS");
3649        activate_provider(&provider).expect("initial external signer activates");
3650
3651        let successor_calls = Arc::new(AtomicUsize::new(0));
3652        let successor_backend = Arc::new(TestOnlyDynamicRs256Backend {
3653            calls: Arc::clone(&successor_calls),
3654            signing_inputs: Mutex::new(Vec::new()),
3655        });
3656        let successor = dynamic_test_signer(
3657            Arc::clone(&successor_backend) as Arc<dyn ExternalRs256SignerBackend>
3658        );
3659        let successor_ring = Rs256PublicKeyRing::new(successor, vec![active], 24)
3660            .expect("successor ring retains active verification key");
3661        let successor_jwks = successor_ring
3662            .canonical_public_jwks()
3663            .expect("successor canonical overlapping JWKS");
3664        provider
3665            .begin_id_token_signing_key_ring_rotation(
3666                successor_ring,
3667                vec!["https://fastmcp.invalid/oidc/jwks".to_string()],
3668            )
3669            .expect("Active generation accepts a successor Pending transition");
3670        verifier.set(
3671            successor_jwks.as_bytes().to_vec(),
3672            successor_jwks.generation(),
3673        );
3674        provider
3675            .publish_id_token_signing_key_ring_jwks(successor_jwks)
3676            .expect("successor reaches Published with public overlap");
3677        assert_eq!(
3678            AdmittedRsaJwks::from_json(
3679                &provider
3680                    .published_jwks_document("https://fastmcp.invalid/oidc/jwks")
3681                    .expect("published successor JWKS"),
3682            )
3683            .expect("published successor JWKS admits")
3684            .len(),
3685            2,
3686        );
3687        activate_provider(&provider).expect("successor becomes Active after external read-back");
3688        assert_eq!(
3689            AdmittedRsaJwks::from_json(
3690                &provider
3691                    .activated_jwks_document()
3692                    .expect("active successor JWKS"),
3693            )
3694            .expect("active successor JWKS admits")
3695            .len(),
3696            2,
3697        );
3698        assert_eq!(active_calls.load(Ordering::Acquire), 1);
3699        assert_eq!(successor_calls.load(Ordering::Acquire), 1);
3700    }
3701
3702    #[test]
3703    fn rh5_second_rotation_retains_older_live_key_and_carries_durable_expiry() {
3704        let initial_calls = Arc::new(AtomicUsize::new(0));
3705        let initial_backend = Arc::new(TestOnlyDynamicRs256Backend {
3706            calls: Arc::clone(&initial_calls),
3707            signing_inputs: Mutex::new(Vec::new()),
3708        });
3709        let initial = dynamic_test_signer_with_kid(
3710            Arc::clone(&initial_backend) as Arc<dyn ExternalRs256SignerBackend>,
3711            "dynamic-old",
3712            Rs256SigningBinding::new(21, 22, 23, 24).expect("initial dynamic signer generations"),
3713        );
3714        let initial_jwks = initial
3715            .canonical_public_jwks()
3716            .expect("initial dynamic JWKS");
3717        let verifier = Arc::new(MutableReadBackVerifier {
3718            evidence: Mutex::new((
3719                initial_jwks.as_bytes().to_vec(),
3720                initial.binding().ring_generation(),
3721                "https://fastmcp.invalid",
3722            )),
3723        });
3724        let store = Arc::new(TestDurableActivationStore::default());
3725        let (oauth, issued) = issue_access_token(&["openid"]);
3726        let provider = OidcProvider::with_defaults(Arc::clone(&oauth)).expect("OIDC provider");
3727        provider
3728            .set_id_token_signing_activation_dependencies(Arc::clone(&verifier), Arc::clone(&store))
3729            .expect("install mutable verifier and durable store");
3730        provider
3731            .begin_id_token_signing_activation(
3732                Arc::clone(&initial),
3733                "https://fastmcp.invalid/oidc/jwks",
3734            )
3735            .expect("begin initial activation");
3736        provider
3737            .publish_id_token_signing_jwks(initial_jwks)
3738            .expect("publish initial dynamic JWKS");
3739        activate_provider(&provider).expect("activate initial dynamic signer");
3740        let issued_token = fastmcp_core::block_on(provider.issue_id_token(
3741            &Cx::for_testing(),
3742            &issued.access_token,
3743            Some("rotation-live-key"),
3744            signing_deadline(),
3745        ))
3746        .0
3747        .expect("issue token under the oldest key");
3748
3749        let middle_calls = Arc::new(AtomicUsize::new(0));
3750        let middle = test_signer(Arc::new(FixedCanaryBackend {
3751            calls: Arc::clone(&middle_calls),
3752        }));
3753        let first_successor =
3754            Rs256PublicKeyRing::new(Arc::clone(&middle), vec![Arc::clone(&initial)], 25)
3755                .expect("first successor retains the issued-token key");
3756        let first_successor_jwks = first_successor
3757            .canonical_public_jwks()
3758            .expect("first successor JWKS");
3759        provider
3760            .begin_id_token_signing_key_ring_rotation(
3761                first_successor,
3762                vec!["https://fastmcp.invalid/oidc/jwks".to_string()],
3763            )
3764            .expect("first successor begins from Active");
3765        verifier.set(
3766            first_successor_jwks.as_bytes().to_vec(),
3767            first_successor_jwks.generation(),
3768        );
3769        provider
3770            .publish_id_token_signing_key_ring_jwks(first_successor_jwks)
3771            .expect("first successor publishes overlap");
3772        activate_provider(&provider).expect("first successor activates");
3773
3774        let final_calls = Arc::new(AtomicUsize::new(0));
3775        let final_backend = Arc::new(TestOnlyDynamicRs256Backend {
3776            calls: Arc::clone(&final_calls),
3777            signing_inputs: Mutex::new(Vec::new()),
3778        });
3779        let final_signer = dynamic_test_signer_with_kid(
3780            Arc::clone(&final_backend) as Arc<dyn ExternalRs256SignerBackend>,
3781            "dynamic-successor",
3782            Rs256SigningBinding::new(31, 32, 33, 34).expect("final dynamic signer generations"),
3783        );
3784        let drops_oldest =
3785            Rs256PublicKeyRing::new(Arc::clone(&final_signer), vec![Arc::clone(&middle)], 26)
3786                .expect("well-formed but incomplete second successor");
3787        assert!(matches!(
3788            provider.begin_id_token_signing_key_ring_rotation(
3789                drops_oldest,
3790                vec!["https://fastmcp.invalid/oidc/jwks".to_string()],
3791            ),
3792            Err(OidcError::SigningError(_))
3793        ));
3794        assert_eq!(final_calls.load(Ordering::Acquire), 0);
3795        assert_eq!(
3796            AdmittedRsaJwks::from_json(
3797                &provider
3798                    .activated_jwks_document()
3799                    .expect("rejected successor leaves active JWKS unchanged"),
3800            )
3801            .expect("unchanged active JWKS admits")
3802            .len(),
3803            2,
3804        );
3805        assert!(
3806            store
3807                .load(&Cx::for_testing(), "https://fastmcp.invalid/")
3808                .expect("read unchanged durable record")
3809                .expect("active durable record remains present")
3810                .maximum_id_token_expires_at()
3811                >= issued_token.claims.exp
3812        );
3813
3814        let final_successor = Rs256PublicKeyRing::new(
3815            final_signer,
3816            vec![Arc::clone(&middle), Arc::clone(&initial)],
3817            26,
3818        )
3819        .expect("second successor retains every still-live key");
3820        let final_jwks = final_successor
3821            .canonical_public_jwks()
3822            .expect("second successor JWKS");
3823        provider
3824            .begin_id_token_signing_key_ring_rotation(
3825                final_successor,
3826                vec!["https://fastmcp.invalid/oidc/jwks".to_string()],
3827            )
3828            .expect("second successor admits only with the oldest key retained");
3829        verifier.set(final_jwks.as_bytes().to_vec(), final_jwks.generation());
3830        provider
3831            .publish_id_token_signing_key_ring_jwks(final_jwks)
3832            .expect("second successor publishes all live keys");
3833        activate_provider(&provider).expect("second successor activates");
3834
3835        assert_eq!(
3836            AdmittedRsaJwks::from_json(
3837                &provider
3838                    .activated_jwks_document()
3839                    .expect("second successor active JWKS"),
3840            )
3841            .expect("second successor JWKS admits")
3842            .len(),
3843            3,
3844        );
3845        assert!(
3846            store
3847                .load(&Cx::for_testing(), "https://fastmcp.invalid/")
3848                .expect("read durable record")
3849                .expect("durable active record")
3850                .maximum_id_token_expires_at()
3851                >= issued_token.claims.exp
3852        );
3853        assert_eq!(initial_calls.load(Ordering::Acquire), 2);
3854        assert_eq!(middle_calls.load(Ordering::Acquire), 1);
3855        assert_eq!(final_calls.load(Ordering::Acquire), 1);
3856    }
3857
3858    #[test]
3859    fn rh5_restart_rebuilds_durable_live_key_expiry_before_rotation() {
3860        let initial_calls = Arc::new(AtomicUsize::new(0));
3861        let initial_backend = Arc::new(TestOnlyDynamicRs256Backend {
3862            calls: Arc::clone(&initial_calls),
3863            signing_inputs: Mutex::new(Vec::new()),
3864        });
3865        let initial = dynamic_test_signer_with_kid(
3866            Arc::clone(&initial_backend) as Arc<dyn ExternalRs256SignerBackend>,
3867            "restart-live-old",
3868            Rs256SigningBinding::new(41, 42, 43, 44).expect("initial generations"),
3869        );
3870        let initial_jwks = initial
3871            .canonical_public_jwks()
3872            .expect("initial canonical JWKS");
3873        let verifier = Arc::new(MutableReadBackVerifier {
3874            evidence: Mutex::new((
3875                initial_jwks.as_bytes().to_vec(),
3876                initial.binding().ring_generation(),
3877                "https://fastmcp.invalid",
3878            )),
3879        });
3880        let store = Arc::new(TestDurableActivationStore::default());
3881        let (oauth, issued) = issue_access_token(&["openid"]);
3882        let first = OidcProvider::with_defaults(Arc::clone(&oauth)).expect("first provider");
3883        first
3884            .set_id_token_signing_activation_dependencies(Arc::clone(&verifier), Arc::clone(&store))
3885            .expect("first activation dependencies");
3886        first
3887            .begin_id_token_signing_activation(
3888                Arc::clone(&initial),
3889                "https://fastmcp.invalid/oidc/jwks",
3890            )
3891            .expect("begin first activation");
3892        first
3893            .publish_id_token_signing_jwks(initial_jwks)
3894            .expect("publish first JWKS");
3895        activate_provider(&first).expect("activate first signer");
3896        let token = fastmcp_core::block_on(first.issue_id_token(
3897            &Cx::for_testing(),
3898            &issued.access_token,
3899            Some("restart-live-key"),
3900            signing_deadline(),
3901        ))
3902        .0
3903        .expect("old key signs an ID token before restart");
3904        let before_restart = store
3905            .load(&Cx::for_testing(), "https://fastmcp.invalid/")
3906            .expect("load first durable record")
3907            .expect("first durable record");
3908        assert_eq!(
3909            before_restart
3910                .key_id_maximum_id_token_expires_at()
3911                .get("restart-live-old")
3912                .map(OidcSigningKeyExpiry::expires_at),
3913            Some(token.claims.exp),
3914        );
3915
3916        let restarted = OidcProvider::with_defaults(oauth).expect("restarted provider");
3917        restarted
3918            .set_id_token_signing_activation_dependencies(Arc::clone(&verifier), Arc::clone(&store))
3919            .expect("restart activation dependencies");
3920        restarted
3921            .begin_id_token_signing_activation(
3922                Arc::clone(&initial),
3923                "https://fastmcp.invalid/oidc/jwks",
3924            )
3925            .expect("restart begins Pending rather than restoring memory");
3926        restarted
3927            .publish_id_token_signing_jwks(
3928                initial
3929                    .canonical_public_jwks()
3930                    .expect("restart canonical JWKS"),
3931            )
3932            .expect("restart republishes exact JWKS");
3933        activate_provider(&restarted).expect("restart read-back rebuilds durable live keys");
3934        let after_restart = store
3935            .load(&Cx::for_testing(), "https://fastmcp.invalid/")
3936            .expect("load restart durable record")
3937            .expect("restart durable record");
3938        assert_eq!(
3939            after_restart
3940                .key_id_maximum_id_token_expires_at()
3941                .get("restart-live-old")
3942                .map(OidcSigningKeyExpiry::expires_at),
3943            Some(token.claims.exp),
3944        );
3945
3946        let successor_calls = Arc::new(AtomicUsize::new(0));
3947        let successor = test_signer(Arc::new(FixedCanaryBackend {
3948            calls: Arc::clone(&successor_calls),
3949        }));
3950        let drops_live_old = Rs256PublicKeyRing::new(Arc::clone(&successor), Vec::new(), 45)
3951            .expect("well-formed restart successor with the old live key omitted");
3952        assert!(matches!(
3953            restarted.begin_id_token_signing_key_ring_rotation(
3954                drops_live_old,
3955                vec!["https://fastmcp.invalid/oidc/jwks".to_string()],
3956            ),
3957            Err(OidcError::SigningError(_))
3958        ));
3959        assert_eq!(successor_calls.load(Ordering::Acquire), 0);
3960        assert_eq!(
3961            store
3962                .load(&Cx::for_testing(), "https://fastmcp.invalid/")
3963                .expect("load durable state after omitted-key rotation"),
3964            Some(after_restart.clone()),
3965        );
3966        let substituted_live_old = test_signer_with_kid_and_binding(
3967            Arc::new(FixedCanaryBackend {
3968                calls: Arc::clone(&successor_calls),
3969            }),
3970            "restart-live-old",
3971            Rs256SigningBinding::new(51, 52, 53, 54).expect("same-kid substitute generations"),
3972        );
3973        assert_eq!(substituted_live_old.key_id(), initial.key_id());
3974        assert_ne!(
3975            substituted_live_old
3976                .canonical_public_jwks()
3977                .expect("same-kid substitute identity")
3978                .as_bytes(),
3979            initial
3980                .canonical_public_jwks()
3981                .expect("durable old-key identity")
3982                .as_bytes(),
3983        );
3984        let substitutes_live_old =
3985            Rs256PublicKeyRing::new(Arc::clone(&successor), vec![substituted_live_old], 45)
3986                .expect("well-formed successor that changes only old RSA material");
3987        assert!(matches!(
3988            restarted.begin_id_token_signing_key_ring_rotation(
3989                substitutes_live_old,
3990                vec!["https://fastmcp.invalid/oidc/jwks".to_string()],
3991            ),
3992            Err(OidcError::SigningError(_))
3993        ));
3994        assert_eq!(successor_calls.load(Ordering::Acquire), 0);
3995        assert_eq!(
3996            AdmittedRsaJwks::from_json(
3997                &restarted
3998                    .activated_jwks_document()
3999                    .expect("failed restart rotation leaves active JWKS unchanged"),
4000            )
4001            .expect("unchanged restart JWKS admits")
4002            .len(),
4003            1,
4004        );
4005        assert_eq!(
4006            store
4007                .load(&Cx::for_testing(), "https://fastmcp.invalid/")
4008                .expect("load durable state after rejected rotation"),
4009            Some(after_restart.clone()),
4010        );
4011
4012        let retains_live_old = Rs256PublicKeyRing::new(successor, vec![initial], 45)
4013            .expect("successor retains restart-live verification key");
4014        let successor_jwks = retains_live_old
4015            .canonical_public_jwks()
4016            .expect("successor overlap JWKS");
4017        restarted
4018            .begin_id_token_signing_key_ring_rotation(
4019                retains_live_old,
4020                vec!["https://fastmcp.invalid/oidc/jwks".to_string()],
4021            )
4022            .expect("durably retained live key admits successor rotation");
4023        verifier.set(
4024            successor_jwks.as_bytes().to_vec(),
4025            successor_jwks.generation(),
4026        );
4027        restarted
4028            .publish_id_token_signing_key_ring_jwks(successor_jwks)
4029            .expect("publish restart successor overlap");
4030        activate_provider(&restarted).expect("activate restart successor with retained old key");
4031        let after_successor = store
4032            .load(&Cx::for_testing(), "https://fastmcp.invalid/")
4033            .expect("load successor durable record")
4034            .expect("successor durable record");
4035        assert_eq!(
4036            after_successor
4037                .key_id_maximum_id_token_expires_at()
4038                .get("restart-live-old")
4039                .map(OidcSigningKeyExpiry::expires_at),
4040            Some(token.claims.exp),
4041        );
4042        assert_eq!(
4043            AdmittedRsaJwks::from_json(
4044                &restarted
4045                    .activated_jwks_document()
4046                    .expect("restart successor active JWKS"),
4047            )
4048            .expect("restart successor JWKS admits")
4049            .len(),
4050            2,
4051        );
4052    }
4053
4054    #[test]
4055    fn cancellation_after_canary_dispatch_exposes_no_active_signer() {
4056        let calls = Arc::new(AtomicUsize::new(0));
4057        let signer = test_signer(Arc::new(CancellationAfterDispatchBackend {
4058            calls: Arc::clone(&calls),
4059        }));
4060        let provider = provider_with_published_jwks(Arc::new(OAuthServer::with_defaults()), signer);
4061        assert!(
4062            provider
4063                .published_jwks_document("https://fastmcp.invalid/oidc/jwks")
4064                .is_ok()
4065        );
4066        let cx = Cx::for_testing();
4067
4068        let result =
4069            fastmcp_core::block_on(provider.activate_id_token_signing(&cx, signing_deadline())).0;
4070        assert!(matches!(
4071            result,
4072            Err(OidcError::ExternalSigning(
4073                JwsSigningError::CancelledAfterDispatch(_)
4074            ))
4075        ));
4076        assert!(cx.is_cancel_requested());
4077        assert!(provider.activated_jwks_document().is_err());
4078        assert_eq!(calls.load(Ordering::Acquire), 1);
4079    }
4080
4081    #[test]
4082    fn cancellation_after_id_token_signing_exposes_no_token() {
4083        let calls = Arc::new(AtomicUsize::new(0));
4084        let signer = test_signer(Arc::new(CancelIdTokenAfterSigningBackend {
4085            calls: Arc::clone(&calls),
4086        }));
4087        let (oauth, issued) = issue_access_token(&["openid"]);
4088        let provider = provider_with_published_jwks(oauth, signer);
4089        activate_provider(&provider).expect("canary activation remains valid");
4090        let cx = Cx::for_testing();
4091        let result = fastmcp_core::block_on(provider.issue_id_token(
4092            &cx,
4093            &issued.access_token,
4094            None,
4095            signing_deadline(),
4096        ))
4097        .0;
4098        assert!(matches!(
4099            result,
4100            Err(OidcError::ExternalSigning(
4101                JwsSigningError::CancelledAfterDispatch(_)
4102            ))
4103        ));
4104        assert!(cx.is_cancel_requested());
4105        assert_eq!(calls.load(Ordering::Acquire), 2);
4106    }
4107
4108    #[test]
4109    fn retirement_refuses_live_tokens_then_closes_issuance_while_retaining_public_jwks() {
4110        let calls = Arc::new(AtomicUsize::new(0));
4111        let backend = Arc::new(TestOnlyDynamicRs256Backend {
4112            calls: Arc::clone(&calls),
4113            signing_inputs: Mutex::new(Vec::new()),
4114        });
4115        let signer =
4116            dynamic_test_signer(Arc::clone(&backend) as Arc<dyn ExternalRs256SignerBackend>);
4117        let (oauth, issued) = issue_access_token(&["openid"]);
4118        let provider = provider_with_published_jwks(oauth, signer);
4119        activate_provider(&provider).expect("activate external signer generation");
4120        let token = fastmcp_core::block_on(provider.issue_id_token(
4121            &Cx::for_testing(),
4122            &issued.access_token,
4123            None,
4124            signing_deadline(),
4125        ))
4126        .0
4127        .expect("issue ID token to establish durable retirement fence");
4128        assert!(
4129            provider
4130                .retire_id_token_signing_generation(&Cx::for_testing(), token.claims.exp - 1)
4131                .is_err()
4132        );
4133        provider
4134            .retire_id_token_signing_generation(&Cx::for_testing(), token.claims.exp)
4135            .expect("retire only after maximum token expiry");
4136        assert!(provider.activated_jwks_document().is_err());
4137        assert!(
4138            provider
4139                .published_jwks_document("https://fastmcp.invalid/oidc/jwks")
4140                .is_ok()
4141        );
4142        assert_eq!(calls.load(Ordering::Acquire), 2);
4143    }
4144
4145    #[test]
4146    fn durable_store_fences_restart_reactivation_without_restoring_active_memory() {
4147        let calls = Arc::new(AtomicUsize::new(0));
4148        let signer = test_signer(Arc::new(FixedCanaryBackend {
4149            calls: Arc::clone(&calls),
4150        }));
4151        let jwks = signer
4152            .canonical_public_jwks()
4153            .expect("canonical public JWKS")
4154            .as_bytes()
4155            .to_vec();
4156        let store = Arc::new(TestDurableActivationStore::default());
4157        let oauth = Arc::new(OAuthServer::with_defaults());
4158
4159        let first = OidcProvider::with_defaults(Arc::clone(&oauth)).expect("first provider");
4160        first
4161            .set_id_token_signing_activation_dependencies(
4162                Arc::new(FixedReadBackVerifier {
4163                    bytes: jwks.clone(),
4164                    generation: signer.binding().ring_generation(),
4165                    origin: "https://fastmcp.invalid",
4166                }),
4167                Arc::clone(&store),
4168            )
4169            .expect("first dependencies");
4170        first
4171            .begin_id_token_signing_activation(
4172                Arc::clone(&signer),
4173                "https://fastmcp.invalid/oidc/jwks",
4174            )
4175            .expect("first pending");
4176        first
4177            .publish_id_token_signing_jwks(
4178                signer
4179                    .canonical_public_jwks()
4180                    .expect("first canonical JWKS"),
4181            )
4182            .expect("first publication");
4183        fastmcp_core::block_on(
4184            first.activate_id_token_signing(&Cx::for_testing(), signing_deadline()),
4185        )
4186        .0
4187        .expect("first activation");
4188
4189        let restarted = OidcProvider::with_defaults(oauth).expect("fresh-process provider");
4190        assert!(restarted.activated_jwks_document().is_err());
4191        restarted
4192            .set_id_token_signing_activation_dependencies(
4193                Arc::new(FixedReadBackVerifier {
4194                    bytes: jwks,
4195                    generation: signer.binding().ring_generation(),
4196                    origin: "https://fastmcp.invalid",
4197                }),
4198                Arc::clone(&store),
4199            )
4200            .expect("restart dependencies");
4201        restarted
4202            .begin_id_token_signing_activation(
4203                Arc::clone(&signer),
4204                "https://fastmcp.invalid/oidc/jwks",
4205            )
4206            .expect("restart pending");
4207        restarted
4208            .publish_id_token_signing_jwks(
4209                signer
4210                    .canonical_public_jwks()
4211                    .expect("restart canonical JWKS"),
4212            )
4213            .expect("restart publication");
4214        fastmcp_core::block_on(
4215            restarted.activate_id_token_signing(&Cx::for_testing(), signing_deadline()),
4216        )
4217        .0
4218        .expect("fresh read-back and CAS are required after restart");
4219        assert_eq!(
4220            store
4221                .load(&Cx::for_testing(), "https://fastmcp.invalid/")
4222                .expect("durable store read")
4223                .expect("durable activation record")
4224                .activation_generation(),
4225            2,
4226        );
4227        assert_eq!(calls.load(Ordering::Acquire), 2);
4228    }
4229}