Skip to main content

fastmcp_server/
oidc.rs

1//! OpenID Connect (OIDC) Provider for MCP.
2//!
3//! This module extends the OAuth 2.0/2.1 server with OpenID Connect identity
4//! layer features:
5//!
6//! - **ID Token Issuance**: JWT tokens containing user identity claims
7//! - **UserInfo Endpoint**: Standard endpoint for retrieving user claims
8//! - **Discovery Document**: `.well-known/openid-configuration` metadata
9//! - **Standard Claims**: OpenID Connect standard claim types
10//!
11//! # Architecture
12//!
13//! The OIDC provider builds on top of [`OAuthServer`] by:
14//!
15//! 1. Adding the `openid` scope to enable OIDC flows
16//! 2. Issuing ID tokens alongside access tokens
17//! 3. Providing standard endpoints for identity operations
18//!
19//! # Example
20//!
21//! ```ignore
22//! use fastmcp_rust::oidc::{OidcProvider, OidcProviderConfig, UserClaims};
23//! use fastmcp_rust::oauth::{OAuthServer, OAuthServerConfig};
24//!
25//! // Create OAuth server first
26//! let oauth = Arc::new(OAuthServer::new(OAuthServerConfig::default()));
27//!
28//! // Create OIDC provider on top
29//! let oidc = OidcProvider::new(oauth, OidcProviderConfig::default())
30//!     .expect("oidc provider");
31//!
32//! // Set up user claims provider
33//! oidc.set_claims_provider(|subject| {
34//!     UserClaims::new(subject)
35//!         .with_name("John Doe")
36//!         .with_email("john@example.com")
37//! });
38//! ```
39
40use std::collections::HashMap;
41use std::sync::{Arc, RwLock};
42use std::time::{Duration, SystemTime, UNIX_EPOCH};
43
44use crate::oauth::{OAuthError, OAuthServer, OAuthToken};
45
46// =============================================================================
47// Configuration
48// =============================================================================
49
50/// Configuration for the OIDC provider.
51#[derive(Debug, Clone)]
52pub struct OidcProviderConfig {
53    /// Issuer identifier (URL) - must match OAuth server issuer.
54    pub issuer: String,
55    /// ID token lifetime.
56    pub id_token_lifetime: Duration,
57    /// Signing algorithm for ID tokens.
58    pub signing_algorithm: SigningAlgorithm,
59    /// Key ID for token signing.
60    pub key_id: Option<String>,
61    /// RS256 signing key (PEM-encoded private key).
62    ///
63    /// Required when `signing_algorithm = RS256`.
64    pub rsa_private_key_pem: Option<Vec<u8>>,
65    /// JSON Web Key Set (JWKS) served at `/.well-known/jwks.json`.
66    ///
67    /// Required when `signing_algorithm = RS256`.
68    pub jwks: Option<serde_json::Value>,
69    /// Supported claims.
70    pub supported_claims: Vec<String>,
71    /// Supported scopes beyond `openid`.
72    pub supported_scopes: Vec<String>,
73}
74
75impl Default for OidcProviderConfig {
76    fn default() -> Self {
77        Self {
78            issuer: "fastmcp".to_string(),
79            id_token_lifetime: Duration::from_secs(3600), // 1 hour
80            signing_algorithm: SigningAlgorithm::HS256,
81            key_id: None,
82            rsa_private_key_pem: None,
83            jwks: None,
84            supported_claims: vec![
85                "sub".to_string(),
86                "name".to_string(),
87                "email".to_string(),
88                "email_verified".to_string(),
89                "preferred_username".to_string(),
90                "picture".to_string(),
91                "updated_at".to_string(),
92            ],
93            supported_scopes: vec![
94                "openid".to_string(),
95                "profile".to_string(),
96                "email".to_string(),
97            ],
98        }
99    }
100}
101
102/// Signing algorithm for ID tokens.
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum SigningAlgorithm {
105    /// HMAC-SHA256 (symmetric).
106    HS256,
107    /// RSA-SHA256 (asymmetric) - requires RSA key pair.
108    RS256,
109}
110
111impl SigningAlgorithm {
112    /// Returns the algorithm name as used in JWT headers.
113    #[must_use]
114    pub fn as_str(&self) -> &'static str {
115        match self {
116            Self::HS256 => "HS256",
117            Self::RS256 => "RS256",
118        }
119    }
120}
121
122// =============================================================================
123// User Claims
124// =============================================================================
125
126/// Standard OpenID Connect user claims.
127///
128/// These claims describe the authenticated user and are included in
129/// ID tokens and returned from the userinfo endpoint.
130#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
131pub struct UserClaims {
132    /// Subject identifier (required, unique user ID).
133    pub sub: String,
134
135    // Profile scope claims
136    /// User's full name.
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub name: Option<String>,
139    /// User's given/first name.
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub given_name: Option<String>,
142    /// User's family/last name.
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub family_name: Option<String>,
145    /// User's middle name.
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub middle_name: Option<String>,
148    /// User's nickname/username.
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub nickname: Option<String>,
151    /// User's preferred username.
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub preferred_username: Option<String>,
154    /// URL of user's profile page.
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub profile: Option<String>,
157    /// URL of user's profile picture.
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub picture: Option<String>,
160    /// URL of user's website.
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub website: Option<String>,
163    /// User's gender.
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub gender: Option<String>,
166    /// User's birthday (ISO 8601 date).
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub birthdate: Option<String>,
169    /// User's timezone (IANA timezone string).
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub zoneinfo: Option<String>,
172    /// User's locale (BCP47 language tag).
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub locale: Option<String>,
175    /// Time the user's info was last updated (Unix timestamp).
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub updated_at: Option<i64>,
178
179    // Email scope claims
180    /// User's email address.
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub email: Option<String>,
183    /// Whether the email has been verified.
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub email_verified: Option<bool>,
186
187    // Phone scope claims
188    /// User's phone number.
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub phone_number: Option<String>,
191    /// Whether the phone number has been verified.
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub phone_number_verified: Option<bool>,
194
195    // Address scope claims
196    /// User's address (JSON object).
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub address: Option<AddressClaim>,
199
200    /// Additional custom claims.
201    #[serde(flatten)]
202    pub custom: HashMap<String, serde_json::Value>,
203}
204
205impl UserClaims {
206    /// Creates new user claims with the given subject.
207    #[must_use]
208    pub fn new(sub: impl Into<String>) -> Self {
209        Self {
210            sub: sub.into(),
211            ..Default::default()
212        }
213    }
214
215    /// Sets the user's full name.
216    #[must_use]
217    pub fn with_name(mut self, name: impl Into<String>) -> Self {
218        self.name = Some(name.into());
219        self
220    }
221
222    /// Sets the user's email.
223    #[must_use]
224    pub fn with_email(mut self, email: impl Into<String>) -> Self {
225        self.email = Some(email.into());
226        self
227    }
228
229    /// Sets whether the email is verified.
230    #[must_use]
231    pub fn with_email_verified(mut self, verified: bool) -> Self {
232        self.email_verified = Some(verified);
233        self
234    }
235
236    /// Sets the user's preferred username.
237    #[must_use]
238    pub fn with_preferred_username(mut self, username: impl Into<String>) -> Self {
239        self.preferred_username = Some(username.into());
240        self
241    }
242
243    /// Sets the user's profile picture URL.
244    #[must_use]
245    pub fn with_picture(mut self, url: impl Into<String>) -> Self {
246        self.picture = Some(url.into());
247        self
248    }
249
250    /// Sets the user's given name.
251    #[must_use]
252    pub fn with_given_name(mut self, name: impl Into<String>) -> Self {
253        self.given_name = Some(name.into());
254        self
255    }
256
257    /// Sets the user's family name.
258    #[must_use]
259    pub fn with_family_name(mut self, name: impl Into<String>) -> Self {
260        self.family_name = Some(name.into());
261        self
262    }
263
264    /// Sets the user's phone number.
265    #[must_use]
266    pub fn with_phone_number(mut self, phone: impl Into<String>) -> Self {
267        self.phone_number = Some(phone.into());
268        self
269    }
270
271    /// Sets the updated_at timestamp.
272    #[must_use]
273    pub fn with_updated_at(mut self, timestamp: i64) -> Self {
274        self.updated_at = Some(timestamp);
275        self
276    }
277
278    /// Adds a custom claim.
279    #[must_use]
280    pub fn with_custom(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
281        self.custom.insert(key.into(), value);
282        self
283    }
284
285    /// Filters claims based on requested scopes.
286    ///
287    /// Only returns claims that are allowed by the given scopes.
288    #[must_use]
289    #[allow(clippy::assigning_clones)]
290    pub fn filter_by_scopes(&self, scopes: &[String]) -> UserClaims {
291        let mut filtered = UserClaims::new(&self.sub);
292
293        // Profile scope claims
294        if scopes.iter().any(|s| s == "profile") {
295            filtered.name = self.name.clone();
296            filtered.given_name = self.given_name.clone();
297            filtered.family_name = self.family_name.clone();
298            filtered.middle_name = self.middle_name.clone();
299            filtered.nickname = self.nickname.clone();
300            filtered.preferred_username = self.preferred_username.clone();
301            filtered.profile = self.profile.clone();
302            filtered.picture = self.picture.clone();
303            filtered.website = self.website.clone();
304            filtered.gender = self.gender.clone();
305            filtered.birthdate = self.birthdate.clone();
306            filtered.zoneinfo = self.zoneinfo.clone();
307            filtered.locale = self.locale.clone();
308            filtered.updated_at = self.updated_at;
309        }
310
311        // Email scope claims
312        if scopes.iter().any(|s| s == "email") {
313            filtered.email = self.email.clone();
314            filtered.email_verified = self.email_verified;
315        }
316
317        // Phone scope claims
318        if scopes.iter().any(|s| s == "phone") {
319            filtered.phone_number = self.phone_number.clone();
320            filtered.phone_number_verified = self.phone_number_verified;
321        }
322
323        // Address scope claims
324        if scopes.iter().any(|s| s == "address") {
325            filtered.address = self.address.clone();
326        }
327
328        filtered
329    }
330}
331
332/// Address claim structure per OpenID Connect spec.
333#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
334pub struct AddressClaim {
335    /// Full formatted address.
336    #[serde(skip_serializing_if = "Option::is_none")]
337    pub formatted: Option<String>,
338    /// Street address.
339    #[serde(skip_serializing_if = "Option::is_none")]
340    pub street_address: Option<String>,
341    /// City/locality.
342    #[serde(skip_serializing_if = "Option::is_none")]
343    pub locality: Option<String>,
344    /// State/region.
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub region: Option<String>,
347    /// Postal/zip code.
348    #[serde(skip_serializing_if = "Option::is_none")]
349    pub postal_code: Option<String>,
350    /// Country.
351    #[serde(skip_serializing_if = "Option::is_none")]
352    pub country: Option<String>,
353}
354
355// =============================================================================
356// ID Token
357// =============================================================================
358
359/// ID Token claims (JWT payload).
360#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
361pub struct IdTokenClaims {
362    /// Issuer identifier.
363    pub iss: String,
364    /// Subject identifier.
365    pub sub: String,
366    /// Audience (client ID).
367    pub aud: String,
368    /// Expiration time (Unix timestamp).
369    pub exp: i64,
370    /// Issued at time (Unix timestamp).
371    pub iat: i64,
372    /// Authentication time (Unix timestamp).
373    #[serde(skip_serializing_if = "Option::is_none")]
374    pub auth_time: Option<i64>,
375    /// Nonce from authorization request.
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub nonce: Option<String>,
378    /// Authentication Context Class Reference.
379    #[serde(skip_serializing_if = "Option::is_none")]
380    pub acr: Option<String>,
381    /// Authentication Methods References.
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub amr: Option<Vec<String>>,
384    /// Authorized party (client ID that was issued the token).
385    #[serde(skip_serializing_if = "Option::is_none")]
386    pub azp: Option<String>,
387    /// Access token hash (for hybrid flows).
388    #[serde(skip_serializing_if = "Option::is_none")]
389    pub at_hash: Option<String>,
390    /// Code hash (for hybrid flows).
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub c_hash: Option<String>,
393    /// Additional user claims.
394    #[serde(flatten)]
395    pub user_claims: UserClaims,
396}
397
398/// A signed ID token.
399#[derive(Debug, Clone)]
400pub struct IdToken {
401    /// The raw JWT string.
402    pub raw: String,
403    /// The parsed claims.
404    pub claims: IdTokenClaims,
405}
406
407// =============================================================================
408// Discovery Document
409// =============================================================================
410
411/// OpenID Connect Discovery Document.
412///
413/// This is served at `/.well-known/openid-configuration`.
414#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
415pub struct DiscoveryDocument {
416    /// Issuer identifier URL.
417    pub issuer: String,
418    /// Authorization endpoint URL.
419    pub authorization_endpoint: String,
420    /// Token endpoint URL.
421    pub token_endpoint: String,
422    /// UserInfo endpoint URL.
423    #[serde(skip_serializing_if = "Option::is_none")]
424    pub userinfo_endpoint: Option<String>,
425    /// JWKs URI for public key retrieval.
426    #[serde(skip_serializing_if = "Option::is_none")]
427    pub jwks_uri: Option<String>,
428    /// Registration endpoint URL.
429    #[serde(skip_serializing_if = "Option::is_none")]
430    pub registration_endpoint: Option<String>,
431    /// Revocation endpoint URL.
432    #[serde(skip_serializing_if = "Option::is_none")]
433    pub revocation_endpoint: Option<String>,
434    /// Supported scopes.
435    pub scopes_supported: Vec<String>,
436    /// Supported response types.
437    pub response_types_supported: Vec<String>,
438    /// Supported response modes.
439    #[serde(skip_serializing_if = "Option::is_none")]
440    pub response_modes_supported: Option<Vec<String>>,
441    /// Supported grant types.
442    pub grant_types_supported: Vec<String>,
443    /// Supported subject types.
444    pub subject_types_supported: Vec<String>,
445    /// Supported ID token signing algorithms.
446    pub id_token_signing_alg_values_supported: Vec<String>,
447    /// Supported token endpoint auth methods.
448    pub token_endpoint_auth_methods_supported: Vec<String>,
449    /// Supported claims.
450    #[serde(skip_serializing_if = "Option::is_none")]
451    pub claims_supported: Option<Vec<String>>,
452    /// Supported code challenge methods.
453    #[serde(skip_serializing_if = "Option::is_none")]
454    pub code_challenge_methods_supported: Option<Vec<String>>,
455}
456
457impl DiscoveryDocument {
458    /// Creates a new discovery document with the given issuer and base URL.
459    #[must_use]
460    pub fn new(issuer: impl Into<String>, base_url: impl Into<String>) -> Self {
461        let issuer = issuer.into();
462        let base = base_url.into();
463
464        Self {
465            issuer: issuer.clone(),
466            authorization_endpoint: format!("{}/authorize", base),
467            token_endpoint: format!("{}/token", base),
468            userinfo_endpoint: Some(format!("{}/userinfo", base)),
469            jwks_uri: None,
470            registration_endpoint: None,
471            revocation_endpoint: Some(format!("{}/revoke", base)),
472            scopes_supported: vec![
473                "openid".to_string(),
474                "profile".to_string(),
475                "email".to_string(),
476            ],
477            response_types_supported: vec!["code".to_string()],
478            response_modes_supported: Some(vec!["query".to_string()]),
479            grant_types_supported: vec![
480                "authorization_code".to_string(),
481                "refresh_token".to_string(),
482            ],
483            subject_types_supported: vec!["public".to_string()],
484            id_token_signing_alg_values_supported: vec!["HS256".to_string()],
485            token_endpoint_auth_methods_supported: vec![
486                "client_secret_post".to_string(),
487                "client_secret_basic".to_string(),
488            ],
489            claims_supported: Some(vec![
490                "sub".to_string(),
491                "iss".to_string(),
492                "aud".to_string(),
493                "exp".to_string(),
494                "iat".to_string(),
495                "name".to_string(),
496                "email".to_string(),
497                "email_verified".to_string(),
498                "preferred_username".to_string(),
499                "picture".to_string(),
500            ]),
501            code_challenge_methods_supported: Some(vec!["plain".to_string(), "S256".to_string()]),
502        }
503    }
504}
505
506// =============================================================================
507// Claims Provider
508// =============================================================================
509
510/// Trait for providing user claims.
511pub trait ClaimsProvider: Send + Sync {
512    /// Retrieves claims for a user by subject identifier.
513    ///
514    /// Returns `None` if the user is not found.
515    fn get_claims(&self, subject: &str) -> Option<UserClaims>;
516}
517
518/// Simple in-memory claims provider.
519#[derive(Debug, Default)]
520pub struct InMemoryClaimsProvider {
521    claims: RwLock<HashMap<String, UserClaims>>,
522}
523
524impl InMemoryClaimsProvider {
525    /// Creates a new empty claims provider.
526    #[must_use]
527    pub fn new() -> Self {
528        Self::default()
529    }
530
531    /// Adds or updates claims for a user.
532    pub fn set_claims(&self, claims: UserClaims) {
533        if let Ok(mut guard) = self.claims.write() {
534            guard.insert(claims.sub.clone(), claims);
535        }
536    }
537
538    /// Removes claims for a user.
539    pub fn remove_claims(&self, subject: &str) {
540        if let Ok(mut guard) = self.claims.write() {
541            guard.remove(subject);
542        }
543    }
544}
545
546impl ClaimsProvider for InMemoryClaimsProvider {
547    fn get_claims(&self, subject: &str) -> Option<UserClaims> {
548        self.claims
549            .read()
550            .ok()
551            .and_then(|guard| guard.get(subject).cloned())
552    }
553}
554
555/// Function-based claims provider.
556pub struct FnClaimsProvider<F>
557where
558    F: Fn(&str) -> Option<UserClaims> + Send + Sync,
559{
560    func: F,
561}
562
563impl<F> FnClaimsProvider<F>
564where
565    F: Fn(&str) -> Option<UserClaims> + Send + Sync,
566{
567    /// Creates a new function-based claims provider.
568    #[must_use]
569    pub fn new(func: F) -> Self {
570        Self { func }
571    }
572}
573
574impl<F> ClaimsProvider for FnClaimsProvider<F>
575where
576    F: Fn(&str) -> Option<UserClaims> + Send + Sync,
577{
578    fn get_claims(&self, subject: &str) -> Option<UserClaims> {
579        (self.func)(subject)
580    }
581}
582
583impl ClaimsProvider for Arc<dyn ClaimsProvider> {
584    fn get_claims(&self, subject: &str) -> Option<UserClaims> {
585        (**self).get_claims(subject)
586    }
587}
588
589// =============================================================================
590// OIDC Errors
591// =============================================================================
592
593/// OIDC-specific errors.
594#[derive(Debug, Clone)]
595pub enum OidcError {
596    /// Underlying OAuth error.
597    OAuth(OAuthError),
598    /// Missing openid scope.
599    MissingOpenIdScope,
600    /// User claims not found.
601    ClaimsNotFound(String),
602    /// Token signing failed.
603    SigningError(String),
604    /// Invalid ID token.
605    InvalidIdToken(String),
606}
607
608impl std::fmt::Display for OidcError {
609    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
610        match self {
611            Self::OAuth(e) => write!(f, "OAuth error: {}", e),
612            Self::MissingOpenIdScope => write!(f, "missing 'openid' scope"),
613            Self::ClaimsNotFound(s) => write!(f, "claims not found for subject: {}", s),
614            Self::SigningError(s) => write!(f, "signing error: {}", s),
615            Self::InvalidIdToken(s) => write!(f, "invalid ID token: {}", s),
616        }
617    }
618}
619
620impl std::error::Error for OidcError {}
621
622impl From<OAuthError> for OidcError {
623    fn from(err: OAuthError) -> Self {
624        Self::OAuth(err)
625    }
626}
627
628// =============================================================================
629// OIDC Provider
630// =============================================================================
631
632/// OpenID Connect Provider.
633///
634/// This extends the OAuth server with OIDC identity features.
635pub struct OidcProvider {
636    /// Underlying OAuth server.
637    oauth: Arc<OAuthServer>,
638    /// OIDC configuration.
639    config: OidcProviderConfig,
640    /// Signing key (HMAC secret).
641    signing_key: RwLock<SigningKey>,
642    /// Claims provider.
643    claims_provider: RwLock<Option<Arc<dyn ClaimsProvider>>>,
644    /// Cached ID tokens by access token.
645    id_tokens: RwLock<HashMap<String, IdToken>>,
646}
647
648/// Signing key for ID tokens.
649#[derive(Clone, Default)]
650enum SigningKey {
651    /// HMAC-SHA256 secret.
652    Hmac(Vec<u8>),
653    /// No key configured (will generate on first use).
654    #[default]
655    None,
656}
657
658fn validate_oidc_config(config: &OidcProviderConfig) -> Result<(), OidcError> {
659    match config.signing_algorithm {
660        SigningAlgorithm::HS256 => Ok(()),
661        SigningAlgorithm::RS256 => {
662            #[cfg(feature = "jwt")]
663            {
664                let kid = config.key_id.as_deref().ok_or_else(|| {
665                    OidcError::SigningError("RS256 requires `key_id` to be set".to_string())
666                })?;
667
668                let pem = config.rsa_private_key_pem.as_ref().ok_or_else(|| {
669                    OidcError::SigningError("RS256 requires `rsa_private_key_pem`".to_string())
670                })?;
671                // Fail fast: reject invalid PEM so we never advertise RS256 with a broken signer.
672                jsonwebtoken::EncodingKey::from_rsa_pem(pem).map_err(|e| {
673                    OidcError::SigningError(format!("invalid RSA private key PEM: {e}"))
674                })?;
675
676                let jwks = config.jwks.as_ref().ok_or_else(|| {
677                    OidcError::SigningError("RS256 requires `jwks` (JWKS JSON)".to_string())
678                })?;
679
680                // Validate JWKS shape and that it includes a plausible RSA key with the configured kid.
681                let keys = jwks.get("keys").and_then(|v| v.as_array()).ok_or_else(|| {
682                    OidcError::SigningError(
683                        "JWKS must be an object with a `keys` array".to_string(),
684                    )
685                })?;
686
687                let mut found = false;
688                for key in keys {
689                    let Some(obj) = key.as_object() else { continue };
690                    let key_kid = obj.get("kid").and_then(|v| v.as_str());
691                    if key_kid != Some(kid) {
692                        continue;
693                    }
694                    let kty = obj.get("kty").and_then(|v| v.as_str());
695                    if kty != Some("RSA") {
696                        continue;
697                    }
698                    // Require the minimal RSA public key components so the JWKS is usable.
699                    if obj.get("n").and_then(|v| v.as_str()).is_none()
700                        || obj.get("e").and_then(|v| v.as_str()).is_none()
701                    {
702                        return Err(OidcError::SigningError(format!(
703                            "JWKS key kid={kid} is missing RSA components `n`/`e`"
704                        )));
705                    }
706                    found = true;
707                    break;
708                }
709
710                if !found {
711                    return Err(OidcError::SigningError(format!(
712                        "JWKS does not contain an RSA key with kid={kid}"
713                    )));
714                }
715
716                Ok(())
717            }
718            #[cfg(not(feature = "jwt"))]
719            {
720                Err(OidcError::SigningError(
721                    "RS256 requires the `fastmcp-server/jwt` feature".to_string(),
722                ))
723            }
724        }
725    }
726}
727
728impl OidcProvider {
729    /// Creates a new OIDC provider with the given OAuth server.
730    pub fn new(oauth: Arc<OAuthServer>, config: OidcProviderConfig) -> Result<Self, OidcError> {
731        validate_oidc_config(&config)?;
732        Ok(Self {
733            oauth,
734            config,
735            signing_key: RwLock::new(SigningKey::None),
736            claims_provider: RwLock::new(None),
737            id_tokens: RwLock::new(HashMap::new()),
738        })
739    }
740
741    /// Creates a new OIDC provider with default configuration.
742    pub fn with_defaults(oauth: Arc<OAuthServer>) -> Result<Self, OidcError> {
743        Self::new(oauth, OidcProviderConfig::default())
744    }
745
746    /// Returns the OIDC configuration.
747    #[must_use]
748    pub fn config(&self) -> &OidcProviderConfig {
749        &self.config
750    }
751
752    /// Returns a reference to the underlying OAuth server.
753    #[must_use]
754    pub fn oauth(&self) -> &Arc<OAuthServer> {
755        &self.oauth
756    }
757
758    /// Sets the HMAC signing key.
759    pub fn set_hmac_key(&self, key: impl AsRef<[u8]>) {
760        if let Ok(mut guard) = self.signing_key.write() {
761            *guard = SigningKey::Hmac(key.as_ref().to_vec());
762        }
763    }
764
765    /// Sets the claims provider.
766    pub fn set_claims_provider<P: ClaimsProvider + 'static>(&self, provider: P) {
767        if let Ok(mut guard) = self.claims_provider.write() {
768            *guard = Some(Arc::new(provider));
769        }
770    }
771
772    /// Sets a function-based claims provider.
773    pub fn set_claims_fn<F>(&self, func: F)
774    where
775        F: Fn(&str) -> Option<UserClaims> + Send + Sync + 'static,
776    {
777        self.set_claims_provider(FnClaimsProvider::new(func));
778    }
779
780    /// Generates the discovery document.
781    #[must_use]
782    pub fn discovery_document(&self, base_url: impl Into<String>) -> DiscoveryDocument {
783        let base_url = base_url.into();
784        let mut doc = DiscoveryDocument::new(&self.config.issuer, base_url.clone());
785        doc.scopes_supported = self.config.supported_scopes.clone();
786        doc.claims_supported = Some(self.config.supported_claims.clone());
787        doc.id_token_signing_alg_values_supported =
788            vec![self.config.signing_algorithm.as_str().to_string()];
789        doc.jwks_uri = match self.config.signing_algorithm {
790            SigningAlgorithm::HS256 => None,
791            SigningAlgorithm::RS256 => Some(format!("{}/.well-known/jwks.json", base_url)),
792        };
793        doc
794    }
795
796    /// Returns the configured JSON Web Key Set (JWKS), if any.
797    ///
798    /// For HS256 this is typically `None`. For RS256 it is required and should be served at
799    /// `/.well-known/jwks.json` alongside the discovery document.
800    #[must_use]
801    pub fn jwks(&self) -> Option<serde_json::Value> {
802        self.config.jwks.clone()
803    }
804
805    // -------------------------------------------------------------------------
806    // ID Token Issuance
807    // -------------------------------------------------------------------------
808
809    /// Issues an ID token for the given access token.
810    ///
811    /// This should be called after a successful token exchange when the
812    /// `openid` scope was requested.
813    pub fn issue_id_token(
814        &self,
815        access_token: &OAuthToken,
816        nonce: Option<&str>,
817    ) -> Result<IdToken, OidcError> {
818        // Verify openid scope
819        if !access_token.scopes.iter().any(|s| s == "openid") {
820            return Err(OidcError::MissingOpenIdScope);
821        }
822
823        let subject = access_token
824            .subject
825            .as_ref()
826            .ok_or_else(|| OidcError::ClaimsNotFound("no subject in access token".to_string()))?;
827
828        // Get user claims
829        let user_claims = self.get_user_claims(subject, &access_token.scopes)?;
830
831        // Build ID token claims
832        let now = SystemTime::now()
833            .duration_since(UNIX_EPOCH)
834            .unwrap_or_default()
835            .as_secs() as i64;
836
837        let claims = IdTokenClaims {
838            iss: self.config.issuer.clone(),
839            sub: subject.clone(),
840            aud: access_token.client_id.clone(),
841            exp: now + self.config.id_token_lifetime.as_secs() as i64,
842            iat: now,
843            auth_time: Some(now),
844            nonce: nonce.map(String::from),
845            acr: None,
846            amr: None,
847            azp: Some(access_token.client_id.clone()),
848            at_hash: Some(self.compute_at_hash(&access_token.token)),
849            c_hash: None,
850            user_claims,
851        };
852
853        // Sign the token
854        let raw = self.sign_id_token(&claims)?;
855
856        let issued = IdToken { raw, claims };
857
858        // Cache the ID token
859        if let Ok(mut guard) = self.id_tokens.write() {
860            guard.insert(access_token.token.clone(), issued.clone());
861        }
862
863        Ok(issued)
864    }
865
866    /// Gets the ID token associated with an access token.
867    #[must_use]
868    pub fn get_id_token(&self, access_token: &str) -> Option<IdToken> {
869        self.id_tokens
870            .read()
871            .ok()
872            .and_then(|guard| guard.get(access_token).cloned())
873    }
874
875    // -------------------------------------------------------------------------
876    // UserInfo Endpoint
877    // -------------------------------------------------------------------------
878
879    /// Handles a userinfo request.
880    ///
881    /// Returns the user's claims filtered by the access token's scopes.
882    pub fn userinfo(&self, access_token: &str) -> Result<UserClaims, OidcError> {
883        // Validate access token
884        let validated = self
885            .oauth
886            .validate_access_token(access_token)
887            .ok_or_else(|| {
888                OidcError::OAuth(OAuthError::InvalidGrant(
889                    "invalid or expired access token".to_string(),
890                ))
891            })?;
892
893        // Verify openid scope
894        if !validated.scopes.iter().any(|s| s == "openid") {
895            return Err(OidcError::MissingOpenIdScope);
896        }
897
898        let subject = validated
899            .subject
900            .as_ref()
901            .ok_or_else(|| OidcError::ClaimsNotFound("no subject in access token".to_string()))?;
902
903        self.get_user_claims(subject, &validated.scopes)
904    }
905
906    // -------------------------------------------------------------------------
907    // Helper Methods
908    // -------------------------------------------------------------------------
909
910    fn get_user_claims(&self, subject: &str, scopes: &[String]) -> Result<UserClaims, OidcError> {
911        let provider = self
912            .claims_provider
913            .read()
914            .ok()
915            .and_then(|guard| guard.clone());
916
917        let claims = match provider {
918            Some(p) => p
919                .get_claims(subject)
920                .ok_or_else(|| OidcError::ClaimsNotFound(subject.to_string()))?,
921            None => {
922                // Default: just return subject
923                UserClaims::new(subject)
924            }
925        };
926
927        Ok(claims.filter_by_scopes(scopes))
928    }
929
930    fn sign_id_token(&self, claims: &IdTokenClaims) -> Result<String, OidcError> {
931        match self.config.signing_algorithm {
932            SigningAlgorithm::HS256 => {
933                let key = self.get_or_generate_signing_key()?;
934
935                // Build JWT (manual, minimal dependencies)
936                let header = serde_json::json!({
937                    "alg": "HS256",
938                    "typ": "JWT",
939                    "kid": self.config.key_id.as_deref().unwrap_or("default"),
940                });
941
942                let header_b64 = base64url_encode(&serde_json::to_vec(&header).map_err(|e| {
943                    OidcError::SigningError(format!("failed to serialize header: {e}"))
944                })?);
945
946                let claims_b64 = base64url_encode(&serde_json::to_vec(claims).map_err(|e| {
947                    OidcError::SigningError(format!("failed to serialize claims: {e}"))
948                })?);
949
950                let signing_input = format!("{header_b64}.{claims_b64}");
951
952                let signature = match &key {
953                    SigningKey::Hmac(secret) => hmac_sha256(&signing_input, secret)?,
954                    SigningKey::None => {
955                        return Err(OidcError::SigningError(
956                            "no signing key configured".to_string(),
957                        ));
958                    }
959                };
960
961                let signature_b64 = base64url_encode(&signature);
962                Ok(format!("{signing_input}.{signature_b64}"))
963            }
964            SigningAlgorithm::RS256 => {
965                #[cfg(feature = "jwt")]
966                {
967                    use jsonwebtoken::{Algorithm, EncodingKey, Header, encode};
968
969                    let pem = self.config.rsa_private_key_pem.as_ref().ok_or_else(|| {
970                        OidcError::SigningError("RS256 requires `rsa_private_key_pem`".to_string())
971                    })?;
972
973                    let kid = self.config.key_id.as_deref().ok_or_else(|| {
974                        OidcError::SigningError("RS256 requires `key_id` to be set".to_string())
975                    })?;
976
977                    let mut header = Header::new(Algorithm::RS256);
978                    header.typ = Some("JWT".to_string());
979                    header.kid = Some(kid.to_string());
980
981                    let key = EncodingKey::from_rsa_pem(pem).map_err(|e| {
982                        OidcError::SigningError(format!("failed to parse RSA private key PEM: {e}"))
983                    })?;
984
985                    encode(&header, claims, &key)
986                        .map_err(|e| OidcError::SigningError(format!("RS256 signing failed: {e}")))
987                }
988                #[cfg(not(feature = "jwt"))]
989                {
990                    Err(OidcError::SigningError(
991                        "RS256 requires the `fastmcp-server/jwt` feature".to_string(),
992                    ))
993                }
994            }
995        }
996    }
997
998    fn get_or_generate_signing_key(&self) -> Result<SigningKey, OidcError> {
999        let guard = self
1000            .signing_key
1001            .read()
1002            .map_err(|_| OidcError::SigningError("failed to acquire read lock".to_string()))?;
1003
1004        match &*guard {
1005            SigningKey::None => {
1006                // Generate a random key
1007                drop(guard);
1008                let mut write_guard = self.signing_key.write().map_err(|_| {
1009                    OidcError::SigningError("failed to acquire write lock".to_string())
1010                })?;
1011
1012                // Double-check after acquiring write lock
1013                if matches!(&*write_guard, SigningKey::None) {
1014                    let key = generate_random_bytes(32)?;
1015                    *write_guard = SigningKey::Hmac(key.clone());
1016                    Ok(SigningKey::Hmac(key))
1017                } else {
1018                    Ok(write_guard.clone())
1019                }
1020            }
1021            key => Ok(key.clone()),
1022        }
1023    }
1024
1025    fn compute_at_hash(&self, access_token: &str) -> String {
1026        // at_hash is left half of hash of access token
1027        let hash = simple_sha256(access_token.as_bytes());
1028        base64url_encode(&hash[..16])
1029    }
1030
1031    /// Removes expired ID tokens from cache.
1032    pub fn cleanup_expired(&self) {
1033        let now = SystemTime::now()
1034            .duration_since(UNIX_EPOCH)
1035            .unwrap_or_default()
1036            .as_secs() as i64;
1037
1038        if let Ok(mut guard) = self.id_tokens.write() {
1039            guard.retain(|_, token| token.claims.exp > now);
1040        }
1041    }
1042}
1043
1044// =============================================================================
1045// Helper Functions
1046// =============================================================================
1047
1048/// Base64url encodes bytes (no padding).
1049fn base64url_encode(data: &[u8]) -> String {
1050    use base64::Engine;
1051    use base64::engine::general_purpose::URL_SAFE_NO_PAD;
1052    URL_SAFE_NO_PAD.encode(data)
1053}
1054
1055fn simple_sha256(data: &[u8]) -> [u8; 32] {
1056    use sha2::Digest;
1057    let digest = sha2::Sha256::digest(data);
1058    let mut out = [0u8; 32];
1059    out.copy_from_slice(&digest);
1060    out
1061}
1062
1063fn hmac_sha256(message: &str, key: &[u8]) -> Result<[u8; 32], OidcError> {
1064    use hmac::Mac;
1065    type HmacSha256 = hmac::Hmac<sha2::Sha256>;
1066
1067    let mut mac = HmacSha256::new_from_slice(key)
1068        .map_err(|e| OidcError::SigningError(format!("invalid HMAC key: {e}")))?;
1069    mac.update(message.as_bytes());
1070
1071    let bytes = mac.finalize().into_bytes();
1072    let mut out = [0u8; 32];
1073    out.copy_from_slice(&bytes);
1074    Ok(out)
1075}
1076
1077/// Generates random bytes.
1078fn generate_random_bytes(len: usize) -> Result<Vec<u8>, OidcError> {
1079    let mut buf = vec![0u8; len];
1080    getrandom::fill(&mut buf)
1081        .map_err(|e| OidcError::SigningError(format!("secure random generation failed: {e}")))?;
1082    Ok(buf)
1083}
1084
1085// =============================================================================
1086// Tests
1087// =============================================================================
1088
1089#[cfg(test)]
1090mod tests {
1091    use super::*;
1092    use crate::oauth::{
1093        AuthorizationRequest, CodeChallengeMethod, OAuthClient, OAuthServerConfig, TokenRequest,
1094    };
1095
1096    const TEST_CLIENT_ID: &str = "test-client";
1097    const TEST_REDIRECT_URI: &str = "http://localhost:3000/callback";
1098    const TEST_CODE_VERIFIER: &str = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
1099
1100    fn create_test_provider() -> OidcProvider {
1101        let oauth = Arc::new(OAuthServer::new(OAuthServerConfig::default()));
1102        OidcProvider::with_defaults(oauth).expect("create provider")
1103    }
1104
1105    fn issue_token_via_auth_code(oauth: &OAuthServer, scopes: &[&str], subject: &str) -> String {
1106        let mut client_builder =
1107            OAuthClient::builder(TEST_CLIENT_ID).redirect_uri(TEST_REDIRECT_URI);
1108        for scope in scopes {
1109            client_builder = client_builder.scope(*scope);
1110        }
1111        let client = client_builder.build().expect("build client");
1112        oauth.register_client(client).expect("register client");
1113
1114        let auth_request = AuthorizationRequest {
1115            response_type: "code".to_string(),
1116            client_id: TEST_CLIENT_ID.to_string(),
1117            redirect_uri: TEST_REDIRECT_URI.to_string(),
1118            scopes: scopes.iter().map(|scope| (*scope).to_string()).collect(),
1119            state: Some("state-123".to_string()),
1120            code_challenge: TEST_CODE_VERIFIER.to_string(),
1121            code_challenge_method: CodeChallengeMethod::Plain,
1122        };
1123
1124        let (code, _redirect) = oauth
1125            .authorize(&auth_request, Some(subject.to_string()))
1126            .expect("authorize");
1127        oauth
1128            .token(&TokenRequest {
1129                grant_type: "authorization_code".to_string(),
1130                code: Some(code),
1131                redirect_uri: Some(TEST_REDIRECT_URI.to_string()),
1132                client_id: TEST_CLIENT_ID.to_string(),
1133                client_secret: None,
1134                code_verifier: Some(TEST_CODE_VERIFIER.to_string()),
1135                refresh_token: None,
1136                scopes: None,
1137            })
1138            .expect("exchange token")
1139            .access_token
1140    }
1141
1142    #[test]
1143    fn test_user_claims_builder() {
1144        let claims = UserClaims::new("user123")
1145            .with_name("John Doe")
1146            .with_email("john@example.com")
1147            .with_email_verified(true)
1148            .with_preferred_username("johnd");
1149
1150        assert_eq!(claims.sub, "user123");
1151        assert_eq!(claims.name, Some("John Doe".to_string()));
1152        assert_eq!(claims.email, Some("john@example.com".to_string()));
1153        assert_eq!(claims.email_verified, Some(true));
1154        assert_eq!(claims.preferred_username, Some("johnd".to_string()));
1155    }
1156
1157    #[test]
1158    fn test_claims_filter_by_scopes() {
1159        let claims = UserClaims::new("user123")
1160            .with_name("John Doe")
1161            .with_email("john@example.com")
1162            .with_phone_number("+1234567890");
1163
1164        // Only openid scope - just sub
1165        let filtered = claims.filter_by_scopes(&["openid".to_string()]);
1166        assert_eq!(filtered.sub, "user123");
1167        assert!(filtered.name.is_none());
1168        assert!(filtered.email.is_none());
1169
1170        // Profile scope
1171        let filtered = claims.filter_by_scopes(&["openid".to_string(), "profile".to_string()]);
1172        assert_eq!(filtered.name, Some("John Doe".to_string()));
1173        assert!(filtered.email.is_none());
1174
1175        // Email scope
1176        let filtered = claims.filter_by_scopes(&["openid".to_string(), "email".to_string()]);
1177        assert!(filtered.name.is_none());
1178        assert_eq!(filtered.email, Some("john@example.com".to_string()));
1179
1180        // All scopes
1181        let filtered = claims.filter_by_scopes(&[
1182            "openid".to_string(),
1183            "profile".to_string(),
1184            "email".to_string(),
1185            "phone".to_string(),
1186        ]);
1187        assert_eq!(filtered.name, Some("John Doe".to_string()));
1188        assert_eq!(filtered.email, Some("john@example.com".to_string()));
1189        assert_eq!(filtered.phone_number, Some("+1234567890".to_string()));
1190    }
1191
1192    #[test]
1193    fn test_discovery_document() {
1194        let provider = create_test_provider();
1195        let doc = provider.discovery_document("https://example.com");
1196
1197        assert_eq!(doc.issuer, "fastmcp");
1198        assert_eq!(doc.authorization_endpoint, "https://example.com/authorize");
1199        assert_eq!(doc.token_endpoint, "https://example.com/token");
1200        assert!(doc.jwks_uri.is_none(), "HS256 must not publish jwks_uri");
1201        assert!(doc.scopes_supported.contains(&"openid".to_string()));
1202        assert!(doc.response_types_supported.contains(&"code".to_string()));
1203    }
1204
1205    #[test]
1206    #[cfg(not(feature = "jwt"))]
1207    fn test_rs256_requires_jwt_feature() {
1208        let oauth = Arc::new(OAuthServer::new(OAuthServerConfig::default()));
1209        let mut config = OidcProviderConfig::default();
1210        config.signing_algorithm = SigningAlgorithm::RS256;
1211        config.key_id = Some("test-kid".to_string());
1212        config.rsa_private_key_pem = Some(b"dummy".to_vec());
1213        config.jwks = Some(serde_json::json!({
1214            "keys": [{
1215                "kty": "RSA",
1216                "kid": "test-kid",
1217                "n": "x",
1218                "e": "AQAB"
1219            }]
1220        }));
1221
1222        let res = OidcProvider::new(oauth, config);
1223        assert!(
1224            res.is_err(),
1225            "expected RS256 to be rejected without jwt feature"
1226        );
1227    }
1228
1229    #[test]
1230    #[cfg(feature = "jwt")]
1231    fn test_rs256_rejects_invalid_pem() {
1232        let oauth = Arc::new(OAuthServer::new(OAuthServerConfig::default()));
1233        let mut config = OidcProviderConfig::default();
1234        config.signing_algorithm = SigningAlgorithm::RS256;
1235        config.key_id = Some("test-kid".to_string());
1236        config.rsa_private_key_pem = Some(b"not a pem".to_vec());
1237        config.jwks = Some(serde_json::json!({
1238            "keys": [{
1239                "kty": "RSA",
1240                "kid": "test-kid",
1241                "n": "x",
1242                "e": "AQAB"
1243            }]
1244        }));
1245
1246        let res = OidcProvider::new(oauth, config);
1247        assert!(res.is_err(), "expected invalid PEM to be rejected");
1248    }
1249
1250    #[test]
1251    fn test_in_memory_claims_provider() {
1252        let provider = InMemoryClaimsProvider::new();
1253
1254        let claims = UserClaims::new("user123")
1255            .with_name("John Doe")
1256            .with_email("john@example.com");
1257
1258        provider.set_claims(claims);
1259
1260        let retrieved = provider.get_claims("user123");
1261        assert!(retrieved.is_some());
1262        assert_eq!(retrieved.unwrap().name, Some("John Doe".to_string()));
1263
1264        assert!(provider.get_claims("nonexistent").is_none());
1265
1266        provider.remove_claims("user123");
1267        assert!(provider.get_claims("user123").is_none());
1268    }
1269
1270    #[test]
1271    fn test_fn_claims_provider() {
1272        let provider = FnClaimsProvider::new(|subject| {
1273            if subject == "user123" {
1274                Some(UserClaims::new(subject).with_name("John Doe"))
1275            } else {
1276                None
1277            }
1278        });
1279
1280        let claims = provider.get_claims("user123");
1281        assert!(claims.is_some());
1282        assert_eq!(claims.unwrap().name, Some("John Doe".to_string()));
1283
1284        assert!(provider.get_claims("other").is_none());
1285    }
1286
1287    #[test]
1288    fn test_signing_algorithm() {
1289        assert_eq!(SigningAlgorithm::HS256.as_str(), "HS256");
1290        assert_eq!(SigningAlgorithm::RS256.as_str(), "RS256");
1291    }
1292
1293    #[test]
1294    fn test_oidc_error_display() {
1295        let err = OidcError::MissingOpenIdScope;
1296        assert_eq!(err.to_string(), "missing 'openid' scope");
1297
1298        let err = OidcError::ClaimsNotFound("user123".to_string());
1299        assert!(err.to_string().contains("user123"));
1300    }
1301
1302    #[test]
1303    fn test_base64url_encode() {
1304        assert_eq!(base64url_encode(b""), "");
1305        assert_eq!(base64url_encode(b"f"), "Zg");
1306        assert_eq!(base64url_encode(b"fo"), "Zm8");
1307        assert_eq!(base64url_encode(b"foo"), "Zm9v");
1308    }
1309
1310    #[test]
1311    fn test_id_token_issuance() {
1312        let provider = create_test_provider();
1313
1314        // Set up claims provider
1315        let claims_provider = InMemoryClaimsProvider::new();
1316        claims_provider.set_claims(
1317            UserClaims::new("user123")
1318                .with_name("John Doe")
1319                .with_email("john@example.com"),
1320        );
1321        provider.set_claims_provider(claims_provider);
1322
1323        // Set signing key
1324        provider.set_hmac_key(b"test-secret-key");
1325
1326        let oauth_at = provider
1327            .oauth()
1328            .validate_access_token(&issue_token_via_auth_code(
1329                provider.oauth().as_ref(),
1330                &["openid", "profile", "email"],
1331                "user123",
1332            ))
1333            .expect("valid access token");
1334
1335        let result = provider.issue_id_token(&oauth_at, Some("nonce123"));
1336        let issued = result.expect("issue id token");
1337        assert!(!issued.raw.is_empty());
1338        assert!(issued.raw.contains('.'));
1339        assert_eq!(issued.claims.sub, "user123");
1340        assert_eq!(issued.claims.aud, TEST_CLIENT_ID);
1341        assert_eq!(issued.claims.nonce, Some("nonce123".to_string()));
1342        assert_eq!(issued.claims.user_claims.name, Some("John Doe".to_string()));
1343    }
1344
1345    #[test]
1346    fn test_id_token_requires_openid_scope() {
1347        let provider = create_test_provider();
1348        let oauth_at = provider
1349            .oauth()
1350            .validate_access_token(&issue_token_via_auth_code(
1351                provider.oauth().as_ref(),
1352                &["profile"],
1353                "user123",
1354            ))
1355            .expect("valid access token");
1356
1357        let result = provider.issue_id_token(&oauth_at, None);
1358        assert!(matches!(result, Err(OidcError::MissingOpenIdScope)));
1359    }
1360
1361    #[test]
1362    fn test_userinfo() {
1363        let oauth = Arc::new(OAuthServer::new(OAuthServerConfig::default()));
1364
1365        let provider = OidcProvider::with_defaults(oauth).expect("create provider");
1366
1367        // Set up claims
1368        let claims_store = InMemoryClaimsProvider::new();
1369        claims_store.set_claims(UserClaims::new("user123").with_name("John Doe"));
1370        provider.set_claims_provider(claims_store);
1371
1372        let result = provider.userinfo(&issue_token_via_auth_code(
1373            provider.oauth().as_ref(),
1374            &["openid", "profile"],
1375            "user123",
1376        ));
1377        assert!(result.is_ok());
1378
1379        let claims = result.unwrap();
1380        assert_eq!(claims.sub, "user123");
1381        assert_eq!(claims.name, Some("John Doe".to_string()));
1382    }
1383
1384    #[test]
1385    fn test_address_claim() {
1386        let address = AddressClaim {
1387            formatted: Some("123 Main St, City, ST 12345".to_string()),
1388            street_address: Some("123 Main St".to_string()),
1389            locality: Some("City".to_string()),
1390            region: Some("ST".to_string()),
1391            postal_code: Some("12345".to_string()),
1392            country: Some("US".to_string()),
1393        };
1394
1395        let json = serde_json::to_string(&address).unwrap();
1396        assert!(json.contains("formatted"));
1397        assert!(json.contains("street_address"));
1398    }
1399
1400    #[test]
1401    fn test_custom_claims() {
1402        let claims = UserClaims::new("user123")
1403            .with_custom("custom_field", serde_json::json!("custom_value"))
1404            .with_custom("roles", serde_json::json!(["admin", "user"]));
1405
1406        assert_eq!(
1407            claims.custom.get("custom_field"),
1408            Some(&serde_json::json!("custom_value"))
1409        );
1410        assert_eq!(
1411            claims.custom.get("roles"),
1412            Some(&serde_json::json!(["admin", "user"]))
1413        );
1414    }
1415
1416    // ── OidcProviderConfig ─────────────────────────────────────────
1417
1418    #[test]
1419    fn config_default_values() {
1420        let cfg = OidcProviderConfig::default();
1421        assert_eq!(cfg.issuer, "fastmcp");
1422        assert_eq!(cfg.id_token_lifetime, Duration::from_secs(3600));
1423        assert_eq!(cfg.signing_algorithm, SigningAlgorithm::HS256);
1424        assert!(cfg.key_id.is_none());
1425        assert!(cfg.rsa_private_key_pem.is_none());
1426        assert!(cfg.jwks.is_none());
1427        assert!(cfg.supported_claims.contains(&"sub".to_string()));
1428        assert!(cfg.supported_claims.contains(&"email".to_string()));
1429        assert!(cfg.supported_scopes.contains(&"openid".to_string()));
1430        assert!(cfg.supported_scopes.contains(&"profile".to_string()));
1431    }
1432
1433    #[test]
1434    fn config_debug() {
1435        let cfg = OidcProviderConfig::default();
1436        let debug = format!("{:?}", cfg);
1437        assert!(debug.contains("fastmcp"));
1438        assert!(debug.contains("HS256"));
1439    }
1440
1441    #[test]
1442    fn config_clone() {
1443        let cfg = OidcProviderConfig::default();
1444        let cloned = cfg.clone();
1445        assert_eq!(cloned.issuer, cfg.issuer);
1446        assert_eq!(cloned.signing_algorithm, cfg.signing_algorithm);
1447    }
1448
1449    // ── SigningAlgorithm ───────────────────────────────────────────
1450
1451    #[test]
1452    fn signing_algorithm_copy() {
1453        let alg = SigningAlgorithm::HS256;
1454        let copied = alg;
1455        assert_eq!(alg, copied);
1456    }
1457
1458    #[test]
1459    fn signing_algorithm_eq() {
1460        assert_eq!(SigningAlgorithm::HS256, SigningAlgorithm::HS256);
1461        assert_eq!(SigningAlgorithm::RS256, SigningAlgorithm::RS256);
1462        assert_ne!(SigningAlgorithm::HS256, SigningAlgorithm::RS256);
1463    }
1464
1465    #[test]
1466    fn signing_algorithm_debug() {
1467        let debug = format!("{:?}", SigningAlgorithm::RS256);
1468        assert!(debug.contains("RS256"));
1469    }
1470
1471    #[test]
1472    fn signing_algorithm_clone() {
1473        let alg = SigningAlgorithm::RS256;
1474        let cloned = alg.clone();
1475        assert_eq!(alg, cloned);
1476    }
1477
1478    // ── UserClaims additional builders ─────────────────────────────
1479
1480    #[test]
1481    fn user_claims_with_given_name() {
1482        let claims = UserClaims::new("u").with_given_name("Jane");
1483        assert_eq!(claims.given_name, Some("Jane".to_string()));
1484    }
1485
1486    #[test]
1487    fn user_claims_with_family_name() {
1488        let claims = UserClaims::new("u").with_family_name("Smith");
1489        assert_eq!(claims.family_name, Some("Smith".to_string()));
1490    }
1491
1492    #[test]
1493    fn user_claims_with_phone_number() {
1494        let claims = UserClaims::new("u").with_phone_number("+15551234567");
1495        assert_eq!(claims.phone_number, Some("+15551234567".to_string()));
1496    }
1497
1498    #[test]
1499    fn user_claims_with_updated_at() {
1500        let claims = UserClaims::new("u").with_updated_at(1700000000);
1501        assert_eq!(claims.updated_at, Some(1700000000));
1502    }
1503
1504    #[test]
1505    fn user_claims_with_picture() {
1506        let claims = UserClaims::new("u").with_picture("https://example.com/pic.jpg");
1507        assert_eq!(
1508            claims.picture,
1509            Some("https://example.com/pic.jpg".to_string())
1510        );
1511    }
1512
1513    #[test]
1514    fn user_claims_debug() {
1515        let claims = UserClaims::new("dbg-user");
1516        let debug = format!("{:?}", claims);
1517        assert!(debug.contains("dbg-user"));
1518    }
1519
1520    #[test]
1521    fn user_claims_clone() {
1522        let claims = UserClaims::new("u1").with_name("Alice");
1523        let cloned = claims.clone();
1524        assert_eq!(cloned.sub, "u1");
1525        assert_eq!(cloned.name, Some("Alice".to_string()));
1526    }
1527
1528    #[test]
1529    fn user_claims_default() {
1530        let claims = UserClaims::default();
1531        assert_eq!(claims.sub, "");
1532        assert!(claims.name.is_none());
1533        assert!(claims.email.is_none());
1534        assert!(claims.custom.is_empty());
1535    }
1536
1537    #[test]
1538    fn user_claims_serde_roundtrip() {
1539        let claims = UserClaims::new("serde-user")
1540            .with_name("Test")
1541            .with_email("test@example.com")
1542            .with_email_verified(true)
1543            .with_given_name("T")
1544            .with_family_name("Est")
1545            .with_phone_number("+1")
1546            .with_updated_at(123)
1547            .with_picture("http://pic")
1548            .with_preferred_username("tester")
1549            .with_custom("role", serde_json::json!("admin"));
1550
1551        let json = serde_json::to_string(&claims).unwrap();
1552        let deserialized: UserClaims = serde_json::from_str(&json).unwrap();
1553
1554        assert_eq!(deserialized.sub, "serde-user");
1555        assert_eq!(deserialized.name, Some("Test".to_string()));
1556        assert_eq!(deserialized.email, Some("test@example.com".to_string()));
1557        assert_eq!(deserialized.email_verified, Some(true));
1558        assert_eq!(deserialized.given_name, Some("T".to_string()));
1559        assert_eq!(deserialized.family_name, Some("Est".to_string()));
1560        assert_eq!(deserialized.phone_number, Some("+1".to_string()));
1561        assert_eq!(deserialized.updated_at, Some(123));
1562        assert_eq!(deserialized.picture, Some("http://pic".to_string()));
1563        assert_eq!(deserialized.preferred_username, Some("tester".to_string()));
1564        assert_eq!(
1565            deserialized.custom.get("role"),
1566            Some(&serde_json::json!("admin"))
1567        );
1568    }
1569
1570    #[test]
1571    fn user_claims_serde_skip_nones() {
1572        let claims = UserClaims::new("minimal");
1573        let json = serde_json::to_string(&claims).unwrap();
1574        // None fields should be skipped
1575        assert!(!json.contains("name"));
1576        assert!(!json.contains("email"));
1577        assert!(!json.contains("phone_number"));
1578        assert!(json.contains("sub"));
1579    }
1580
1581    #[test]
1582    fn filter_by_scopes_address() {
1583        let address = AddressClaim {
1584            formatted: Some("123 Main St".to_string()),
1585            ..Default::default()
1586        };
1587        let claims = UserClaims {
1588            sub: "u1".to_string(),
1589            address: Some(address),
1590            name: Some("Name".to_string()),
1591            ..Default::default()
1592        };
1593
1594        // Only address scope
1595        let filtered = claims.filter_by_scopes(&["address".to_string()]);
1596        assert!(filtered.address.is_some());
1597        assert!(filtered.name.is_none());
1598
1599        // Without address scope
1600        let filtered = claims.filter_by_scopes(&["profile".to_string()]);
1601        assert!(filtered.address.is_none());
1602        assert!(filtered.name.is_some());
1603    }
1604
1605    #[test]
1606    fn filter_by_scopes_phone_verified() {
1607        let claims = UserClaims {
1608            sub: "u1".to_string(),
1609            phone_number: Some("+1".to_string()),
1610            phone_number_verified: Some(true),
1611            ..Default::default()
1612        };
1613
1614        let filtered = claims.filter_by_scopes(&["phone".to_string()]);
1615        assert_eq!(filtered.phone_number, Some("+1".to_string()));
1616        assert_eq!(filtered.phone_number_verified, Some(true));
1617
1618        let filtered = claims.filter_by_scopes(&["email".to_string()]);
1619        assert!(filtered.phone_number.is_none());
1620        assert!(filtered.phone_number_verified.is_none());
1621    }
1622
1623    // ── AddressClaim ───────────────────────────────────────────────
1624
1625    #[test]
1626    fn address_claim_default() {
1627        let addr = AddressClaim::default();
1628        assert!(addr.formatted.is_none());
1629        assert!(addr.street_address.is_none());
1630        assert!(addr.locality.is_none());
1631        assert!(addr.region.is_none());
1632        assert!(addr.postal_code.is_none());
1633        assert!(addr.country.is_none());
1634    }
1635
1636    #[test]
1637    fn address_claim_debug() {
1638        let addr = AddressClaim {
1639            country: Some("US".to_string()),
1640            ..Default::default()
1641        };
1642        let debug = format!("{:?}", addr);
1643        assert!(debug.contains("US"));
1644    }
1645
1646    #[test]
1647    fn address_claim_clone() {
1648        let addr = AddressClaim {
1649            locality: Some("NYC".to_string()),
1650            ..Default::default()
1651        };
1652        let cloned = addr.clone();
1653        assert_eq!(cloned.locality, Some("NYC".to_string()));
1654    }
1655
1656    #[test]
1657    fn address_claim_serde_roundtrip() {
1658        let addr = AddressClaim {
1659            formatted: Some("123 Main St, City, ST 12345, US".to_string()),
1660            street_address: Some("123 Main St".to_string()),
1661            locality: Some("City".to_string()),
1662            region: Some("ST".to_string()),
1663            postal_code: Some("12345".to_string()),
1664            country: Some("US".to_string()),
1665        };
1666
1667        let json = serde_json::to_string(&addr).unwrap();
1668        let deserialized: AddressClaim = serde_json::from_str(&json).unwrap();
1669
1670        assert_eq!(deserialized.formatted, addr.formatted);
1671        assert_eq!(deserialized.country, addr.country);
1672    }
1673
1674    #[test]
1675    fn address_claim_serde_skip_nones() {
1676        let addr = AddressClaim {
1677            country: Some("US".to_string()),
1678            ..Default::default()
1679        };
1680        let json = serde_json::to_string(&addr).unwrap();
1681        assert!(json.contains("country"));
1682        assert!(!json.contains("formatted"));
1683        assert!(!json.contains("street_address"));
1684    }
1685
1686    // ── IdTokenClaims ──────────────────────────────────────────────
1687
1688    #[test]
1689    fn id_token_claims_debug() {
1690        let claims = IdTokenClaims {
1691            iss: "iss".to_string(),
1692            sub: "sub".to_string(),
1693            aud: "aud".to_string(),
1694            exp: 999,
1695            iat: 100,
1696            auth_time: None,
1697            nonce: None,
1698            acr: None,
1699            amr: None,
1700            azp: None,
1701            at_hash: None,
1702            c_hash: None,
1703            user_claims: UserClaims::new("sub"),
1704        };
1705        let debug = format!("{:?}", claims);
1706        assert!(debug.contains("iss"));
1707        assert!(debug.contains("sub"));
1708    }
1709
1710    #[test]
1711    fn id_token_claims_clone() {
1712        let claims = IdTokenClaims {
1713            iss: "issuer".to_string(),
1714            sub: "subject".to_string(),
1715            aud: "audience".to_string(),
1716            exp: 999,
1717            iat: 100,
1718            auth_time: Some(100),
1719            nonce: Some("n".to_string()),
1720            acr: Some("1".to_string()),
1721            amr: Some(vec!["pwd".to_string()]),
1722            azp: Some("azp".to_string()),
1723            at_hash: Some("hash".to_string()),
1724            c_hash: Some("chash".to_string()),
1725            user_claims: UserClaims::new("subject"),
1726        };
1727        let cloned = claims.clone();
1728        assert_eq!(cloned.iss, "issuer");
1729        assert_eq!(cloned.nonce, Some("n".to_string()));
1730        assert_eq!(cloned.amr, Some(vec!["pwd".to_string()]));
1731    }
1732
1733    #[test]
1734    fn id_token_claims_serialization() {
1735        // Note: IdTokenClaims.sub and user_claims.sub overlap due to
1736        // #[serde(flatten)], so we test serialization output rather than
1737        // a full roundtrip.
1738        let claims = IdTokenClaims {
1739            iss: "fastmcp".to_string(),
1740            sub: "user1".to_string(),
1741            aud: "client1".to_string(),
1742            exp: 1700001000,
1743            iat: 1700000000,
1744            auth_time: Some(1700000000),
1745            nonce: Some("abc".to_string()),
1746            acr: None,
1747            amr: None,
1748            azp: Some("client1".to_string()),
1749            at_hash: Some("h".to_string()),
1750            c_hash: None,
1751            user_claims: UserClaims::new("user1").with_name("Test"),
1752        };
1753        let json = serde_json::to_string(&claims).unwrap();
1754        assert!(json.contains("\"iss\":\"fastmcp\""));
1755        assert!(json.contains("\"aud\":\"client1\""));
1756        assert!(json.contains("\"exp\":1700001000"));
1757        assert!(json.contains("\"nonce\":\"abc\""));
1758        assert!(json.contains("\"name\":\"Test\""));
1759    }
1760
1761    #[test]
1762    fn id_token_claims_serde_skip_nones() {
1763        let claims = IdTokenClaims {
1764            iss: "i".to_string(),
1765            sub: "s".to_string(),
1766            aud: "a".to_string(),
1767            exp: 1,
1768            iat: 0,
1769            auth_time: None,
1770            nonce: None,
1771            acr: None,
1772            amr: None,
1773            azp: None,
1774            at_hash: None,
1775            c_hash: None,
1776            user_claims: UserClaims::new("s"),
1777        };
1778        let json = serde_json::to_string(&claims).unwrap();
1779        assert!(!json.contains("nonce"));
1780        assert!(!json.contains("auth_time"));
1781        assert!(!json.contains("acr"));
1782    }
1783
1784    // ── IdToken ────────────────────────────────────────────────────
1785
1786    #[test]
1787    fn id_token_debug() {
1788        let claims = IdTokenClaims {
1789            iss: "i".to_string(),
1790            sub: "s".to_string(),
1791            aud: "a".to_string(),
1792            exp: 1,
1793            iat: 0,
1794            auth_time: None,
1795            nonce: None,
1796            acr: None,
1797            amr: None,
1798            azp: None,
1799            at_hash: None,
1800            c_hash: None,
1801            user_claims: UserClaims::new("s"),
1802        };
1803        let token = IdToken {
1804            raw: "header.payload.sig".to_string(),
1805            claims,
1806        };
1807        let debug = format!("{:?}", token);
1808        assert!(debug.contains("header.payload.sig"));
1809    }
1810
1811    #[test]
1812    fn id_token_clone() {
1813        let claims = IdTokenClaims {
1814            iss: "i".to_string(),
1815            sub: "s".to_string(),
1816            aud: "a".to_string(),
1817            exp: 1,
1818            iat: 0,
1819            auth_time: None,
1820            nonce: None,
1821            acr: None,
1822            amr: None,
1823            azp: None,
1824            at_hash: None,
1825            c_hash: None,
1826            user_claims: UserClaims::new("s"),
1827        };
1828        let token = IdToken {
1829            raw: "jwt-token".to_string(),
1830            claims,
1831        };
1832        let cloned = token.clone();
1833        assert_eq!(cloned.raw, "jwt-token");
1834        assert_eq!(cloned.claims.sub, "s");
1835    }
1836
1837    // ── DiscoveryDocument ──────────────────────────────────────────
1838
1839    #[test]
1840    fn discovery_document_new_defaults() {
1841        let doc = DiscoveryDocument::new("https://issuer.example", "https://api.example");
1842        assert_eq!(doc.issuer, "https://issuer.example");
1843        assert_eq!(doc.authorization_endpoint, "https://api.example/authorize");
1844        assert_eq!(doc.token_endpoint, "https://api.example/token");
1845        assert_eq!(
1846            doc.userinfo_endpoint,
1847            Some("https://api.example/userinfo".to_string())
1848        );
1849        assert_eq!(
1850            doc.revocation_endpoint,
1851            Some("https://api.example/revoke".to_string())
1852        );
1853        assert!(doc.jwks_uri.is_none());
1854        assert!(doc.registration_endpoint.is_none());
1855        assert!(doc.scopes_supported.contains(&"openid".to_string()));
1856        assert_eq!(doc.response_types_supported, vec!["code"]);
1857        assert_eq!(
1858            doc.response_modes_supported,
1859            Some(vec!["query".to_string()])
1860        );
1861        assert!(
1862            doc.grant_types_supported
1863                .contains(&"authorization_code".to_string())
1864        );
1865        assert!(
1866            doc.grant_types_supported
1867                .contains(&"refresh_token".to_string())
1868        );
1869        assert_eq!(doc.subject_types_supported, vec!["public"]);
1870        assert_eq!(doc.id_token_signing_alg_values_supported, vec!["HS256"]);
1871        assert!(
1872            doc.token_endpoint_auth_methods_supported
1873                .contains(&"client_secret_post".to_string())
1874        );
1875        assert!(doc.claims_supported.is_some());
1876        assert!(
1877            doc.code_challenge_methods_supported
1878                .as_ref()
1879                .unwrap()
1880                .contains(&"S256".to_string())
1881        );
1882    }
1883
1884    #[test]
1885    fn discovery_document_debug() {
1886        let doc = DiscoveryDocument::new("iss", "http://base");
1887        let debug = format!("{:?}", doc);
1888        assert!(debug.contains("iss"));
1889    }
1890
1891    #[test]
1892    fn discovery_document_clone() {
1893        let doc = DiscoveryDocument::new("iss", "http://base");
1894        let cloned = doc.clone();
1895        assert_eq!(cloned.issuer, "iss");
1896        assert_eq!(cloned.token_endpoint, doc.token_endpoint);
1897    }
1898
1899    #[test]
1900    fn discovery_document_serde_roundtrip() {
1901        let doc = DiscoveryDocument::new("iss", "http://base");
1902        let json = serde_json::to_string(&doc).unwrap();
1903        let deserialized: DiscoveryDocument = serde_json::from_str(&json).unwrap();
1904        assert_eq!(deserialized.issuer, "iss");
1905        assert_eq!(deserialized.token_endpoint, "http://base/token");
1906        assert_eq!(
1907            deserialized.userinfo_endpoint,
1908            Some("http://base/userinfo".to_string())
1909        );
1910    }
1911
1912    // ── OidcError ──────────────────────────────────────────────────
1913
1914    #[test]
1915    fn oidc_error_oauth_display() {
1916        let inner = OAuthError::InvalidClient("bad".to_string());
1917        let err = OidcError::OAuth(inner);
1918        let msg = err.to_string();
1919        assert!(msg.contains("OAuth error"));
1920        assert!(msg.contains("bad"));
1921    }
1922
1923    #[test]
1924    fn oidc_error_signing_error_display() {
1925        let err = OidcError::SigningError("key problem".to_string());
1926        assert!(err.to_string().contains("signing error"));
1927        assert!(err.to_string().contains("key problem"));
1928    }
1929
1930    #[test]
1931    fn oidc_error_invalid_id_token_display() {
1932        let err = OidcError::InvalidIdToken("malformed".to_string());
1933        assert!(err.to_string().contains("invalid ID token"));
1934        assert!(err.to_string().contains("malformed"));
1935    }
1936
1937    #[test]
1938    fn oidc_error_debug() {
1939        let err = OidcError::MissingOpenIdScope;
1940        let debug = format!("{:?}", err);
1941        assert!(debug.contains("MissingOpenIdScope"));
1942    }
1943
1944    #[test]
1945    fn oidc_error_clone() {
1946        let err = OidcError::ClaimsNotFound("u".to_string());
1947        let cloned = err.clone();
1948        assert!(cloned.to_string().contains('u'));
1949    }
1950
1951    #[test]
1952    fn oidc_error_std_error() {
1953        let err = OidcError::SigningError("x".to_string());
1954        let std_err: &dyn std::error::Error = &err;
1955        assert!(std_err.to_string().contains('x'));
1956    }
1957
1958    #[test]
1959    fn oidc_error_from_oauth_error() {
1960        let oauth_err = OAuthError::InvalidGrant("expired".to_string());
1961        let oidc_err: OidcError = oauth_err.into();
1962        match &oidc_err {
1963            OidcError::OAuth(inner) => {
1964                assert!(inner.to_string().contains("expired"));
1965            }
1966            _ => panic!("expected OAuth variant"),
1967        }
1968    }
1969
1970    // ── OidcProvider ───────────────────────────────────────────────
1971
1972    #[test]
1973    fn provider_config_accessor() {
1974        let provider = create_test_provider();
1975        let cfg = provider.config();
1976        assert_eq!(cfg.issuer, "fastmcp");
1977        assert_eq!(cfg.signing_algorithm, SigningAlgorithm::HS256);
1978    }
1979
1980    #[test]
1981    fn provider_oauth_accessor() {
1982        let provider = create_test_provider();
1983        let _oauth = provider.oauth();
1984        // Just check it returns without panic
1985    }
1986
1987    #[test]
1988    fn provider_set_hmac_key() {
1989        let provider = create_test_provider();
1990        provider.set_hmac_key(b"my-secret");
1991        // Verify it works by issuing a token (the key gets used during signing)
1992        let claims_store = InMemoryClaimsProvider::new();
1993        claims_store.set_claims(UserClaims::new("user1"));
1994        provider.set_claims_provider(claims_store);
1995
1996        let at = issue_token_via_auth_code(provider.oauth().as_ref(), &["openid"], "user1");
1997        let oauth_token = provider.oauth().validate_access_token(&at).unwrap();
1998        let result = provider.issue_id_token(&oauth_token, None);
1999        assert!(result.is_ok());
2000    }
2001
2002    #[test]
2003    fn provider_set_claims_fn() {
2004        let provider = create_test_provider();
2005        provider.set_claims_fn(|sub| Some(UserClaims::new(sub).with_name("FnUser")));
2006        provider.set_hmac_key(b"secret");
2007
2008        let at =
2009            issue_token_via_auth_code(provider.oauth().as_ref(), &["openid", "profile"], "fn-user");
2010        let oauth_token = provider.oauth().validate_access_token(&at).unwrap();
2011        let id_token = provider.issue_id_token(&oauth_token, None).unwrap();
2012        assert_eq!(id_token.claims.user_claims.name, Some("FnUser".to_string()));
2013    }
2014
2015    #[test]
2016    fn provider_jwks_hs256_returns_none() {
2017        let provider = create_test_provider();
2018        assert!(provider.jwks().is_none());
2019    }
2020
2021    #[test]
2022    fn provider_get_id_token_nonexistent() {
2023        let provider = create_test_provider();
2024        assert!(provider.get_id_token("nonexistent-token").is_none());
2025    }
2026
2027    #[test]
2028    fn provider_get_id_token_after_issue() {
2029        let provider = create_test_provider();
2030        provider.set_hmac_key(b"key123");
2031        let claims_store = InMemoryClaimsProvider::new();
2032        claims_store.set_claims(UserClaims::new("cached-user"));
2033        provider.set_claims_provider(claims_store);
2034
2035        let at = issue_token_via_auth_code(provider.oauth().as_ref(), &["openid"], "cached-user");
2036        let oauth_token = provider.oauth().validate_access_token(&at).unwrap();
2037        provider
2038            .issue_id_token(&oauth_token, Some("nonce1"))
2039            .unwrap();
2040
2041        // Should be retrievable via get_id_token
2042        let retrieved = provider.get_id_token(&at);
2043        assert!(retrieved.is_some());
2044        assert_eq!(retrieved.unwrap().claims.sub, "cached-user");
2045    }
2046
2047    #[test]
2048    fn provider_cleanup_expired() {
2049        let provider = create_test_provider();
2050        // cleanup_expired should run without panic even with empty cache
2051        provider.cleanup_expired();
2052    }
2053
2054    #[test]
2055    fn provider_issue_id_token_no_claims_provider() {
2056        // Without a claims provider, should use default UserClaims (just sub)
2057        let provider = create_test_provider();
2058        provider.set_hmac_key(b"key");
2059
2060        let at = issue_token_via_auth_code(provider.oauth().as_ref(), &["openid"], "default-user");
2061        let oauth_token = provider.oauth().validate_access_token(&at).unwrap();
2062        let id_token = provider.issue_id_token(&oauth_token, None).unwrap();
2063        assert_eq!(id_token.claims.sub, "default-user");
2064        // No claims provider → default claims (no name, email, etc.)
2065        assert!(id_token.claims.user_claims.name.is_none());
2066    }
2067
2068    #[test]
2069    fn provider_issue_id_token_claims_not_found() {
2070        let provider = create_test_provider();
2071        provider.set_hmac_key(b"key");
2072        // Set claims provider that has no user "missing-user"
2073        let claims_store = InMemoryClaimsProvider::new();
2074        claims_store.set_claims(UserClaims::new("other-user"));
2075        provider.set_claims_provider(claims_store);
2076
2077        let at = issue_token_via_auth_code(provider.oauth().as_ref(), &["openid"], "missing-user");
2078        let oauth_token = provider.oauth().validate_access_token(&at).unwrap();
2079        let result = provider.issue_id_token(&oauth_token, None);
2080        assert!(matches!(result, Err(OidcError::ClaimsNotFound(_))));
2081    }
2082
2083    #[test]
2084    fn provider_discovery_document_overrides_scopes_and_claims() {
2085        let mut config = OidcProviderConfig::default();
2086        config.supported_scopes = vec!["openid".to_string(), "custom".to_string()];
2087        config.supported_claims = vec!["sub".to_string(), "custom_field".to_string()];
2088
2089        let oauth = Arc::new(OAuthServer::new(OAuthServerConfig::default()));
2090        let provider = OidcProvider::new(oauth, config).unwrap();
2091        let doc = provider.discovery_document("https://api");
2092
2093        assert!(doc.scopes_supported.contains(&"custom".to_string()));
2094        assert!(!doc.scopes_supported.contains(&"profile".to_string()));
2095        assert!(
2096            doc.claims_supported
2097                .as_ref()
2098                .unwrap()
2099                .contains(&"custom_field".to_string())
2100        );
2101    }
2102
2103    #[test]
2104    fn provider_issue_id_token_jwt_structure() {
2105        let provider = create_test_provider();
2106        provider.set_hmac_key(b"secret");
2107
2108        let at = issue_token_via_auth_code(provider.oauth().as_ref(), &["openid"], "jwt-user");
2109        let oauth_token = provider.oauth().validate_access_token(&at).unwrap();
2110        let id_token = provider.issue_id_token(&oauth_token, None).unwrap();
2111
2112        // JWT must have 3 parts
2113        let parts: Vec<&str> = id_token.raw.split('.').collect();
2114        assert_eq!(parts.len(), 3);
2115        // Each part is non-empty
2116        assert!(!parts[0].is_empty());
2117        assert!(!parts[1].is_empty());
2118        assert!(!parts[2].is_empty());
2119    }
2120
2121    #[test]
2122    fn provider_issue_id_token_auto_generates_key() {
2123        // Without set_hmac_key, should auto-generate a key
2124        let provider = create_test_provider();
2125
2126        let at = issue_token_via_auth_code(provider.oauth().as_ref(), &["openid"], "auto-key-user");
2127        let oauth_token = provider.oauth().validate_access_token(&at).unwrap();
2128        let result = provider.issue_id_token(&oauth_token, None);
2129        assert!(result.is_ok());
2130    }
2131
2132    #[test]
2133    fn provider_userinfo_invalid_token() {
2134        let provider = create_test_provider();
2135        let result = provider.userinfo("invalid-access-token");
2136        assert!(matches!(result, Err(OidcError::OAuth(_))));
2137    }
2138
2139    #[test]
2140    fn provider_userinfo_without_openid_scope() {
2141        let provider = create_test_provider();
2142        let at = issue_token_via_auth_code(provider.oauth().as_ref(), &["profile"], "no-openid");
2143        let result = provider.userinfo(&at);
2144        assert!(matches!(result, Err(OidcError::MissingOpenIdScope)));
2145    }
2146
2147    // ── Arc<dyn ClaimsProvider> delegation ──────────────────────────
2148
2149    #[test]
2150    fn arc_claims_provider_delegation() {
2151        let inner = InMemoryClaimsProvider::new();
2152        inner.set_claims(UserClaims::new("arc-user").with_name("ArcUser"));
2153        let arc_provider: Arc<dyn ClaimsProvider> = Arc::new(inner);
2154
2155        // Arc<dyn ClaimsProvider> should delegate
2156        let claims = arc_provider.get_claims("arc-user");
2157        assert!(claims.is_some());
2158        assert_eq!(claims.unwrap().name, Some("ArcUser".to_string()));
2159
2160        assert!(arc_provider.get_claims("missing").is_none());
2161    }
2162
2163    // ── InMemoryClaimsProvider ──────────────────────────────────────
2164
2165    #[test]
2166    fn in_memory_claims_provider_debug() {
2167        let provider = InMemoryClaimsProvider::new();
2168        let debug = format!("{:?}", provider);
2169        assert!(debug.contains("InMemoryClaimsProvider"));
2170    }
2171
2172    #[test]
2173    fn in_memory_claims_provider_default() {
2174        let provider = InMemoryClaimsProvider::default();
2175        assert!(provider.get_claims("any").is_none());
2176    }
2177
2178    #[test]
2179    fn in_memory_claims_provider_overwrite() {
2180        let provider = InMemoryClaimsProvider::new();
2181        provider.set_claims(UserClaims::new("u1").with_name("First"));
2182        provider.set_claims(UserClaims::new("u1").with_name("Second"));
2183        let claims = provider.get_claims("u1").unwrap();
2184        assert_eq!(claims.name, Some("Second".to_string()));
2185    }
2186
2187    // ── Helper functions ────────────────────────────────────────────
2188
2189    #[test]
2190    fn simple_sha256_deterministic() {
2191        let hash1 = simple_sha256(b"hello world");
2192        let hash2 = simple_sha256(b"hello world");
2193        assert_eq!(hash1, hash2);
2194        assert_eq!(hash1.len(), 32);
2195
2196        // Different input → different hash
2197        let hash3 = simple_sha256(b"different");
2198        assert_ne!(hash1, hash3);
2199    }
2200
2201    #[test]
2202    fn hmac_sha256_deterministic() {
2203        let sig1 = hmac_sha256("message", b"key").unwrap();
2204        let sig2 = hmac_sha256("message", b"key").unwrap();
2205        assert_eq!(sig1, sig2);
2206        assert_eq!(sig1.len(), 32);
2207    }
2208
2209    #[test]
2210    fn hmac_sha256_different_keys() {
2211        let sig1 = hmac_sha256("msg", b"key1").unwrap();
2212        let sig2 = hmac_sha256("msg", b"key2").unwrap();
2213        assert_ne!(sig1, sig2);
2214    }
2215
2216    #[test]
2217    fn hmac_sha256_different_messages() {
2218        let sig1 = hmac_sha256("msg1", b"key").unwrap();
2219        let sig2 = hmac_sha256("msg2", b"key").unwrap();
2220        assert_ne!(sig1, sig2);
2221    }
2222
2223    #[test]
2224    fn generate_random_bytes_length() {
2225        let bytes = generate_random_bytes(16).unwrap();
2226        assert_eq!(bytes.len(), 16);
2227
2228        let bytes = generate_random_bytes(64).unwrap();
2229        assert_eq!(bytes.len(), 64);
2230    }
2231
2232    #[test]
2233    fn generate_random_bytes_unique() {
2234        let a = generate_random_bytes(32).unwrap();
2235        let b = generate_random_bytes(32).unwrap();
2236        // Probabilistically, two random 32-byte sequences should differ
2237        assert_ne!(a, b);
2238    }
2239
2240    // ── validate_oidc_config ───────────────────────────────────────
2241
2242    #[test]
2243    fn validate_config_hs256_always_ok() {
2244        let config = OidcProviderConfig::default();
2245        assert!(validate_oidc_config(&config).is_ok());
2246    }
2247
2248    #[test]
2249    #[cfg(not(feature = "jwt"))]
2250    fn validate_config_rs256_without_jwt_feature() {
2251        let mut config = OidcProviderConfig::default();
2252        config.signing_algorithm = SigningAlgorithm::RS256;
2253        let result = validate_oidc_config(&config);
2254        assert!(result.is_err());
2255        assert!(result.unwrap_err().to_string().contains("jwt"));
2256    }
2257
2258    // ── SigningKey ──────────────────────────────────────────────────
2259
2260    #[test]
2261    fn signing_key_default_is_none() {
2262        let key = SigningKey::default();
2263        assert!(matches!(key, SigningKey::None));
2264    }
2265
2266    #[test]
2267    fn signing_key_clone() {
2268        let key = SigningKey::Hmac(vec![1, 2, 3]);
2269        let cloned = key.clone();
2270        match cloned {
2271            SigningKey::Hmac(bytes) => assert_eq!(bytes, vec![1, 2, 3]),
2272            SigningKey::None => panic!("expected Hmac"),
2273        }
2274    }
2275
2276    #[test]
2277    fn userinfo_claims_not_found() {
2278        let provider = create_test_provider();
2279        // Set a claims provider that only knows about "other-user"
2280        let store = InMemoryClaimsProvider::new();
2281        store.set_claims(UserClaims::new("other-user"));
2282        provider.set_claims_provider(store);
2283
2284        let at = issue_token_via_auth_code(
2285            provider.oauth().as_ref(),
2286            &["openid", "profile"],
2287            "missing-user",
2288        );
2289        let result = provider.userinfo(&at);
2290        assert!(matches!(result, Err(OidcError::ClaimsNotFound(_))));
2291    }
2292
2293    #[test]
2294    fn filter_by_scopes_profile_all_fields() {
2295        let claims = UserClaims {
2296            sub: "u".to_string(),
2297            name: Some("N".to_string()),
2298            given_name: Some("G".to_string()),
2299            family_name: Some("F".to_string()),
2300            middle_name: Some("M".to_string()),
2301            nickname: Some("Nick".to_string()),
2302            preferred_username: Some("Pref".to_string()),
2303            profile: Some("http://profile".to_string()),
2304            picture: Some("http://pic".to_string()),
2305            website: Some("http://web".to_string()),
2306            gender: Some("other".to_string()),
2307            birthdate: Some("2000-01-01".to_string()),
2308            zoneinfo: Some("UTC".to_string()),
2309            locale: Some("en-US".to_string()),
2310            updated_at: Some(12345),
2311            ..Default::default()
2312        };
2313
2314        let filtered = claims.filter_by_scopes(&["profile".to_string()]);
2315        assert_eq!(filtered.name, Some("N".to_string()));
2316        assert_eq!(filtered.given_name, Some("G".to_string()));
2317        assert_eq!(filtered.family_name, Some("F".to_string()));
2318        assert_eq!(filtered.middle_name, Some("M".to_string()));
2319        assert_eq!(filtered.nickname, Some("Nick".to_string()));
2320        assert_eq!(filtered.preferred_username, Some("Pref".to_string()));
2321        assert_eq!(filtered.profile, Some("http://profile".to_string()));
2322        assert_eq!(filtered.picture, Some("http://pic".to_string()));
2323        assert_eq!(filtered.website, Some("http://web".to_string()));
2324        assert_eq!(filtered.gender, Some("other".to_string()));
2325        assert_eq!(filtered.birthdate, Some("2000-01-01".to_string()));
2326        assert_eq!(filtered.zoneinfo, Some("UTC".to_string()));
2327        assert_eq!(filtered.locale, Some("en-US".to_string()));
2328        assert_eq!(filtered.updated_at, Some(12345));
2329    }
2330
2331    #[test]
2332    fn filter_by_scopes_does_not_include_custom_claims() {
2333        let claims = UserClaims::new("u")
2334            .with_name("Name")
2335            .with_custom("role", serde_json::json!("admin"));
2336
2337        let filtered = claims.filter_by_scopes(&["profile".to_string()]);
2338        assert_eq!(filtered.name, Some("Name".to_string()));
2339        assert!(
2340            filtered.custom.is_empty(),
2341            "custom claims should not pass through scope filtering"
2342        );
2343    }
2344
2345    #[test]
2346    fn issue_id_token_fields_populated() {
2347        let provider = create_test_provider();
2348        provider.set_hmac_key(b"key");
2349
2350        let at = issue_token_via_auth_code(provider.oauth().as_ref(), &["openid"], "field-user");
2351        let oauth_token = provider.oauth().validate_access_token(&at).unwrap();
2352        let id_token = provider.issue_id_token(&oauth_token, None).unwrap();
2353
2354        assert_eq!(id_token.claims.iss, "fastmcp");
2355        assert_eq!(id_token.claims.aud, TEST_CLIENT_ID);
2356        assert_eq!(id_token.claims.azp, Some(TEST_CLIENT_ID.to_string()));
2357        assert!(id_token.claims.at_hash.is_some());
2358        assert!(id_token.claims.auth_time.is_some());
2359        assert!(id_token.claims.nonce.is_none());
2360        assert!(id_token.claims.exp > id_token.claims.iat);
2361    }
2362
2363    #[test]
2364    fn base64url_encode_url_safe_characters() {
2365        // Bytes 0xFB, 0xFF, 0xFE produce +//+ in standard base64,
2366        // but base64url should use - and _ instead.
2367        let encoded = base64url_encode(&[0xFB, 0xFF, 0xFE]);
2368        assert!(
2369            !encoded.contains('+') && !encoded.contains('/'),
2370            "base64url must not contain + or /: {encoded}"
2371        );
2372        assert!(
2373            encoded.contains('-') || encoded.contains('_'),
2374            "base64url should use URL-safe chars: {encoded}"
2375        );
2376    }
2377
2378    #[test]
2379    fn discovery_document_serde_skip_nones() {
2380        let mut doc = DiscoveryDocument::new("iss", "http://base");
2381        doc.jwks_uri = None;
2382        doc.registration_endpoint = None;
2383        let json = serde_json::to_string(&doc).unwrap();
2384        assert!(!json.contains("jwks_uri"));
2385        assert!(!json.contains("registration_endpoint"));
2386        // Present optional fields should be in the JSON
2387        assert!(json.contains("userinfo_endpoint"));
2388        assert!(json.contains("revocation_endpoint"));
2389    }
2390
2391    #[test]
2392    fn issue_id_token_with_nonce_round_trips() {
2393        let provider = create_test_provider();
2394        provider.set_hmac_key(b"key");
2395
2396        let at = issue_token_via_auth_code(provider.oauth().as_ref(), &["openid"], "nonce-rt-user");
2397        let oauth_token = provider.oauth().validate_access_token(&at).unwrap();
2398
2399        // With nonce
2400        let with = provider
2401            .issue_id_token(&oauth_token, Some("my-nonce"))
2402            .unwrap();
2403        assert_eq!(with.claims.nonce, Some("my-nonce".to_string()));
2404
2405        // Verify the nonce appears in the raw JWT payload
2406        let parts: Vec<&str> = with.raw.split('.').collect();
2407        use base64::Engine;
2408        let payload_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
2409            .decode(parts[1])
2410            .unwrap();
2411        let payload: serde_json::Value = serde_json::from_slice(&payload_bytes).unwrap();
2412        assert_eq!(payload["nonce"], "my-nonce");
2413    }
2414}