1use crate::auth::{error::AuthError, state::Identity};
2
3use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7pub mod cert_binding;
8pub mod dpop;
9pub mod sd_jwt;
10
11#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
22#[serde(untagged)]
23pub enum Audience {
24 Single(String),
25 Multiple(Vec<String>),
26}
27
28impl Audience {
29 pub fn contains(&self, value: &str) -> bool {
33 match self {
34 Audience::Single(s) => s == value,
35 Audience::Multiple(values) => values.iter().any(|s| s == value),
36 }
37 }
38}
39
40impl std::fmt::Display for Audience {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 match self {
52 Audience::Single(s) => f.write_str(s),
53 Audience::Multiple(values) => {
54 for (i, value) in values.iter().enumerate() {
55 if i > 0 {
56 f.write_str(", ")?;
57 }
58 f.write_str(value)?;
59 }
60 Ok(())
61 }
62 }
63 }
64}
65
66impl PartialEq<str> for Audience {
72 fn eq(&self, other: &str) -> bool {
73 matches!(self, Audience::Single(s) if s == other)
74 }
75}
76
77impl PartialEq<&str> for Audience {
78 fn eq(&self, other: &&str) -> bool {
79 self == *other
80 }
81}
82
83impl PartialEq<String> for Audience {
84 fn eq(&self, other: &String) -> bool {
85 self == other.as_str()
86 }
87}
88
89impl From<String> for Audience {
90 fn from(value: String) -> Self {
91 Audience::Single(value)
92 }
93}
94
95impl From<&str> for Audience {
96 fn from(value: &str) -> Self {
97 Audience::Single(value.to_string())
98 }
99}
100
101fn take_jti(extra: &mut HashMap<String, serde_json::Value>) -> String {
118 match extra.remove("jti") {
119 Some(serde_json::Value::String(jti)) => jti,
120 _ => uuid::Uuid::new_v4().to_string(),
121 }
122}
123
124pub(crate) const NAMED_CLAIM_FIELDS: &[&str] = &[
142 "iss", "sub", "aud", "exp", "iat", "nbf", "scope", "identity",
143];
144
145fn reject_named_claim_collisions(
169 extra: &HashMap<String, serde_json::Value>,
170) -> Result<(), AuthError> {
171 let collisions: Vec<&str> = NAMED_CLAIM_FIELDS
172 .iter()
173 .copied()
174 .filter(|name| extra.contains_key(*name))
175 .collect();
176
177 if collisions.is_empty() {
178 return Ok(());
179 }
180
181 let joined = collisions.join(", ");
182 tracing::warn!(
183 colliding_keys = %joined,
184 "rejecting token issuance: `extra` keys collide with named Claims fields"
185 );
186 Err(AuthError::Token(format!(
187 "extra claim key(s) [{joined}] collide with named claims and would be serialized \
188 twice, producing a token that cannot be decoded again; remove them from `extra` \
189 (`jti` is the one supported override)"
190 )))
191}
192
193#[derive(Debug, Serialize, Deserialize, Clone)]
194#[non_exhaustive]
195pub struct Claims {
196 pub iss: Option<String>,
198 pub sub: String,
199 pub aud: Option<Audience>,
200 pub exp: usize,
201 pub iat: usize,
202 pub nbf: Option<usize>,
203 pub jti: Option<String>,
204
205 pub scope: Option<String>,
207 #[serde(skip_serializing_if = "Option::is_none")]
210 pub identity: Option<Identity>,
211
212 #[serde(flatten)]
214 pub extra: HashMap<String, serde_json::Value>,
215}
216
217#[derive(Clone)]
218#[non_exhaustive]
219pub struct TokenManager {
220 encoding_key: EncodingKey,
221 decoding_key: DecodingKey,
222 issuer: Option<String>,
223 kid: Option<String>,
224 alg: Algorithm,
225 public_jwk: Option<crate::token::jwk::Jwk>,
226}
227
228impl TokenManager {
229 pub fn new(secret: &[u8], issuer: Option<String>) -> Self {
231 Self {
232 encoding_key: EncodingKey::from_secret(secret),
233 decoding_key: DecodingKey::from_secret(secret),
234 issuer,
235 kid: None,
236 alg: Algorithm::HS256,
237 public_jwk: None,
238 }
239 }
240
241 pub fn new_asymmetric(
246 private_key_pem: &[u8],
247 issuer: Option<String>,
248 kid: Option<String>,
249 ) -> Result<Self, AuthError> {
250 let encoding_key = EncodingKey::from_rsa_pem(private_key_pem)
251 .map_err(|e| AuthError::Token(e.to_string()))?;
252
253 let pem_str = std::str::from_utf8(private_key_pem)
254 .map_err(|_| AuthError::Token("Invalid PEM UTF-8".into()))?;
255
256 use rsa::pkcs1::DecodeRsaPrivateKey;
257 use rsa::pkcs8::DecodePrivateKey;
258 let rsa_key = rsa::RsaPrivateKey::from_pkcs8_pem(pem_str)
259 .or_else(|_| rsa::RsaPrivateKey::from_pkcs1_pem(pem_str))
260 .map_err(|e| AuthError::Token(format!("Failed to parse RSA key: {}", e)))?;
261
262 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
263 use rsa::traits::PublicKeyParts;
264
265 let n = URL_SAFE_NO_PAD.encode(rsa_key.n().to_bytes_be());
266 let e = URL_SAFE_NO_PAD.encode(rsa_key.e().to_bytes_be());
267
268 let kid_val = kid.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
269
270 let jwk = crate::token::jwk::Jwk {
271 kid: Some(kid_val.clone()),
272 kty: "RSA".to_string(),
273 alg: Some("RS256".to_string()),
274 n: Some(n),
275 e: Some(e),
276 crv: None,
277 x: None,
278 };
279
280 let decoding_key = jwk.to_decoding_key()?;
285
286 Ok(Self {
287 encoding_key,
288 decoding_key,
289 issuer,
290 kid: Some(kid_val),
291 alg: Algorithm::RS256,
292 public_jwk: Some(jwk),
293 })
294 }
295
296 pub fn new_ed25519(
308 private_key_pem: &[u8],
309 issuer: Option<String>,
310 kid: Option<String>,
311 ) -> Result<Self, AuthError> {
312 let encoding_key = EncodingKey::from_ed_pem(private_key_pem)
313 .map_err(|e| AuthError::Token(e.to_string()))?;
314
315 let pem_str = std::str::from_utf8(private_key_pem)
316 .map_err(|_| AuthError::Token("Invalid PEM UTF-8".into()))?;
317
318 use ed25519_dalek::pkcs8::DecodePrivateKey;
319 let signing_key = ed25519_dalek::SigningKey::from_pkcs8_pem(pem_str)
320 .map_err(|e| AuthError::Token(format!("Failed to parse Ed25519 key: {}", e)))?;
321
322 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
323 let x = URL_SAFE_NO_PAD.encode(signing_key.verifying_key().to_bytes());
324
325 let kid_val = kid.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
326
327 let jwk = crate::token::jwk::Jwk {
328 kid: Some(kid_val.clone()),
329 kty: "OKP".to_string(),
330 alg: Some("EdDSA".to_string()),
331 n: None,
332 e: None,
333 crv: Some("Ed25519".to_string()),
334 x: Some(x),
335 };
336
337 let decoding_key = jwk.to_decoding_key()?;
343
344 Ok(Self {
345 encoding_key,
346 decoding_key,
347 issuer,
348 kid: Some(kid_val),
349 alg: Algorithm::EdDSA,
350 public_jwk: Some(jwk),
351 })
352 }
353
354 pub fn public_jwk(&self) -> Option<crate::token::jwk::Jwk> {
355 self.public_jwk.clone()
356 }
357
358 pub fn with_issuer(mut self, issuer: String) -> Self {
359 self.issuer = Some(issuer);
360 self
361 }
362
363 pub fn issue_user_token(
365 &self,
366 identity: Identity,
367 expires_in_secs: u64,
368 scope: Option<String>,
369 aud: Option<String>,
370 ) -> Result<String, AuthError> {
371 self.issue_user_token_with_extra(identity, expires_in_secs, scope, aud, HashMap::new())
372 }
373
374 pub fn issue_user_token_with_extra(
405 &self,
406 identity: Identity,
407 expires_in_secs: u64,
408 scope: Option<String>,
409 aud: Option<String>,
410 mut extra: HashMap<String, serde_json::Value>,
411 ) -> Result<String, AuthError> {
412 let now = chrono::Utc::now().timestamp() as usize;
413 let expiration = now + expires_in_secs as usize;
414 let jti = take_jti(&mut extra);
415 reject_named_claim_collisions(&extra)?;
416
417 let claims = Claims {
418 iss: self.issuer.clone(),
419 sub: identity.external_id.clone(),
420 aud: aud.map(Audience::from),
421 exp: expiration,
422 iat: now,
423 nbf: Some(now),
424 jti: Some(jti),
425 scope,
426 identity: Some(identity),
427 extra,
428 };
429
430 let mut header = Header::new(self.alg);
431 if let Some(ref kid) = self.kid {
432 header.kid = Some(kid.clone());
433 }
434
435 encode(&header, &claims, &self.encoding_key).map_err(|e| AuthError::Token(e.to_string()))
436 }
437
438 pub fn issue_id_token(
440 &self,
441 identity: Identity,
442 client_id: &str,
443 nonce: Option<String>,
444 expires_in_secs: u64,
445 ) -> Result<String, AuthError> {
446 self.issue_id_token_with_extra(identity, client_id, nonce, expires_in_secs, HashMap::new())
447 }
448
449 pub fn issue_id_token_with_extra(
472 &self,
473 identity: Identity,
474 client_id: &str,
475 nonce: Option<String>,
476 expires_in_secs: u64,
477 mut extra: HashMap<String, serde_json::Value>,
478 ) -> Result<String, AuthError> {
479 let now = chrono::Utc::now().timestamp() as usize;
480 let expiration = now + expires_in_secs as usize;
481 let jti = take_jti(&mut extra);
482 reject_named_claim_collisions(&extra)?;
483
484 let mut claims = Claims {
485 iss: self.issuer.clone(),
486 sub: identity.external_id.clone(),
487 aud: Some(Audience::from(client_id)),
488 exp: expiration,
489 iat: now,
490 nbf: Some(now),
491 jti: Some(jti),
492 scope: None,
493 identity: Some(identity),
494 extra,
495 };
496
497 if let Some(n) = nonce {
498 claims
499 .extra
500 .insert("nonce".to_string(), serde_json::Value::String(n));
501 }
502
503 let mut header = Header::new(self.alg);
504 if let Some(ref kid) = self.kid {
505 header.kid = Some(kid.clone());
506 }
507
508 encode(&header, &claims, &self.encoding_key).map_err(|e| AuthError::Token(e.to_string()))
509 }
510
511 pub fn issue_client_token(
513 &self,
514 client_id: &str,
515 expires_in_secs: u64,
516 scope: Option<String>,
517 aud: Option<String>,
518 ) -> Result<String, AuthError> {
519 self.issue_client_token_with_extra(client_id, expires_in_secs, scope, aud, HashMap::new())
520 }
521
522 pub fn issue_client_token_with_extra(
533 &self,
534 client_id: &str,
535 expires_in_secs: u64,
536 scope: Option<String>,
537 aud: Option<String>,
538 mut extra: HashMap<String, serde_json::Value>,
539 ) -> Result<String, AuthError> {
540 let now = chrono::Utc::now().timestamp() as usize;
541 let expiration = now + expires_in_secs as usize;
542 let jti = take_jti(&mut extra);
543 reject_named_claim_collisions(&extra)?;
544
545 let claims = Claims {
546 iss: self.issuer.clone(),
547 sub: client_id.to_string(),
548 aud: aud.map(Audience::from),
549 exp: expiration,
550 iat: now,
551 nbf: Some(now),
552 jti: Some(jti),
553 scope,
554 identity: None,
555 extra,
556 };
557
558 let mut header = Header::new(self.alg);
559 if let Some(ref kid) = self.kid {
560 header.kid = Some(kid.clone());
561 }
562
563 encode(&header, &claims, &self.encoding_key).map_err(|e| AuthError::Token(e.to_string()))
564 }
565
566 pub fn issue_custom_token(
581 &self,
582 sub: String,
583 expires_in_secs: u64,
584 typ: &str,
585 mut extra: HashMap<String, serde_json::Value>,
586 ) -> Result<String, AuthError> {
587 let now = chrono::Utc::now().timestamp() as usize;
588 let jti = take_jti(&mut extra);
589 reject_named_claim_collisions(&extra)?;
590
591 let claims = Claims {
592 iss: self.issuer.clone(),
593 sub,
594 aud: None,
595 exp: now + expires_in_secs as usize,
596 iat: now,
597 nbf: Some(now),
598 jti: Some(jti),
599 scope: None,
600 identity: None,
601 extra,
602 };
603
604 let mut header = Header::new(self.alg);
605 header.typ = Some(typ.to_string());
606 if let Some(ref kid) = self.kid {
607 header.kid = Some(kid.clone());
608 }
609
610 encode(&header, &claims, &self.encoding_key).map_err(|e| AuthError::Token(e.to_string()))
611 }
612
613 pub fn validate_token(
614 &self,
615 token: &str,
616 expected_aud: Option<&str>,
617 ) -> Result<Claims, AuthError> {
618 let mut validation = Validation::new(self.alg);
619 if let Some(aud) = expected_aud {
620 validation.set_audience(&[aud]);
621 } else {
622 validation.validate_aud = false;
623 }
624 if let Some(ref iss) = self.issuer {
625 validation.set_issuer(&[iss]);
626 }
627
628 let token_data = decode::<Claims>(token, &self.decoding_key, &validation)
629 .map_err(|e| AuthError::Token(e.to_string()))?;
630
631 Ok(token_data.claims)
632 }
633}
634
635#[cfg(test)]
636mod tests {
637
638 use super::*;
639 use crate::auth::state::Identity;
640 use std::collections::HashMap;
641
642 const TEST_RSA_PRIVATE_KEY_PEM: &[u8] = b"-----BEGIN PRIVATE KEY-----
644MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDA5hJIcQ+2rxMz
645VM8ZH5WAmguCr0xmNDAdy0IzzsUeFLG7BebB7izOkU36J4t8t5tUaQwrBMnx2Fvt
646VqJjbdE242UDpvWF/8m9zJ2HR5298cbwT5cGMKLB0HWzDMahugs+Bbh2lCgwyLZk
647Tr3Diwxp5SwFew/Wb+Ke9cNG9Hu5IFH3BCuJ839d9hfqisIeYrBPfb52xxckM37R
6487zSGu/eDP/HZAeLkQuptZJW4A3u7xni14u4qyqXDqsHsYFNgJaxMSAwWgBRY6HNu
649TnvBArTXCiVfL+F73B2L6mdYr64g+QS9nK9v97MlJu/E3mSduz54pren4mpCHc9m
650/S2+VjCZAgMBAAECggEAASC9qQbGnL7XuExRDOIn/m4bWx92ehjo0lCTibhpY3LW
651umbSbpfbhmmuSj3CjW9VZsaM3hBTgSjoTX72lbY/eIUXD7c0memUK5pV4XcEIrQw
652AZlPIye6ckx4I7ZGnKasO8FoAel9dd7DXw36AuBK3LBzJwtzkEFsBc0e3/wixqmG
653UJBbbt/+5ya7CxyjuePaQhKtkLD5R6DpvN2XnCYq5nHJNJdvSVg1pOzsTHYIf+Ee
6542Rz42fGsfFKqeEQCcBFRZaGb/ELeP4c6UZdktZAvmHb1p1fursVZc6X9JXmiJ2OJ
655Kv2H2tMKuysP8L0fXFOMgkH2SVt6rcdHkO6xhlhWsQKBgQDqR8rAJeEE5BFoXA8T
656VVW6CLMlW51x4ey7PEGOaYh39dTG2Q+GZQBZ9G+SZk3f5Y85UCACSyc//4qaz/c3
6570nWsegZ+JPyymmuc79wzIAFFvXB7pL6wyn0Ed1P620kOZTtA8iBcXrsuxL+KP7iu
658MXfWmU1QiZpbndILtyDnY+70uwKBgQDSyCljWkydQCaPU+fiAXLxP8CvcJTSSNQD
659mVUlwJ+OpHnU+Alsi1rBavMgUtLlYbFqzH7NmYrLC8Yadq3ZOwLt0VEK0r8qstAL
6607QCDUD2WNuQjpZupRnXuMUl3iXB96i2gb+VQKGuUAJvVWjdIbYa4+Gu+sBMfcDcX
661dBihDLuEuwKBgAgX4tEwfc2Fc3R/eaXZVNTQaB/qQk4k1+C//CPHUYeTXn5gEUE7
662S//PiesszZPmgkQgmHp7zidP1KH0fT3Yb2g97ut8q54f54fMYXcCrAiUusYKsuu4
663kwkMdkI8QRHWPW3I74VBYIYFFfjYqrCZ1OH8+cbGeiagFRmCggh8U0zxAoGAVW3u
6646Ge22Z0gg8LcHsu7jG/sZq7Ygool8/d3fT+e669Z+ak2GJo6hF4WgClRdMqtn72W
665PzpV+ImjFyK2v26dd0n48MwN0v56N/ss1Av3iiRhPtlmR6tZLNspDZvUzhPVvkrb
666xCs9vtSoVEamVWKe0eVNthGjDoDqs0TInq2MavUCgYB6REavSJs/CLkSS7iimjxZ
667G7g5YQi9/p1lXLOEUDiwEmvRr0XTwzzxUsIc535IXhh/ZUYpthenW+qBBzn85pEC
668TowIqciHu5redqlQ8rITA8/AOY98vaDIhppDg1rfpnHHaZHFbXD/keYAEbhBtbvf
669a0QMqKUcs8+YTy5R5K6qtw==
670-----END PRIVATE KEY-----";
671
672 const TEST_ED25519_PRIVATE_KEY_PEM: &[u8] = b"-----BEGIN PRIVATE KEY-----
675MC4CAQAwBQYDK2VwBCIEIKIPR2jojpdobYr1M/pjIRuMONpZGYQ+y5yxSqKX9T9/
676-----END PRIVATE KEY-----";
677
678 const TEST_ED25519_PRIVATE_KEY_PEM_B: &[u8] = b"-----BEGIN PRIVATE KEY-----
681MC4CAQAwBQYDK2VwBCIEIPlsnSfvh53rJ+Tlbo8e7cgq2mIkWQ1NCM5paVeinUh8
682-----END PRIVATE KEY-----";
683
684 #[test]
685 fn test_claims_serialization() {
686 let mut extra = HashMap::new();
687 extra.insert(
688 "custom".to_string(),
689 serde_json::Value::String("value".to_string()),
690 );
691
692 let claims = Claims {
693 iss: Some("issuer".to_string()),
694 sub: "user123".to_string(),
695 aud: Some(Audience::from("audience")),
696 exp: 1000,
697 iat: 500,
698 nbf: Some(500),
699 jti: Some("jti".to_string()),
700 scope: Some("openid profile".to_string()),
701 identity: Some(Identity {
702 provider_id: "google".to_string(),
703 external_id: "user123".to_string(),
704 email: Some("user@example.com".to_string()),
705 username: Some("user".to_string()),
706 attributes: HashMap::new(),
707 }),
708 extra,
709 };
710
711 let serialized = serde_json::to_string(&claims).unwrap();
712 let deserialized: Claims = serde_json::from_str(&serialized).unwrap();
713
714 assert_eq!(deserialized.iss, claims.iss);
715 assert_eq!(deserialized.sub, claims.sub);
716 assert_eq!(deserialized.extra.get("custom").unwrap(), "value");
717 }
718
719 #[test]
720 fn test_token_manager_issuance() {
721 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
722 let identity = Identity {
723 provider_id: "mock".to_string(),
724 external_id: "user123".to_string(),
725 email: None,
726 username: None,
727 attributes: HashMap::new(),
728 };
729
730 let token = manager
731 .issue_user_token(identity, 3600, None, None)
732 .unwrap();
733 let claims = manager.validate_token(&token, None).unwrap();
734
735 assert_eq!(claims.iss, Some("issuer".to_string()));
736 assert_eq!(claims.sub, "user123");
737 assert!(claims.jti.is_some());
738 assert!(claims.nbf.is_some());
739 }
740
741 #[test]
742 fn test_token_manager_asymmetric_issuance() {
743 let manager = TokenManager::new_asymmetric(
744 TEST_RSA_PRIVATE_KEY_PEM,
745 Some("issuer".to_string()),
746 Some("my-kid-123".to_string()),
747 )
748 .unwrap();
749
750 let identity = Identity {
751 provider_id: "mock".to_string(),
752 external_id: "user123".to_string(),
753 email: None,
754 username: None,
755 attributes: HashMap::new(),
756 };
757
758 let token = manager
759 .issue_user_token(identity, 3600, None, None)
760 .unwrap();
761
762 let jwk = manager.public_jwk().unwrap();
764 assert_eq!(jwk.kid.as_deref(), Some("my-kid-123"));
765
766 let decoding_key = jwk.to_decoding_key().unwrap();
767 let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::RS256);
768 validation.set_issuer(&["issuer"]);
769
770 let token_data =
771 jsonwebtoken::decode::<Claims>(&token, &decoding_key, &validation).unwrap();
772 assert_eq!(token_data.claims.sub, "user123");
773 assert_eq!(token_data.header.kid.as_deref(), Some("my-kid-123"));
774 }
775
776 #[test]
785 fn test_asymmetric_manager_validates_its_own_tokens() {
786 let manager = TokenManager::new_asymmetric(
787 TEST_RSA_PRIVATE_KEY_PEM,
788 Some("issuer".to_string()),
789 Some("my-kid-123".to_string()),
790 )
791 .unwrap();
792
793 let identity = Identity {
794 provider_id: "mock".to_string(),
795 external_id: "user123".to_string(),
796 email: None,
797 username: None,
798 attributes: HashMap::new(),
799 };
800
801 let token = manager
802 .issue_user_token(identity, 3600, None, None)
803 .unwrap();
804
805 let claims = manager.validate_token(&token, None).unwrap();
806 assert_eq!(claims.sub, "user123");
807 assert_eq!(claims.iss, Some("issuer".to_string()));
808
809 let attestation = manager
811 .issue_custom_token(
812 "device-1".to_string(),
813 60,
814 "webank-attest+jws",
815 HashMap::new(),
816 )
817 .unwrap();
818
819 let attest_claims = manager.validate_token(&attestation, None).unwrap();
820 assert_eq!(attest_claims.sub, "device-1");
821 }
822
823 #[test]
824 fn test_issue_id_token() {
825 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
826 let identity = Identity {
827 provider_id: "mock".to_string(),
828 external_id: "user123".to_string(),
829 email: None,
830 username: None,
831 attributes: HashMap::new(),
832 };
833
834 let token = manager
835 .issue_id_token(identity, "client-1", Some("nonce123".to_string()), 3600)
836 .unwrap();
837
838 let claims = manager.validate_token(&token, None).unwrap();
839
840 assert_eq!(claims.iss, Some("issuer".to_string()));
841 assert_eq!(claims.sub, "user123");
842 assert_eq!(claims.aud, Some(Audience::from("client-1")));
843 assert_eq!(claims.extra.get("nonce").unwrap(), "nonce123");
844 }
845 #[test]
846 fn test_token_manager_audience_validation() {
847 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
848 let identity = Identity {
849 provider_id: "mock".to_string(),
850 external_id: "user123".to_string(),
851 email: None,
852 username: None,
853 attributes: HashMap::new(),
854 };
855
856 let token = manager
858 .issue_id_token(identity, "client-1", None, 3600)
859 .unwrap();
860
861 let claims = manager.validate_token(&token, Some("client-1")).unwrap();
863 assert_eq!(claims.aud, Some(Audience::from("client-1")));
864
865 let err = manager
867 .validate_token(&token, Some("client-2"))
868 .unwrap_err();
869 assert!(err.to_string().contains("InvalidAudience"));
870 }
871
872 #[test]
873 fn test_issue_custom_token_sets_typ_and_no_aud() {
874 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
875
876 let mut extra = HashMap::new();
877 extra.insert("cnf".to_string(), serde_json::json!({"jkt": "abc"}));
878
879 let token = manager
880 .issue_custom_token("device-1".to_string(), 60, "webank-attest+jws", extra)
881 .unwrap();
882
883 let header = jsonwebtoken::decode_header(&token).unwrap();
884 assert_eq!(header.typ.as_deref(), Some("webank-attest+jws"));
885
886 let claims = manager.validate_token(&token, None).unwrap();
887 assert_eq!(claims.sub, "device-1");
888 assert_eq!(claims.aud, None);
889 assert_eq!(claims.extra.get("cnf").unwrap()["jkt"], "abc");
890 }
891
892 #[test]
896 fn test_issue_custom_token_jti_override_round_trips() {
897 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
898
899 let mut extra = HashMap::new();
900 extra.insert(
901 "jti".to_string(),
902 serde_json::json!("caller-supplied-cuid2"),
903 );
904
905 let token = manager
906 .issue_custom_token("device-1".to_string(), 60, "webank-attest+jws", extra)
907 .unwrap();
908 let claims = manager
909 .validate_token(&token, None)
910 .expect("TokenManager must be able to decode the token it just issued");
911
912 assert_eq!(claims.jti, Some("caller-supplied-cuid2".to_string()));
913 assert!(!claims.extra.contains_key("jti"));
914 }
915
916 #[test]
917 fn test_issue_user_token_with_extra_round_trips_custom_claims() {
918 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
919 let identity = Identity {
920 provider_id: "mock".to_string(),
921 external_id: "user123".to_string(),
922 email: None,
923 username: None,
924 attributes: HashMap::new(),
925 };
926
927 let mut extra = HashMap::new();
928 extra.insert("api_key_id".to_string(), serde_json::json!("key-abc"));
929 extra.insert("project_id".to_string(), serde_json::json!("proj-42"));
930
931 let token = manager
932 .issue_user_token_with_extra(identity, 3600, None, None, extra)
933 .unwrap();
934 let claims = manager.validate_token(&token, None).unwrap();
935
936 assert_eq!(
937 claims.extra.get("api_key_id"),
938 Some(&serde_json::json!("key-abc"))
939 );
940 assert_eq!(
941 claims.extra.get("project_id"),
942 Some(&serde_json::json!("proj-42"))
943 );
944 }
945
946 #[test]
947 fn test_issue_user_token_wrapper_still_has_empty_extra() {
948 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
949 let identity = Identity {
950 provider_id: "mock".to_string(),
951 external_id: "user123".to_string(),
952 email: None,
953 username: None,
954 attributes: HashMap::new(),
955 };
956
957 let token = manager
958 .issue_user_token(identity, 3600, None, None)
959 .unwrap();
960 let claims = manager.validate_token(&token, None).unwrap();
961
962 assert!(claims.extra.is_empty());
963 }
964
965 #[test]
973 fn test_issue_user_token_with_extra_jti_override_no_duplicate_key() {
974 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
975 let identity = Identity {
976 provider_id: "mock".to_string(),
977 external_id: "user123".to_string(),
978 email: None,
979 username: None,
980 attributes: HashMap::new(),
981 };
982
983 let mut extra = HashMap::new();
984 extra.insert(
985 "jti".to_string(),
986 serde_json::json!("caller-supplied-cuid2"),
987 );
988
989 let token = manager
990 .issue_user_token_with_extra(identity, 3600, None, None, extra)
991 .unwrap();
992
993 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
994 let payload_b64 = token.split('.').nth(1).unwrap();
995 let payload_json = String::from_utf8(URL_SAFE_NO_PAD.decode(payload_b64).unwrap()).unwrap();
996 assert_eq!(
997 payload_json.matches("\"jti\"").count(),
998 1,
999 "wire payload must contain exactly one jti key, got: {payload_json}"
1000 );
1001
1002 let claims = manager
1003 .validate_token(&token, None)
1004 .expect("TokenManager must be able to decode the token it just issued");
1005 assert_eq!(claims.jti, Some("caller-supplied-cuid2".to_string()));
1006 }
1007
1008 #[test]
1011 fn test_issue_user_token_with_extra_default_jti_is_still_generated_uuid() {
1012 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
1013 let identity = Identity {
1014 provider_id: "mock".to_string(),
1015 external_id: "user123".to_string(),
1016 email: None,
1017 username: None,
1018 attributes: HashMap::new(),
1019 };
1020
1021 let token = manager
1022 .issue_user_token_with_extra(identity, 3600, None, None, HashMap::new())
1023 .unwrap();
1024 let claims = manager.validate_token(&token, None).unwrap();
1025
1026 let jti = claims.jti.expect("jti must still be generated by default");
1027 assert!(
1028 uuid::Uuid::parse_str(&jti).is_ok(),
1029 "default-path jti must still be a UUID, got: {jti}"
1030 );
1031 }
1032
1033 #[test]
1034 fn test_issue_client_token_with_extra_round_trips_custom_claims() {
1035 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
1036
1037 let mut extra = HashMap::new();
1038 extra.insert("roles".to_string(), serde_json::json!(["admin", "billing"]));
1039
1040 let token = manager
1041 .issue_client_token_with_extra("client-1", 3600, None, None, extra)
1042 .unwrap();
1043 let claims = manager.validate_token(&token, None).unwrap();
1044
1045 assert_eq!(
1046 claims.extra.get("roles"),
1047 Some(&serde_json::json!(["admin", "billing"]))
1048 );
1049 }
1050
1051 #[test]
1052 fn test_issue_client_token_wrapper_still_has_empty_extra() {
1053 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
1054
1055 let token = manager
1056 .issue_client_token("client-1", 3600, None, None)
1057 .unwrap();
1058 let claims = manager.validate_token(&token, None).unwrap();
1059
1060 assert!(claims.extra.is_empty());
1061 }
1062
1063 #[test]
1067 fn test_issue_client_token_with_extra_jti_override_round_trips() {
1068 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
1069
1070 let mut extra = HashMap::new();
1071 extra.insert(
1072 "jti".to_string(),
1073 serde_json::json!("caller-supplied-cuid2"),
1074 );
1075
1076 let token = manager
1077 .issue_client_token_with_extra("client-1", 3600, None, None, extra)
1078 .unwrap();
1079 let claims = manager
1080 .validate_token(&token, None)
1081 .expect("TokenManager must be able to decode the token it just issued");
1082
1083 assert_eq!(claims.jti, Some("caller-supplied-cuid2".to_string()));
1084 assert!(!claims.extra.contains_key("jti"));
1085 }
1086
1087 #[test]
1088 fn test_issue_id_token_with_extra_round_trips_custom_claims() {
1089 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
1090 let identity = Identity {
1091 provider_id: "mock".to_string(),
1092 external_id: "user123".to_string(),
1093 email: None,
1094 username: None,
1095 attributes: HashMap::new(),
1096 };
1097
1098 let mut extra = HashMap::new();
1099 extra.insert("org_id".to_string(), serde_json::json!("org-7"));
1100
1101 let token = manager
1102 .issue_id_token_with_extra(
1103 identity,
1104 "client-1",
1105 Some("nonce123".to_string()),
1106 3600,
1107 extra,
1108 )
1109 .unwrap();
1110 let claims = manager.validate_token(&token, None).unwrap();
1111
1112 assert_eq!(
1113 claims.extra.get("org_id"),
1114 Some(&serde_json::json!("org-7"))
1115 );
1116 assert_eq!(
1117 claims.extra.get("nonce"),
1118 Some(&serde_json::json!("nonce123"))
1119 );
1120 }
1121
1122 #[test]
1123 fn test_issue_id_token_with_extra_explicit_nonce_wins_over_extra_nonce() {
1124 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
1128 let identity = Identity {
1129 provider_id: "mock".to_string(),
1130 external_id: "user123".to_string(),
1131 email: None,
1132 username: None,
1133 attributes: HashMap::new(),
1134 };
1135
1136 let mut extra = HashMap::new();
1137 extra.insert(
1138 "nonce".to_string(),
1139 serde_json::json!("attacker-supplied-nonce"),
1140 );
1141
1142 let token = manager
1143 .issue_id_token_with_extra(
1144 identity,
1145 "client-1",
1146 Some("real-nonce".to_string()),
1147 3600,
1148 extra,
1149 )
1150 .unwrap();
1151 let claims = manager.validate_token(&token, None).unwrap();
1152
1153 assert_eq!(
1154 claims.extra.get("nonce"),
1155 Some(&serde_json::json!("real-nonce"))
1156 );
1157 }
1158
1159 #[test]
1163 fn test_issue_id_token_with_extra_jti_override_round_trips() {
1164 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
1165 let identity = Identity {
1166 provider_id: "mock".to_string(),
1167 external_id: "user123".to_string(),
1168 email: None,
1169 username: None,
1170 attributes: HashMap::new(),
1171 };
1172
1173 let mut extra = HashMap::new();
1174 extra.insert(
1175 "jti".to_string(),
1176 serde_json::json!("caller-supplied-cuid2"),
1177 );
1178
1179 let token = manager
1180 .issue_id_token_with_extra(identity, "client-1", None, 3600, extra)
1181 .unwrap();
1182 let claims = manager
1183 .validate_token(&token, None)
1184 .expect("TokenManager must be able to decode the token it just issued");
1185
1186 assert_eq!(claims.jti, Some("caller-supplied-cuid2".to_string()));
1187 assert!(!claims.extra.contains_key("jti"));
1188 }
1189
1190 #[test]
1191 fn test_token_manager_ed25519_issuance() {
1192 let manager = TokenManager::new_ed25519(
1193 TEST_ED25519_PRIVATE_KEY_PEM,
1194 Some("issuer".to_string()),
1195 Some("my-ed25519-kid".to_string()),
1196 )
1197 .unwrap();
1198
1199 let identity = Identity {
1200 provider_id: "mock".to_string(),
1201 external_id: "user123".to_string(),
1202 email: None,
1203 username: None,
1204 attributes: HashMap::new(),
1205 };
1206
1207 let token = manager
1208 .issue_user_token(identity, 3600, None, None)
1209 .unwrap();
1210
1211 let header = jsonwebtoken::decode_header(&token).unwrap();
1212 assert_eq!(header.alg, Algorithm::EdDSA);
1213 assert_eq!(header.kid.as_deref(), Some("my-ed25519-kid"));
1214 }
1215
1216 #[test]
1223 fn test_ed25519_token_round_trips_via_published_jwk() {
1224 let manager = TokenManager::new_ed25519(
1225 TEST_ED25519_PRIVATE_KEY_PEM,
1226 Some("issuer".to_string()),
1227 Some("my-ed25519-kid".to_string()),
1228 )
1229 .unwrap();
1230
1231 let identity = Identity {
1232 provider_id: "mock".to_string(),
1233 external_id: "user123".to_string(),
1234 email: None,
1235 username: None,
1236 attributes: HashMap::new(),
1237 };
1238
1239 let token = manager
1240 .issue_user_token(identity, 3600, None, None)
1241 .unwrap();
1242
1243 let jwk = manager.public_jwk().unwrap();
1244 assert_eq!(jwk.kid.as_deref(), Some("my-ed25519-kid"));
1245
1246 let decoding_key = jwk.to_decoding_key().unwrap();
1247 let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::EdDSA);
1248 validation.set_issuer(&["issuer"]);
1249
1250 let token_data =
1251 jsonwebtoken::decode::<Claims>(&token, &decoding_key, &validation).unwrap();
1252 assert_eq!(token_data.claims.sub, "user123");
1253 assert_eq!(token_data.header.kid.as_deref(), Some("my-ed25519-kid"));
1254 }
1255
1256 #[test]
1260 fn test_ed25519_manager_validates_its_own_tokens() {
1261 let manager = TokenManager::new_ed25519(
1262 TEST_ED25519_PRIVATE_KEY_PEM,
1263 Some("issuer".to_string()),
1264 Some("my-ed25519-kid".to_string()),
1265 )
1266 .unwrap();
1267
1268 let identity = Identity {
1269 provider_id: "mock".to_string(),
1270 external_id: "user123".to_string(),
1271 email: None,
1272 username: None,
1273 attributes: HashMap::new(),
1274 };
1275
1276 let token = manager
1277 .issue_user_token(identity, 3600, None, None)
1278 .unwrap();
1279
1280 let claims = manager.validate_token(&token, None).unwrap();
1281 assert_eq!(claims.sub, "user123");
1282 assert_eq!(claims.iss, Some("issuer".to_string()));
1283 }
1284
1285 #[test]
1290 fn test_ed25519_public_jwk_is_spec_correct_okp_json() {
1291 let manager = TokenManager::new_ed25519(
1292 TEST_ED25519_PRIVATE_KEY_PEM,
1293 Some("issuer".to_string()),
1294 Some("my-ed25519-kid".to_string()),
1295 )
1296 .unwrap();
1297
1298 let jwk = manager.public_jwk().unwrap();
1299 assert_eq!(jwk.kty, "OKP");
1300 assert_eq!(jwk.alg.as_deref(), Some("EdDSA"));
1301 assert_eq!(jwk.crv.as_deref(), Some("Ed25519"));
1302 assert!(jwk.n.is_none());
1303 assert!(jwk.e.is_none());
1304
1305 let x = jwk.x.as_ref().expect("OKP JWK must have 'x'");
1306 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
1307 let decoded = URL_SAFE_NO_PAD
1308 .decode(x)
1309 .expect("'x' must be valid base64url (unpadded)");
1310 assert_eq!(decoded.len(), 32, "Ed25519 public key must be 32 bytes");
1311
1312 let value = serde_json::to_value(&jwk).unwrap();
1313 let obj = value.as_object().unwrap();
1314 assert_eq!(obj.get("kty").unwrap(), "OKP");
1315 assert_eq!(obj.get("crv").unwrap(), "Ed25519");
1316 assert_eq!(obj.get("alg").unwrap(), "EdDSA");
1317 assert!(obj.contains_key("x"));
1318 assert!(
1319 !obj.contains_key("n"),
1320 "OKP JWK must not serialize the RSA 'n' field, got: {}",
1321 value
1322 );
1323 assert!(
1324 !obj.contains_key("e"),
1325 "OKP JWK must not serialize the RSA 'e' field, got: {}",
1326 value
1327 );
1328 }
1329
1330 #[test]
1333 fn test_rsa_public_jwk_still_omits_okp_fields() {
1334 let manager = TokenManager::new_asymmetric(
1335 TEST_RSA_PRIVATE_KEY_PEM,
1336 Some("issuer".to_string()),
1337 Some("my-kid-123".to_string()),
1338 )
1339 .unwrap();
1340
1341 let jwk = manager.public_jwk().unwrap();
1342 let value = serde_json::to_value(&jwk).unwrap();
1343 let obj = value.as_object().unwrap();
1344 assert_eq!(obj.get("kty").unwrap(), "RSA");
1345 assert!(obj.contains_key("n"));
1346 assert!(obj.contains_key("e"));
1347 assert!(
1348 !obj.contains_key("crv"),
1349 "RSA JWK must not serialize the OKP 'crv' field, got: {}",
1350 value
1351 );
1352 assert!(
1353 !obj.contains_key("x"),
1354 "RSA JWK must not serialize the OKP 'x' field, got: {}",
1355 value
1356 );
1357 }
1358
1359 #[test]
1364 fn test_ed25519_token_rejected_by_wrong_key_jwk() {
1365 let signer = TokenManager::new_ed25519(
1366 TEST_ED25519_PRIVATE_KEY_PEM,
1367 Some("issuer".to_string()),
1368 Some("kid-a".to_string()),
1369 )
1370 .unwrap();
1371 let other = TokenManager::new_ed25519(
1372 TEST_ED25519_PRIVATE_KEY_PEM_B,
1373 Some("issuer".to_string()),
1374 Some("kid-b".to_string()),
1375 )
1376 .unwrap();
1377
1378 let identity = Identity {
1379 provider_id: "mock".to_string(),
1380 external_id: "user123".to_string(),
1381 email: None,
1382 username: None,
1383 attributes: HashMap::new(),
1384 };
1385
1386 let token = signer.issue_user_token(identity, 3600, None, None).unwrap();
1387
1388 assert!(signer.validate_token(&token, None).is_ok());
1390
1391 let err = other.validate_token(&token, None).unwrap_err();
1393 assert!(err.to_string().contains("InvalidSignature"));
1394
1395 let wrong_jwk = other.public_jwk().unwrap();
1398 let decoding_key = wrong_jwk.to_decoding_key().unwrap();
1399 let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::EdDSA);
1400 validation.set_issuer(&["issuer"]);
1401 let err = jsonwebtoken::decode::<Claims>(&token, &decoding_key, &validation).unwrap_err();
1402 assert_eq!(
1403 err.kind(),
1404 &jsonwebtoken::errors::ErrorKind::InvalidSignature
1405 );
1406 }
1407
1408 #[test]
1412 fn test_ed25519_tampered_token_rejected() {
1413 let manager = TokenManager::new_ed25519(
1414 TEST_ED25519_PRIVATE_KEY_PEM,
1415 Some("issuer".to_string()),
1416 Some("my-ed25519-kid".to_string()),
1417 )
1418 .unwrap();
1419
1420 let identity = Identity {
1421 provider_id: "mock".to_string(),
1422 external_id: "user123".to_string(),
1423 email: None,
1424 username: None,
1425 attributes: HashMap::new(),
1426 };
1427
1428 let token = manager
1429 .issue_user_token(identity, 3600, None, None)
1430 .unwrap();
1431
1432 let mut parts: Vec<&str> = token.split('.').collect();
1433 assert_eq!(parts.len(), 3);
1434 let mut payload = parts[1].to_string();
1436 let last = payload.pop().unwrap();
1437 let replacement = if last == 'A' { 'B' } else { 'A' };
1438 payload.push(replacement);
1439 parts[1] = &payload;
1440 let tampered = parts.join(".");
1441
1442 let err = manager.validate_token(&tampered, None).unwrap_err();
1443 assert!(
1444 err.to_string().contains("InvalidSignature") || err.to_string().contains("Json"),
1445 "unexpected error for tampered token: {err}"
1446 );
1447 }
1448
1449 #[test]
1457 fn test_validate_token_accepts_array_aud() {
1458 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
1459
1460 let raw_claims = serde_json::json!({
1461 "iss": "issuer",
1462 "sub": "user123",
1463 "aud": ["client-1", "client-2"],
1464 "exp": (chrono::Utc::now().timestamp() as usize) + 3600,
1465 "iat": chrono::Utc::now().timestamp() as usize,
1466 });
1467 let token = jsonwebtoken::encode(
1468 &jsonwebtoken::Header::new(jsonwebtoken::Algorithm::HS256),
1469 &raw_claims,
1470 &jsonwebtoken::EncodingKey::from_secret(b"secret"),
1471 )
1472 .unwrap();
1473
1474 let claims = manager
1475 .validate_token(&token, None)
1476 .expect("multi-audience subject token must deserialize");
1477 let aud = claims.aud.expect("aud claim must be present");
1478 assert!(aud.contains("client-1"));
1479 assert!(aud.contains("client-2"));
1480 assert!(!aud.contains("client-3"));
1481 }
1482
1483 #[test]
1488 fn test_validate_token_still_accepts_string_aud() {
1489 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
1490 let identity = Identity {
1491 provider_id: "mock".to_string(),
1492 external_id: "user123".to_string(),
1493 email: None,
1494 username: None,
1495 attributes: HashMap::new(),
1496 };
1497
1498 let token = manager
1499 .issue_id_token(identity, "client-1", None, 3600)
1500 .unwrap();
1501
1502 let claims = manager.validate_token(&token, None).unwrap();
1503 let aud = claims.aud.expect("aud claim must be present");
1504 assert!(aud.contains("client-1"));
1505 assert!(!aud.contains("client-2"));
1506 }
1507
1508 #[test]
1513 fn test_audience_single_round_trips_as_bare_string() {
1514 let single = Audience::Single("client-1".to_string());
1515 let serialized = serde_json::to_string(&single).unwrap();
1516 assert_eq!(serialized, "\"client-1\"");
1517
1518 let deserialized: Audience = serde_json::from_str(&serialized).unwrap();
1519 assert_eq!(deserialized, single);
1520
1521 let multiple = Audience::Multiple(vec!["client-1".to_string(), "client-2".to_string()]);
1522 let serialized_multi = serde_json::to_string(&multiple).unwrap();
1523 assert_eq!(serialized_multi, "[\"client-1\",\"client-2\"]");
1524
1525 let deserialized_multi: Audience = serde_json::from_str(&serialized_multi).unwrap();
1526 assert_eq!(deserialized_multi, multiple);
1527 }
1528
1529 fn test_identity() -> Identity {
1530 Identity {
1531 provider_id: "mock".to_string(),
1532 external_id: "user123".to_string(),
1533 email: None,
1534 username: None,
1535 attributes: HashMap::new(),
1536 }
1537 }
1538
1539 #[test]
1547 fn test_issue_custom_token_rejects_extra_colliding_with_named_claims() {
1548 let manager =
1549 TokenManager::new(b"probe-secret", Some("https://issuer.example".to_string()));
1550
1551 let mut extra: HashMap<String, serde_json::Value> = HashMap::new();
1552 extra.insert("iss".into(), serde_json::json!("https://caller.example"));
1553 extra.insert("sub".into(), serde_json::json!("caller-subject"));
1554 extra.insert("aud".into(), serde_json::json!("caller-audience"));
1555
1556 let err = manager
1557 .issue_custom_token("engine-subject".to_string(), 3600, "JWT", extra)
1558 .expect_err("extra colliding with named claims must not mint a token");
1559
1560 let msg = err.to_string();
1561 for key in ["iss", "sub", "aud"] {
1562 assert!(
1563 msg.contains(key),
1564 "error must name every colliding key; `{key}` missing from: {msg}"
1565 );
1566 }
1567 }
1568
1569 #[test]
1576 fn test_every_named_claim_field_is_rejected_on_every_issuance_path() {
1577 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
1578
1579 for field in NAMED_CLAIM_FIELDS {
1580 let one = |value: serde_json::Value| {
1581 let mut extra = HashMap::new();
1582 extra.insert((*field).to_string(), value);
1583 extra
1584 };
1585 let probe = serde_json::json!("collides");
1586
1587 assert!(
1588 manager
1589 .issue_user_token_with_extra(
1590 test_identity(),
1591 3600,
1592 None,
1593 None,
1594 one(probe.clone())
1595 )
1596 .is_err(),
1597 "issue_user_token_with_extra must reject extra[{field}]"
1598 );
1599 assert!(
1600 manager
1601 .issue_id_token_with_extra(
1602 test_identity(),
1603 "client-1",
1604 None,
1605 3600,
1606 one(probe.clone())
1607 )
1608 .is_err(),
1609 "issue_id_token_with_extra must reject extra[{field}]"
1610 );
1611 assert!(
1612 manager
1613 .issue_client_token_with_extra("client-1", 3600, None, None, one(probe.clone()))
1614 .is_err(),
1615 "issue_client_token_with_extra must reject extra[{field}]"
1616 );
1617 assert!(
1618 manager
1619 .issue_custom_token("sub".to_string(), 3600, "JWT", one(probe.clone()))
1620 .is_err(),
1621 "issue_custom_token must reject extra[{field}]"
1622 );
1623 assert!(
1624 manager
1625 .issue_sd_jwt(
1626 "sub".to_string(),
1627 3600,
1628 None,
1629 None,
1630 Vec::new(),
1631 one(probe.clone())
1632 )
1633 .is_err(),
1634 "issue_sd_jwt must reject extra[{field}]"
1635 );
1636 }
1637 }
1638
1639 #[test]
1647 fn test_named_claim_fields_covers_every_serialized_claims_key() {
1648 let claims = Claims {
1649 iss: Some("issuer".to_string()),
1650 sub: "sub".to_string(),
1651 aud: Some(Audience::from("client-1")),
1652 exp: 0,
1653 iat: 0,
1654 nbf: Some(0),
1655 jti: Some("jti".to_string()),
1656 scope: Some("openid".to_string()),
1657 identity: Some(test_identity()),
1660 extra: HashMap::new(),
1661 };
1662
1663 let serialized = serde_json::to_value(&claims).unwrap();
1664 let keys: Vec<&str> = serialized
1665 .as_object()
1666 .expect("Claims serializes as a JSON object")
1667 .keys()
1668 .map(String::as_str)
1669 .collect();
1670
1671 for key in keys {
1672 assert!(
1673 key == "jti" || NAMED_CLAIM_FIELDS.contains(&key),
1674 "`Claims` serializes `{key}` but NAMED_CLAIM_FIELDS does not list it, so \
1675 `extra[\"{key}\"]` would silently produce a duplicate key again (#283)"
1676 );
1677 }
1678 }
1679
1680 #[test]
1684 fn test_non_colliding_extra_and_jti_override_still_work() {
1685 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
1686
1687 let mut extra = HashMap::new();
1688 extra.insert("api_key_id".to_string(), serde_json::json!("key-abc"));
1689 extra.insert("cnf".to_string(), serde_json::json!({"jkt": "abc"}));
1690 extra.insert(
1691 "jti".to_string(),
1692 serde_json::json!("caller-supplied-cuid2"),
1693 );
1694
1695 let token = manager
1696 .issue_user_token_with_extra(test_identity(), 3600, None, None, extra)
1697 .expect("non-colliding extra plus a jti override must still issue");
1698
1699 let claims = manager
1700 .validate_token(&token, None)
1701 .expect("TokenManager must be able to decode the token it just issued");
1702
1703 assert_eq!(claims.jti, Some("caller-supplied-cuid2".to_string()));
1704 assert_eq!(
1705 claims.extra.get("api_key_id"),
1706 Some(&serde_json::json!("key-abc"))
1707 );
1708 assert_eq!(claims.extra.get("cnf").unwrap()["jkt"], "abc");
1709 assert!(!claims.extra.contains_key("jti"));
1710 }
1711
1712 #[test]
1717 fn test_id_token_nonce_still_stamped_after_collision_check() {
1718 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
1719
1720 let token = manager
1721 .issue_id_token_with_extra(
1722 test_identity(),
1723 "client-1",
1724 Some("nonce123".to_string()),
1725 3600,
1726 HashMap::new(),
1727 )
1728 .expect("id token issuance must be unaffected");
1729
1730 let claims = manager.validate_token(&token, Some("client-1")).unwrap();
1731 assert_eq!(claims.extra.get("nonce").unwrap(), "nonce123");
1732 }
1733}
1734pub mod jwk;