1use std::collections::HashMap;
41use std::sync::{Arc, RwLock};
42use std::time::{Duration, SystemTime, UNIX_EPOCH};
43
44use crate::oauth::{OAuthError, OAuthServer, OAuthToken};
45
46#[derive(Debug, Clone)]
52pub struct OidcProviderConfig {
53 pub issuer: String,
55 pub id_token_lifetime: Duration,
57 pub signing_algorithm: SigningAlgorithm,
59 pub key_id: Option<String>,
61 pub rsa_private_key_pem: Option<Vec<u8>>,
65 pub jwks: Option<serde_json::Value>,
69 pub supported_claims: Vec<String>,
71 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), 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum SigningAlgorithm {
105 HS256,
107 RS256,
109}
110
111impl SigningAlgorithm {
112 #[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#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
131pub struct UserClaims {
132 pub sub: String,
134
135 #[serde(skip_serializing_if = "Option::is_none")]
138 pub name: Option<String>,
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub given_name: Option<String>,
142 #[serde(skip_serializing_if = "Option::is_none")]
144 pub family_name: Option<String>,
145 #[serde(skip_serializing_if = "Option::is_none")]
147 pub middle_name: Option<String>,
148 #[serde(skip_serializing_if = "Option::is_none")]
150 pub nickname: Option<String>,
151 #[serde(skip_serializing_if = "Option::is_none")]
153 pub preferred_username: Option<String>,
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub profile: Option<String>,
157 #[serde(skip_serializing_if = "Option::is_none")]
159 pub picture: Option<String>,
160 #[serde(skip_serializing_if = "Option::is_none")]
162 pub website: Option<String>,
163 #[serde(skip_serializing_if = "Option::is_none")]
165 pub gender: Option<String>,
166 #[serde(skip_serializing_if = "Option::is_none")]
168 pub birthdate: Option<String>,
169 #[serde(skip_serializing_if = "Option::is_none")]
171 pub zoneinfo: Option<String>,
172 #[serde(skip_serializing_if = "Option::is_none")]
174 pub locale: Option<String>,
175 #[serde(skip_serializing_if = "Option::is_none")]
177 pub updated_at: Option<i64>,
178
179 #[serde(skip_serializing_if = "Option::is_none")]
182 pub email: Option<String>,
183 #[serde(skip_serializing_if = "Option::is_none")]
185 pub email_verified: Option<bool>,
186
187 #[serde(skip_serializing_if = "Option::is_none")]
190 pub phone_number: Option<String>,
191 #[serde(skip_serializing_if = "Option::is_none")]
193 pub phone_number_verified: Option<bool>,
194
195 #[serde(skip_serializing_if = "Option::is_none")]
198 pub address: Option<AddressClaim>,
199
200 #[serde(flatten)]
202 pub custom: HashMap<String, serde_json::Value>,
203}
204
205impl UserClaims {
206 #[must_use]
208 pub fn new(sub: impl Into<String>) -> Self {
209 Self {
210 sub: sub.into(),
211 ..Default::default()
212 }
213 }
214
215 #[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 #[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 #[must_use]
231 pub fn with_email_verified(mut self, verified: bool) -> Self {
232 self.email_verified = Some(verified);
233 self
234 }
235
236 #[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 #[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 #[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 #[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 #[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 #[must_use]
273 pub fn with_updated_at(mut self, timestamp: i64) -> Self {
274 self.updated_at = Some(timestamp);
275 self
276 }
277
278 #[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 #[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 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 if scopes.iter().any(|s| s == "email") {
313 filtered.email = self.email.clone();
314 filtered.email_verified = self.email_verified;
315 }
316
317 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 if scopes.iter().any(|s| s == "address") {
325 filtered.address = self.address.clone();
326 }
327
328 filtered
329 }
330}
331
332#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
334pub struct AddressClaim {
335 #[serde(skip_serializing_if = "Option::is_none")]
337 pub formatted: Option<String>,
338 #[serde(skip_serializing_if = "Option::is_none")]
340 pub street_address: Option<String>,
341 #[serde(skip_serializing_if = "Option::is_none")]
343 pub locality: Option<String>,
344 #[serde(skip_serializing_if = "Option::is_none")]
346 pub region: Option<String>,
347 #[serde(skip_serializing_if = "Option::is_none")]
349 pub postal_code: Option<String>,
350 #[serde(skip_serializing_if = "Option::is_none")]
352 pub country: Option<String>,
353}
354
355#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
361pub struct IdTokenClaims {
362 pub iss: String,
364 pub sub: String,
366 pub aud: String,
368 pub exp: i64,
370 pub iat: i64,
372 #[serde(skip_serializing_if = "Option::is_none")]
374 pub auth_time: Option<i64>,
375 #[serde(skip_serializing_if = "Option::is_none")]
377 pub nonce: Option<String>,
378 #[serde(skip_serializing_if = "Option::is_none")]
380 pub acr: Option<String>,
381 #[serde(skip_serializing_if = "Option::is_none")]
383 pub amr: Option<Vec<String>>,
384 #[serde(skip_serializing_if = "Option::is_none")]
386 pub azp: Option<String>,
387 #[serde(skip_serializing_if = "Option::is_none")]
389 pub at_hash: Option<String>,
390 #[serde(skip_serializing_if = "Option::is_none")]
392 pub c_hash: Option<String>,
393 #[serde(flatten)]
395 pub user_claims: UserClaims,
396}
397
398#[derive(Debug, Clone)]
400pub struct IdToken {
401 pub raw: String,
403 pub claims: IdTokenClaims,
405}
406
407#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
415pub struct DiscoveryDocument {
416 pub issuer: String,
418 pub authorization_endpoint: String,
420 pub token_endpoint: String,
422 #[serde(skip_serializing_if = "Option::is_none")]
424 pub userinfo_endpoint: Option<String>,
425 #[serde(skip_serializing_if = "Option::is_none")]
427 pub jwks_uri: Option<String>,
428 #[serde(skip_serializing_if = "Option::is_none")]
430 pub registration_endpoint: Option<String>,
431 #[serde(skip_serializing_if = "Option::is_none")]
433 pub revocation_endpoint: Option<String>,
434 pub scopes_supported: Vec<String>,
436 pub response_types_supported: Vec<String>,
438 #[serde(skip_serializing_if = "Option::is_none")]
440 pub response_modes_supported: Option<Vec<String>>,
441 pub grant_types_supported: Vec<String>,
443 pub subject_types_supported: Vec<String>,
445 pub id_token_signing_alg_values_supported: Vec<String>,
447 pub token_endpoint_auth_methods_supported: Vec<String>,
449 #[serde(skip_serializing_if = "Option::is_none")]
451 pub claims_supported: Option<Vec<String>>,
452 #[serde(skip_serializing_if = "Option::is_none")]
454 pub code_challenge_methods_supported: Option<Vec<String>>,
455}
456
457impl DiscoveryDocument {
458 #[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
506pub trait ClaimsProvider: Send + Sync {
512 fn get_claims(&self, subject: &str) -> Option<UserClaims>;
516}
517
518#[derive(Debug, Default)]
520pub struct InMemoryClaimsProvider {
521 claims: RwLock<HashMap<String, UserClaims>>,
522}
523
524impl InMemoryClaimsProvider {
525 #[must_use]
527 pub fn new() -> Self {
528 Self::default()
529 }
530
531 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 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
555pub 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 #[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#[derive(Debug, Clone)]
595pub enum OidcError {
596 OAuth(OAuthError),
598 MissingOpenIdScope,
600 ClaimsNotFound(String),
602 SigningError(String),
604 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
628pub struct OidcProvider {
636 oauth: Arc<OAuthServer>,
638 config: OidcProviderConfig,
640 signing_key: RwLock<SigningKey>,
642 claims_provider: RwLock<Option<Arc<dyn ClaimsProvider>>>,
644 id_tokens: RwLock<HashMap<String, IdToken>>,
646}
647
648#[derive(Clone, Default)]
650enum SigningKey {
651 Hmac(Vec<u8>),
653 #[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 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 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 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 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 pub fn with_defaults(oauth: Arc<OAuthServer>) -> Result<Self, OidcError> {
743 Self::new(oauth, OidcProviderConfig::default())
744 }
745
746 #[must_use]
748 pub fn config(&self) -> &OidcProviderConfig {
749 &self.config
750 }
751
752 #[must_use]
754 pub fn oauth(&self) -> &Arc<OAuthServer> {
755 &self.oauth
756 }
757
758 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 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 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 #[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 #[must_use]
801 pub fn jwks(&self) -> Option<serde_json::Value> {
802 self.config.jwks.clone()
803 }
804
805 pub fn issue_id_token(
814 &self,
815 access_token: &OAuthToken,
816 nonce: Option<&str>,
817 ) -> Result<IdToken, OidcError> {
818 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 let user_claims = self.get_user_claims(subject, &access_token.scopes)?;
830
831 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 let raw = self.sign_id_token(&claims)?;
855
856 let issued = IdToken { raw, claims };
857
858 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 #[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 pub fn userinfo(&self, access_token: &str) -> Result<UserClaims, OidcError> {
883 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 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 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 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 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 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 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 let hash = simple_sha256(access_token.as_bytes());
1028 base64url_encode(&hash[..16])
1029 }
1030
1031 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
1044fn 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
1077fn 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#[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 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 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 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 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 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 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 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 #[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 #[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 #[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 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 let filtered = claims.filter_by_scopes(&["address".to_string()]);
1596 assert!(filtered.address.is_some());
1597 assert!(filtered.name.is_none());
1598
1599 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 #[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 #[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 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 #[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 #[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 #[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 #[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 }
1986
1987 #[test]
1988 fn provider_set_hmac_key() {
1989 let provider = create_test_provider();
1990 provider.set_hmac_key(b"my-secret");
1991 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 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 provider.cleanup_expired();
2052 }
2053
2054 #[test]
2055 fn provider_issue_id_token_no_claims_provider() {
2056 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 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 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 let parts: Vec<&str> = id_token.raw.split('.').collect();
2114 assert_eq!(parts.len(), 3);
2115 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 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 #[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 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 #[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 #[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 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 assert_ne!(a, b);
2238 }
2239
2240 #[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 #[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 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 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 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 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 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}