1#[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#[cfg(feature = "builtin-auth-server")]
83const OIDC_SIGNING_CANARY_CLAIMS: &str = r#"{"sub":"fixed-vector","aud":"server-policy-later"}"#;
84
85#[derive(Debug, Clone)]
91pub struct OidcProviderConfig {
92 pub issuer: String,
94 pub supported_claims: Vec<String>,
96 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#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
131pub struct UserClaims {
132 pub sub: String,
134
135 #[serde(skip_serializing_if = "Option::is_none")]
138 pub name: Option<String>,
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub given_name: Option<String>,
142 #[serde(skip_serializing_if = "Option::is_none")]
144 pub family_name: Option<String>,
145 #[serde(skip_serializing_if = "Option::is_none")]
147 pub middle_name: Option<String>,
148 #[serde(skip_serializing_if = "Option::is_none")]
150 pub nickname: Option<String>,
151 #[serde(skip_serializing_if = "Option::is_none")]
153 pub preferred_username: Option<String>,
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub profile: Option<String>,
157 #[serde(skip_serializing_if = "Option::is_none")]
159 pub picture: Option<String>,
160 #[serde(skip_serializing_if = "Option::is_none")]
162 pub website: Option<String>,
163 #[serde(skip_serializing_if = "Option::is_none")]
165 pub gender: Option<String>,
166 #[serde(skip_serializing_if = "Option::is_none")]
168 pub birthdate: Option<String>,
169 #[serde(skip_serializing_if = "Option::is_none")]
171 pub zoneinfo: Option<String>,
172 #[serde(skip_serializing_if = "Option::is_none")]
174 pub locale: Option<String>,
175 #[serde(skip_serializing_if = "Option::is_none")]
177 pub updated_at: Option<i64>,
178
179 #[serde(skip_serializing_if = "Option::is_none")]
182 pub email: Option<String>,
183 #[serde(skip_serializing_if = "Option::is_none")]
185 pub email_verified: Option<bool>,
186
187 #[serde(skip_serializing_if = "Option::is_none")]
190 pub phone_number: Option<String>,
191 #[serde(skip_serializing_if = "Option::is_none")]
193 pub phone_number_verified: Option<bool>,
194
195 #[serde(skip_serializing_if = "Option::is_none")]
198 pub address: Option<AddressClaim>,
199
200 #[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 #[must_use]
246 pub fn new(sub: impl Into<String>) -> Self {
247 Self {
248 sub: sub.into(),
249 ..Default::default()
250 }
251 }
252
253 #[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 #[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 #[must_use]
269 pub fn with_email_verified(mut self, verified: bool) -> Self {
270 self.email_verified = Some(verified);
271 self
272 }
273
274 #[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 #[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 #[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 #[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 #[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 #[must_use]
311 pub fn with_updated_at(mut self, timestamp: i64) -> Self {
312 self.updated_at = Some(timestamp);
313 self
314 }
315
316 #[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 #[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 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 if scopes.iter().any(|s| s == "email") {
351 filtered.email = self.email.clone();
352 filtered.email_verified = self.email_verified;
353 }
354
355 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 if scopes.iter().any(|s| s == "address") {
363 filtered.address = self.address.clone();
364 }
365
366 filtered
367 }
368}
369
370#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
372pub struct AddressClaim {
373 #[serde(skip_serializing_if = "Option::is_none")]
375 pub formatted: Option<String>,
376 #[serde(skip_serializing_if = "Option::is_none")]
378 pub street_address: Option<String>,
379 #[serde(skip_serializing_if = "Option::is_none")]
381 pub locality: Option<String>,
382 #[serde(skip_serializing_if = "Option::is_none")]
384 pub region: Option<String>,
385 #[serde(skip_serializing_if = "Option::is_none")]
387 pub postal_code: Option<String>,
388 #[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#[derive(Clone, serde::Serialize, serde::Deserialize)]
419pub struct IdTokenClaims {
420 pub iss: String,
422 pub sub: String,
424 pub aud: String,
426 pub exp: i64,
428 pub iat: i64,
430 #[serde(skip_serializing_if = "Option::is_none")]
432 pub auth_time: Option<i64>,
433 #[serde(skip_serializing_if = "Option::is_none")]
435 pub nonce: Option<String>,
436 #[serde(skip_serializing_if = "Option::is_none")]
438 pub acr: Option<String>,
439 #[serde(skip_serializing_if = "Option::is_none")]
441 pub amr: Option<Vec<String>>,
442 #[serde(skip_serializing_if = "Option::is_none")]
444 pub azp: Option<String>,
445 #[serde(skip_serializing_if = "Option::is_none")]
447 pub at_hash: Option<String>,
448 #[serde(skip_serializing_if = "Option::is_none")]
450 pub c_hash: Option<String>,
451 #[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#[derive(Clone)]
476pub struct IdToken {
477 pub raw: String,
479 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#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
500pub struct DiscoveryDocument {
501 pub issuer: String,
503 pub authorization_endpoint: String,
505 pub token_endpoint: String,
507 #[serde(skip_serializing_if = "Option::is_none")]
509 pub userinfo_endpoint: Option<String>,
510 #[serde(skip_serializing_if = "Option::is_none")]
512 pub jwks_uri: Option<String>,
513 #[serde(skip_serializing_if = "Option::is_none")]
515 pub registration_endpoint: Option<String>,
516 #[serde(skip_serializing_if = "Option::is_none")]
518 pub revocation_endpoint: Option<String>,
519 pub scopes_supported: Vec<String>,
521 pub response_types_supported: Vec<String>,
523 #[serde(skip_serializing_if = "Option::is_none")]
525 pub response_modes_supported: Option<Vec<String>>,
526 pub grant_types_supported: Vec<String>,
528 pub subject_types_supported: Vec<String>,
530 pub id_token_signing_alg_values_supported: Vec<String>,
532 pub token_endpoint_auth_methods_supported: Vec<String>,
534 #[serde(skip_serializing_if = "Option::is_none")]
536 pub claims_supported: Option<Vec<String>>,
537 #[serde(skip_serializing_if = "Option::is_none")]
539 pub code_challenge_methods_supported: Option<Vec<String>>,
540}
541
542impl DiscoveryDocument {
543 #[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
591pub trait ClaimsProvider: Send + Sync {
597 fn get_claims(&self, subject: &str) -> Option<UserClaims>;
601}
602
603#[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 #[must_use]
621 pub fn new() -> Self {
622 Self::default()
623 }
624
625 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 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
649pub 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 #[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
683pub enum OidcError {
689 OAuth(OAuthError),
691 MissingOpenIdScope,
693 ClaimsNotFound(String),
695 ClaimsSubjectMismatch,
697 SigningError(String),
699 #[cfg(feature = "builtin-auth-server")]
701 ExternalSigning(JwsSigningError),
702 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
755pub struct OidcProvider {
763 oauth: Arc<OAuthServer>,
765 config: OidcProviderConfig,
767 claims_provider: RwLock<Option<Arc<dyn ClaimsProvider>>>,
769 #[cfg(feature = "builtin-auth-server")]
773 signing_activation: RwLock<OidcIdTokenSigningState>,
774 #[cfg(feature = "builtin-auth-server")]
777 signing_activation_dependencies: RwLock<Option<OidcSigningActivationDependencies>>,
778}
779
780#[cfg(feature = "builtin-auth-server")]
787pub trait OidcJwksReadBackVerifier: Send + Sync + 'static {
788 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#[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#[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 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 #[must_use]
844 pub const fn expires_at(&self) -> i64 {
845 self.expires_at
846 }
847
848 #[must_use]
850 pub fn canonical_public_key_identity(&self) -> &[u8] {
851 &self.canonical_public_key_identity
852 }
853}
854
855#[cfg(feature = "builtin-auth-server")]
861#[derive(Debug, Clone, Copy, PartialEq, Eq)]
862pub enum OidcSigningActivationStatus {
863 Active,
865 Retiring,
868 Revoked,
870}
871
872#[cfg(feature = "builtin-auth-server")]
873impl OidcSigningActivationStoreRecord {
874 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 #[must_use]
927 pub fn issuer(&self) -> &str {
928 &self.issuer
929 }
930
931 #[must_use]
933 pub const fn key_ring_generation(&self) -> u64 {
934 self.key_ring_generation
935 }
936
937 #[must_use]
939 pub const fn activation_generation(&self) -> u64 {
940 self.activation_generation
941 }
942
943 #[must_use]
945 pub const fn status(&self) -> OidcSigningActivationStatus {
946 self.status
947 }
948
949 #[must_use]
951 pub const fn maximum_id_token_expires_at(&self) -> i64 {
952 self.maximum_id_token_expires_at
953 }
954
955 #[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#[cfg(feature = "builtin-auth-server")]
970pub trait OidcSigningActivationStore: Send + Sync + 'static {
971 fn load(
973 &self,
974 cx: &Cx,
975 issuer: &str,
976 ) -> Result<Option<OidcSigningActivationStoreRecord>, OidcError>;
977
978 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#[cfg(feature = "builtin-auth-server")]
1002enum OidcIdTokenSigningState {
1003 Inactive,
1005 Pending(OidcIdTokenSigningPending),
1007 Published(OidcIdTokenSigningPublished),
1010 Active(OidcIdTokenSigningActivation),
1012 Rotating {
1016 active: OidcIdTokenSigningActivation,
1017 successor: OidcIdTokenSigningSuccessor,
1018 },
1019 Retiring(OidcIdTokenSigningActivation),
1022}
1023
1024#[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#[cfg(feature = "builtin-auth-server")]
1039struct OidcIdTokenSigningPublished {
1040 pending: OidcIdTokenSigningPending,
1041 published_jwks: Vec<u8>,
1042 publication_generation: u64,
1043}
1044
1045#[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 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 validate_oauth_issuer(&config.issuer).map_err(OidcError::from)
1153}
1154
1155impl OidcProvider {
1156 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 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 #[must_use]
1195 pub fn config(&self) -> &OidcProviderConfig {
1196 &self.config
1197 }
1198
1199 #[must_use]
1201 pub fn oauth(&self) -> &Arc<OAuthServer> {
1202 &self.oauth
1203 }
1204
1205 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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 UserClaims::new(subject)
2195 }
2196 };
2197
2198 if claims.sub != subject {
2202 return Err(OidcError::ClaimsSubjectMismatch);
2203 }
2204
2205 Ok(claims.filter_by_scopes(scopes))
2206 }
2207}
2208
2209#[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#[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 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#[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 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 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 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 #[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 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 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 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}