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 sd_jwt;
9
10#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
21#[serde(untagged)]
22pub enum Audience {
23 Single(String),
24 Multiple(Vec<String>),
25}
26
27impl Audience {
28 pub fn contains(&self, value: &str) -> bool {
32 match self {
33 Audience::Single(s) => s == value,
34 Audience::Multiple(values) => values.iter().any(|s| s == value),
35 }
36 }
37}
38
39impl std::fmt::Display for Audience {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 match self {
51 Audience::Single(s) => f.write_str(s),
52 Audience::Multiple(values) => {
53 for (i, value) in values.iter().enumerate() {
54 if i > 0 {
55 f.write_str(", ")?;
56 }
57 f.write_str(value)?;
58 }
59 Ok(())
60 }
61 }
62 }
63}
64
65impl PartialEq<str> for Audience {
71 fn eq(&self, other: &str) -> bool {
72 matches!(self, Audience::Single(s) if s == other)
73 }
74}
75
76impl PartialEq<&str> for Audience {
77 fn eq(&self, other: &&str) -> bool {
78 self == *other
79 }
80}
81
82impl PartialEq<String> for Audience {
83 fn eq(&self, other: &String) -> bool {
84 self == other.as_str()
85 }
86}
87
88impl From<String> for Audience {
89 fn from(value: String) -> Self {
90 Audience::Single(value)
91 }
92}
93
94impl From<&str> for Audience {
95 fn from(value: &str) -> Self {
96 Audience::Single(value.to_string())
97 }
98}
99
100fn take_jti(extra: &mut HashMap<String, serde_json::Value>) -> String {
117 match extra.remove("jti") {
118 Some(serde_json::Value::String(jti)) => jti,
119 _ => uuid::Uuid::new_v4().to_string(),
120 }
121}
122
123#[derive(Debug, Serialize, Deserialize, Clone)]
124#[non_exhaustive]
125pub struct Claims {
126 pub iss: Option<String>,
128 pub sub: String,
129 pub aud: Option<Audience>,
130 pub exp: usize,
131 pub iat: usize,
132 pub nbf: Option<usize>,
133 pub jti: Option<String>,
134
135 pub scope: Option<String>,
137 #[serde(skip_serializing_if = "Option::is_none")]
140 pub identity: Option<Identity>,
141
142 #[serde(flatten)]
144 pub extra: HashMap<String, serde_json::Value>,
145}
146
147#[derive(Clone)]
148#[non_exhaustive]
149pub struct TokenManager {
150 encoding_key: EncodingKey,
151 decoding_key: DecodingKey,
152 issuer: Option<String>,
153 kid: Option<String>,
154 alg: Algorithm,
155 public_jwk: Option<crate::token::jwk::Jwk>,
156}
157
158impl TokenManager {
159 pub fn new(secret: &[u8], issuer: Option<String>) -> Self {
161 Self {
162 encoding_key: EncodingKey::from_secret(secret),
163 decoding_key: DecodingKey::from_secret(secret),
164 issuer,
165 kid: None,
166 alg: Algorithm::HS256,
167 public_jwk: None,
168 }
169 }
170
171 pub fn new_asymmetric(
176 private_key_pem: &[u8],
177 issuer: Option<String>,
178 kid: Option<String>,
179 ) -> Result<Self, AuthError> {
180 let encoding_key = EncodingKey::from_rsa_pem(private_key_pem)
181 .map_err(|e| AuthError::Token(e.to_string()))?;
182
183 let pem_str = std::str::from_utf8(private_key_pem)
184 .map_err(|_| AuthError::Token("Invalid PEM UTF-8".into()))?;
185
186 use rsa::pkcs1::DecodeRsaPrivateKey;
187 use rsa::pkcs8::DecodePrivateKey;
188 let rsa_key = rsa::RsaPrivateKey::from_pkcs8_pem(pem_str)
189 .or_else(|_| rsa::RsaPrivateKey::from_pkcs1_pem(pem_str))
190 .map_err(|e| AuthError::Token(format!("Failed to parse RSA key: {}", e)))?;
191
192 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
193 use rsa::traits::PublicKeyParts;
194
195 let n = URL_SAFE_NO_PAD.encode(rsa_key.n().to_bytes_be());
196 let e = URL_SAFE_NO_PAD.encode(rsa_key.e().to_bytes_be());
197
198 let kid_val = kid.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
199
200 let jwk = crate::token::jwk::Jwk {
201 kid: Some(kid_val.clone()),
202 kty: "RSA".to_string(),
203 alg: Some("RS256".to_string()),
204 n: Some(n),
205 e: Some(e),
206 crv: None,
207 x: None,
208 };
209
210 let decoding_key = jwk.to_decoding_key()?;
215
216 Ok(Self {
217 encoding_key,
218 decoding_key,
219 issuer,
220 kid: Some(kid_val),
221 alg: Algorithm::RS256,
222 public_jwk: Some(jwk),
223 })
224 }
225
226 pub fn new_ed25519(
238 private_key_pem: &[u8],
239 issuer: Option<String>,
240 kid: Option<String>,
241 ) -> Result<Self, AuthError> {
242 let encoding_key = EncodingKey::from_ed_pem(private_key_pem)
243 .map_err(|e| AuthError::Token(e.to_string()))?;
244
245 let pem_str = std::str::from_utf8(private_key_pem)
246 .map_err(|_| AuthError::Token("Invalid PEM UTF-8".into()))?;
247
248 use ed25519_dalek::pkcs8::DecodePrivateKey;
249 let signing_key = ed25519_dalek::SigningKey::from_pkcs8_pem(pem_str)
250 .map_err(|e| AuthError::Token(format!("Failed to parse Ed25519 key: {}", e)))?;
251
252 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
253 let x = URL_SAFE_NO_PAD.encode(signing_key.verifying_key().to_bytes());
254
255 let kid_val = kid.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
256
257 let jwk = crate::token::jwk::Jwk {
258 kid: Some(kid_val.clone()),
259 kty: "OKP".to_string(),
260 alg: Some("EdDSA".to_string()),
261 n: None,
262 e: None,
263 crv: Some("Ed25519".to_string()),
264 x: Some(x),
265 };
266
267 let decoding_key = jwk.to_decoding_key()?;
273
274 Ok(Self {
275 encoding_key,
276 decoding_key,
277 issuer,
278 kid: Some(kid_val),
279 alg: Algorithm::EdDSA,
280 public_jwk: Some(jwk),
281 })
282 }
283
284 pub fn public_jwk(&self) -> Option<crate::token::jwk::Jwk> {
285 self.public_jwk.clone()
286 }
287
288 pub fn with_issuer(mut self, issuer: String) -> Self {
289 self.issuer = Some(issuer);
290 self
291 }
292
293 pub fn issue_user_token(
295 &self,
296 identity: Identity,
297 expires_in_secs: u64,
298 scope: Option<String>,
299 aud: Option<String>,
300 ) -> Result<String, AuthError> {
301 self.issue_user_token_with_extra(identity, expires_in_secs, scope, aud, HashMap::new())
302 }
303
304 pub fn issue_user_token_with_extra(
322 &self,
323 identity: Identity,
324 expires_in_secs: u64,
325 scope: Option<String>,
326 aud: Option<String>,
327 mut extra: HashMap<String, serde_json::Value>,
328 ) -> Result<String, AuthError> {
329 let now = chrono::Utc::now().timestamp() as usize;
330 let expiration = now + expires_in_secs as usize;
331 let jti = take_jti(&mut extra);
332
333 let claims = Claims {
334 iss: self.issuer.clone(),
335 sub: identity.external_id.clone(),
336 aud: aud.map(Audience::from),
337 exp: expiration,
338 iat: now,
339 nbf: Some(now),
340 jti: Some(jti),
341 scope,
342 identity: Some(identity),
343 extra,
344 };
345
346 let mut header = Header::new(self.alg);
347 if let Some(ref kid) = self.kid {
348 header.kid = Some(kid.clone());
349 }
350
351 encode(&header, &claims, &self.encoding_key).map_err(|e| AuthError::Token(e.to_string()))
352 }
353
354 pub fn issue_id_token(
356 &self,
357 identity: Identity,
358 client_id: &str,
359 nonce: Option<String>,
360 expires_in_secs: u64,
361 ) -> Result<String, AuthError> {
362 self.issue_id_token_with_extra(identity, client_id, nonce, expires_in_secs, HashMap::new())
363 }
364
365 pub fn issue_id_token_with_extra(
381 &self,
382 identity: Identity,
383 client_id: &str,
384 nonce: Option<String>,
385 expires_in_secs: u64,
386 mut extra: HashMap<String, serde_json::Value>,
387 ) -> Result<String, AuthError> {
388 let now = chrono::Utc::now().timestamp() as usize;
389 let expiration = now + expires_in_secs as usize;
390 let jti = take_jti(&mut extra);
391
392 let mut claims = Claims {
393 iss: self.issuer.clone(),
394 sub: identity.external_id.clone(),
395 aud: Some(Audience::from(client_id)),
396 exp: expiration,
397 iat: now,
398 nbf: Some(now),
399 jti: Some(jti),
400 scope: None,
401 identity: Some(identity),
402 extra,
403 };
404
405 if let Some(n) = nonce {
406 claims
407 .extra
408 .insert("nonce".to_string(), serde_json::Value::String(n));
409 }
410
411 let mut header = Header::new(self.alg);
412 if let Some(ref kid) = self.kid {
413 header.kid = Some(kid.clone());
414 }
415
416 encode(&header, &claims, &self.encoding_key).map_err(|e| AuthError::Token(e.to_string()))
417 }
418
419 pub fn issue_client_token(
421 &self,
422 client_id: &str,
423 expires_in_secs: u64,
424 scope: Option<String>,
425 aud: Option<String>,
426 ) -> Result<String, AuthError> {
427 self.issue_client_token_with_extra(client_id, expires_in_secs, scope, aud, HashMap::new())
428 }
429
430 pub fn issue_client_token_with_extra(
435 &self,
436 client_id: &str,
437 expires_in_secs: u64,
438 scope: Option<String>,
439 aud: Option<String>,
440 mut extra: HashMap<String, serde_json::Value>,
441 ) -> Result<String, AuthError> {
442 let now = chrono::Utc::now().timestamp() as usize;
443 let expiration = now + expires_in_secs as usize;
444 let jti = take_jti(&mut extra);
445
446 let claims = Claims {
447 iss: self.issuer.clone(),
448 sub: client_id.to_string(),
449 aud: aud.map(Audience::from),
450 exp: expiration,
451 iat: now,
452 nbf: Some(now),
453 jti: Some(jti),
454 scope,
455 identity: None,
456 extra,
457 };
458
459 let mut header = Header::new(self.alg);
460 if let Some(ref kid) = self.kid {
461 header.kid = Some(kid.clone());
462 }
463
464 encode(&header, &claims, &self.encoding_key).map_err(|e| AuthError::Token(e.to_string()))
465 }
466
467 pub fn issue_custom_token(
476 &self,
477 sub: String,
478 expires_in_secs: u64,
479 typ: &str,
480 mut extra: HashMap<String, serde_json::Value>,
481 ) -> Result<String, AuthError> {
482 let now = chrono::Utc::now().timestamp() as usize;
483 let jti = take_jti(&mut extra);
484 let claims = Claims {
485 iss: self.issuer.clone(),
486 sub,
487 aud: None,
488 exp: now + expires_in_secs as usize,
489 iat: now,
490 nbf: Some(now),
491 jti: Some(jti),
492 scope: None,
493 identity: None,
494 extra,
495 };
496
497 let mut header = Header::new(self.alg);
498 header.typ = Some(typ.to_string());
499 if let Some(ref kid) = self.kid {
500 header.kid = Some(kid.clone());
501 }
502
503 encode(&header, &claims, &self.encoding_key).map_err(|e| AuthError::Token(e.to_string()))
504 }
505
506 pub fn validate_token(
507 &self,
508 token: &str,
509 expected_aud: Option<&str>,
510 ) -> Result<Claims, AuthError> {
511 let mut validation = Validation::new(self.alg);
512 if let Some(aud) = expected_aud {
513 validation.set_audience(&[aud]);
514 } else {
515 validation.validate_aud = false;
516 }
517 if let Some(ref iss) = self.issuer {
518 validation.set_issuer(&[iss]);
519 }
520
521 let token_data = decode::<Claims>(token, &self.decoding_key, &validation)
522 .map_err(|e| AuthError::Token(e.to_string()))?;
523
524 Ok(token_data.claims)
525 }
526}
527
528#[cfg(test)]
529mod tests {
530
531 use super::*;
532 use crate::auth::state::Identity;
533 use std::collections::HashMap;
534
535 const TEST_RSA_PRIVATE_KEY_PEM: &[u8] = b"-----BEGIN PRIVATE KEY-----
537MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDA5hJIcQ+2rxMz
538VM8ZH5WAmguCr0xmNDAdy0IzzsUeFLG7BebB7izOkU36J4t8t5tUaQwrBMnx2Fvt
539VqJjbdE242UDpvWF/8m9zJ2HR5298cbwT5cGMKLB0HWzDMahugs+Bbh2lCgwyLZk
540Tr3Diwxp5SwFew/Wb+Ke9cNG9Hu5IFH3BCuJ839d9hfqisIeYrBPfb52xxckM37R
5417zSGu/eDP/HZAeLkQuptZJW4A3u7xni14u4qyqXDqsHsYFNgJaxMSAwWgBRY6HNu
542TnvBArTXCiVfL+F73B2L6mdYr64g+QS9nK9v97MlJu/E3mSduz54pren4mpCHc9m
543/S2+VjCZAgMBAAECggEAASC9qQbGnL7XuExRDOIn/m4bWx92ehjo0lCTibhpY3LW
544umbSbpfbhmmuSj3CjW9VZsaM3hBTgSjoTX72lbY/eIUXD7c0memUK5pV4XcEIrQw
545AZlPIye6ckx4I7ZGnKasO8FoAel9dd7DXw36AuBK3LBzJwtzkEFsBc0e3/wixqmG
546UJBbbt/+5ya7CxyjuePaQhKtkLD5R6DpvN2XnCYq5nHJNJdvSVg1pOzsTHYIf+Ee
5472Rz42fGsfFKqeEQCcBFRZaGb/ELeP4c6UZdktZAvmHb1p1fursVZc6X9JXmiJ2OJ
548Kv2H2tMKuysP8L0fXFOMgkH2SVt6rcdHkO6xhlhWsQKBgQDqR8rAJeEE5BFoXA8T
549VVW6CLMlW51x4ey7PEGOaYh39dTG2Q+GZQBZ9G+SZk3f5Y85UCACSyc//4qaz/c3
5500nWsegZ+JPyymmuc79wzIAFFvXB7pL6wyn0Ed1P620kOZTtA8iBcXrsuxL+KP7iu
551MXfWmU1QiZpbndILtyDnY+70uwKBgQDSyCljWkydQCaPU+fiAXLxP8CvcJTSSNQD
552mVUlwJ+OpHnU+Alsi1rBavMgUtLlYbFqzH7NmYrLC8Yadq3ZOwLt0VEK0r8qstAL
5537QCDUD2WNuQjpZupRnXuMUl3iXB96i2gb+VQKGuUAJvVWjdIbYa4+Gu+sBMfcDcX
554dBihDLuEuwKBgAgX4tEwfc2Fc3R/eaXZVNTQaB/qQk4k1+C//CPHUYeTXn5gEUE7
555S//PiesszZPmgkQgmHp7zidP1KH0fT3Yb2g97ut8q54f54fMYXcCrAiUusYKsuu4
556kwkMdkI8QRHWPW3I74VBYIYFFfjYqrCZ1OH8+cbGeiagFRmCggh8U0zxAoGAVW3u
5576Ge22Z0gg8LcHsu7jG/sZq7Ygool8/d3fT+e669Z+ak2GJo6hF4WgClRdMqtn72W
558PzpV+ImjFyK2v26dd0n48MwN0v56N/ss1Av3iiRhPtlmR6tZLNspDZvUzhPVvkrb
559xCs9vtSoVEamVWKe0eVNthGjDoDqs0TInq2MavUCgYB6REavSJs/CLkSS7iimjxZ
560G7g5YQi9/p1lXLOEUDiwEmvRr0XTwzzxUsIc535IXhh/ZUYpthenW+qBBzn85pEC
561TowIqciHu5redqlQ8rITA8/AOY98vaDIhppDg1rfpnHHaZHFbXD/keYAEbhBtbvf
562a0QMqKUcs8+YTy5R5K6qtw==
563-----END PRIVATE KEY-----";
564
565 const TEST_ED25519_PRIVATE_KEY_PEM: &[u8] = b"-----BEGIN PRIVATE KEY-----
568MC4CAQAwBQYDK2VwBCIEIKIPR2jojpdobYr1M/pjIRuMONpZGYQ+y5yxSqKX9T9/
569-----END PRIVATE KEY-----";
570
571 const TEST_ED25519_PRIVATE_KEY_PEM_B: &[u8] = b"-----BEGIN PRIVATE KEY-----
574MC4CAQAwBQYDK2VwBCIEIPlsnSfvh53rJ+Tlbo8e7cgq2mIkWQ1NCM5paVeinUh8
575-----END PRIVATE KEY-----";
576
577 #[test]
578 fn test_claims_serialization() {
579 let mut extra = HashMap::new();
580 extra.insert(
581 "custom".to_string(),
582 serde_json::Value::String("value".to_string()),
583 );
584
585 let claims = Claims {
586 iss: Some("issuer".to_string()),
587 sub: "user123".to_string(),
588 aud: Some(Audience::from("audience")),
589 exp: 1000,
590 iat: 500,
591 nbf: Some(500),
592 jti: Some("jti".to_string()),
593 scope: Some("openid profile".to_string()),
594 identity: Some(Identity {
595 provider_id: "google".to_string(),
596 external_id: "user123".to_string(),
597 email: Some("user@example.com".to_string()),
598 username: Some("user".to_string()),
599 attributes: HashMap::new(),
600 }),
601 extra,
602 };
603
604 let serialized = serde_json::to_string(&claims).unwrap();
605 let deserialized: Claims = serde_json::from_str(&serialized).unwrap();
606
607 assert_eq!(deserialized.iss, claims.iss);
608 assert_eq!(deserialized.sub, claims.sub);
609 assert_eq!(deserialized.extra.get("custom").unwrap(), "value");
610 }
611
612 #[test]
613 fn test_token_manager_issuance() {
614 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
615 let identity = Identity {
616 provider_id: "mock".to_string(),
617 external_id: "user123".to_string(),
618 email: None,
619 username: None,
620 attributes: HashMap::new(),
621 };
622
623 let token = manager
624 .issue_user_token(identity, 3600, None, None)
625 .unwrap();
626 let claims = manager.validate_token(&token, None).unwrap();
627
628 assert_eq!(claims.iss, Some("issuer".to_string()));
629 assert_eq!(claims.sub, "user123");
630 assert!(claims.jti.is_some());
631 assert!(claims.nbf.is_some());
632 }
633
634 #[test]
635 fn test_token_manager_asymmetric_issuance() {
636 let manager = TokenManager::new_asymmetric(
637 TEST_RSA_PRIVATE_KEY_PEM,
638 Some("issuer".to_string()),
639 Some("my-kid-123".to_string()),
640 )
641 .unwrap();
642
643 let identity = Identity {
644 provider_id: "mock".to_string(),
645 external_id: "user123".to_string(),
646 email: None,
647 username: None,
648 attributes: HashMap::new(),
649 };
650
651 let token = manager
652 .issue_user_token(identity, 3600, None, None)
653 .unwrap();
654
655 let jwk = manager.public_jwk().unwrap();
657 assert_eq!(jwk.kid.as_deref(), Some("my-kid-123"));
658
659 let decoding_key = jwk.to_decoding_key().unwrap();
660 let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::RS256);
661 validation.set_issuer(&["issuer"]);
662
663 let token_data =
664 jsonwebtoken::decode::<Claims>(&token, &decoding_key, &validation).unwrap();
665 assert_eq!(token_data.claims.sub, "user123");
666 assert_eq!(token_data.header.kid.as_deref(), Some("my-kid-123"));
667 }
668
669 #[test]
678 fn test_asymmetric_manager_validates_its_own_tokens() {
679 let manager = TokenManager::new_asymmetric(
680 TEST_RSA_PRIVATE_KEY_PEM,
681 Some("issuer".to_string()),
682 Some("my-kid-123".to_string()),
683 )
684 .unwrap();
685
686 let identity = Identity {
687 provider_id: "mock".to_string(),
688 external_id: "user123".to_string(),
689 email: None,
690 username: None,
691 attributes: HashMap::new(),
692 };
693
694 let token = manager
695 .issue_user_token(identity, 3600, None, None)
696 .unwrap();
697
698 let claims = manager.validate_token(&token, None).unwrap();
699 assert_eq!(claims.sub, "user123");
700 assert_eq!(claims.iss, Some("issuer".to_string()));
701
702 let attestation = manager
704 .issue_custom_token(
705 "device-1".to_string(),
706 60,
707 "webank-attest+jws",
708 HashMap::new(),
709 )
710 .unwrap();
711
712 let attest_claims = manager.validate_token(&attestation, None).unwrap();
713 assert_eq!(attest_claims.sub, "device-1");
714 }
715
716 #[test]
717 fn test_issue_id_token() {
718 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
719 let identity = Identity {
720 provider_id: "mock".to_string(),
721 external_id: "user123".to_string(),
722 email: None,
723 username: None,
724 attributes: HashMap::new(),
725 };
726
727 let token = manager
728 .issue_id_token(identity, "client-1", Some("nonce123".to_string()), 3600)
729 .unwrap();
730
731 let claims = manager.validate_token(&token, None).unwrap();
732
733 assert_eq!(claims.iss, Some("issuer".to_string()));
734 assert_eq!(claims.sub, "user123");
735 assert_eq!(claims.aud, Some(Audience::from("client-1")));
736 assert_eq!(claims.extra.get("nonce").unwrap(), "nonce123");
737 }
738 #[test]
739 fn test_token_manager_audience_validation() {
740 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
741 let identity = Identity {
742 provider_id: "mock".to_string(),
743 external_id: "user123".to_string(),
744 email: None,
745 username: None,
746 attributes: HashMap::new(),
747 };
748
749 let token = manager
751 .issue_id_token(identity, "client-1", None, 3600)
752 .unwrap();
753
754 let claims = manager.validate_token(&token, Some("client-1")).unwrap();
756 assert_eq!(claims.aud, Some(Audience::from("client-1")));
757
758 let err = manager
760 .validate_token(&token, Some("client-2"))
761 .unwrap_err();
762 assert!(err.to_string().contains("InvalidAudience"));
763 }
764
765 #[test]
766 fn test_issue_custom_token_sets_typ_and_no_aud() {
767 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
768
769 let mut extra = HashMap::new();
770 extra.insert("cnf".to_string(), serde_json::json!({"jkt": "abc"}));
771
772 let token = manager
773 .issue_custom_token("device-1".to_string(), 60, "webank-attest+jws", extra)
774 .unwrap();
775
776 let header = jsonwebtoken::decode_header(&token).unwrap();
777 assert_eq!(header.typ.as_deref(), Some("webank-attest+jws"));
778
779 let claims = manager.validate_token(&token, None).unwrap();
780 assert_eq!(claims.sub, "device-1");
781 assert_eq!(claims.aud, None);
782 assert_eq!(claims.extra.get("cnf").unwrap()["jkt"], "abc");
783 }
784
785 #[test]
789 fn test_issue_custom_token_jti_override_round_trips() {
790 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
791
792 let mut extra = HashMap::new();
793 extra.insert(
794 "jti".to_string(),
795 serde_json::json!("caller-supplied-cuid2"),
796 );
797
798 let token = manager
799 .issue_custom_token("device-1".to_string(), 60, "webank-attest+jws", extra)
800 .unwrap();
801 let claims = manager
802 .validate_token(&token, None)
803 .expect("TokenManager must be able to decode the token it just issued");
804
805 assert_eq!(claims.jti, Some("caller-supplied-cuid2".to_string()));
806 assert!(!claims.extra.contains_key("jti"));
807 }
808
809 #[test]
810 fn test_issue_user_token_with_extra_round_trips_custom_claims() {
811 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
812 let identity = Identity {
813 provider_id: "mock".to_string(),
814 external_id: "user123".to_string(),
815 email: None,
816 username: None,
817 attributes: HashMap::new(),
818 };
819
820 let mut extra = HashMap::new();
821 extra.insert("api_key_id".to_string(), serde_json::json!("key-abc"));
822 extra.insert("project_id".to_string(), serde_json::json!("proj-42"));
823
824 let token = manager
825 .issue_user_token_with_extra(identity, 3600, None, None, extra)
826 .unwrap();
827 let claims = manager.validate_token(&token, None).unwrap();
828
829 assert_eq!(
830 claims.extra.get("api_key_id"),
831 Some(&serde_json::json!("key-abc"))
832 );
833 assert_eq!(
834 claims.extra.get("project_id"),
835 Some(&serde_json::json!("proj-42"))
836 );
837 }
838
839 #[test]
840 fn test_issue_user_token_wrapper_still_has_empty_extra() {
841 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
842 let identity = Identity {
843 provider_id: "mock".to_string(),
844 external_id: "user123".to_string(),
845 email: None,
846 username: None,
847 attributes: HashMap::new(),
848 };
849
850 let token = manager
851 .issue_user_token(identity, 3600, None, None)
852 .unwrap();
853 let claims = manager.validate_token(&token, None).unwrap();
854
855 assert!(claims.extra.is_empty());
856 }
857
858 #[test]
866 fn test_issue_user_token_with_extra_jti_override_no_duplicate_key() {
867 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
868 let identity = Identity {
869 provider_id: "mock".to_string(),
870 external_id: "user123".to_string(),
871 email: None,
872 username: None,
873 attributes: HashMap::new(),
874 };
875
876 let mut extra = HashMap::new();
877 extra.insert(
878 "jti".to_string(),
879 serde_json::json!("caller-supplied-cuid2"),
880 );
881
882 let token = manager
883 .issue_user_token_with_extra(identity, 3600, None, None, extra)
884 .unwrap();
885
886 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
887 let payload_b64 = token.split('.').nth(1).unwrap();
888 let payload_json = String::from_utf8(URL_SAFE_NO_PAD.decode(payload_b64).unwrap()).unwrap();
889 assert_eq!(
890 payload_json.matches("\"jti\"").count(),
891 1,
892 "wire payload must contain exactly one jti key, got: {payload_json}"
893 );
894
895 let claims = manager
896 .validate_token(&token, None)
897 .expect("TokenManager must be able to decode the token it just issued");
898 assert_eq!(claims.jti, Some("caller-supplied-cuid2".to_string()));
899 }
900
901 #[test]
904 fn test_issue_user_token_with_extra_default_jti_is_still_generated_uuid() {
905 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
906 let identity = Identity {
907 provider_id: "mock".to_string(),
908 external_id: "user123".to_string(),
909 email: None,
910 username: None,
911 attributes: HashMap::new(),
912 };
913
914 let token = manager
915 .issue_user_token_with_extra(identity, 3600, None, None, HashMap::new())
916 .unwrap();
917 let claims = manager.validate_token(&token, None).unwrap();
918
919 let jti = claims.jti.expect("jti must still be generated by default");
920 assert!(
921 uuid::Uuid::parse_str(&jti).is_ok(),
922 "default-path jti must still be a UUID, got: {jti}"
923 );
924 }
925
926 #[test]
927 fn test_issue_client_token_with_extra_round_trips_custom_claims() {
928 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
929
930 let mut extra = HashMap::new();
931 extra.insert("roles".to_string(), serde_json::json!(["admin", "billing"]));
932
933 let token = manager
934 .issue_client_token_with_extra("client-1", 3600, None, None, extra)
935 .unwrap();
936 let claims = manager.validate_token(&token, None).unwrap();
937
938 assert_eq!(
939 claims.extra.get("roles"),
940 Some(&serde_json::json!(["admin", "billing"]))
941 );
942 }
943
944 #[test]
945 fn test_issue_client_token_wrapper_still_has_empty_extra() {
946 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
947
948 let token = manager
949 .issue_client_token("client-1", 3600, None, None)
950 .unwrap();
951 let claims = manager.validate_token(&token, None).unwrap();
952
953 assert!(claims.extra.is_empty());
954 }
955
956 #[test]
960 fn test_issue_client_token_with_extra_jti_override_round_trips() {
961 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
962
963 let mut extra = HashMap::new();
964 extra.insert(
965 "jti".to_string(),
966 serde_json::json!("caller-supplied-cuid2"),
967 );
968
969 let token = manager
970 .issue_client_token_with_extra("client-1", 3600, None, None, extra)
971 .unwrap();
972 let claims = manager
973 .validate_token(&token, None)
974 .expect("TokenManager must be able to decode the token it just issued");
975
976 assert_eq!(claims.jti, Some("caller-supplied-cuid2".to_string()));
977 assert!(!claims.extra.contains_key("jti"));
978 }
979
980 #[test]
981 fn test_issue_id_token_with_extra_round_trips_custom_claims() {
982 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
983 let identity = Identity {
984 provider_id: "mock".to_string(),
985 external_id: "user123".to_string(),
986 email: None,
987 username: None,
988 attributes: HashMap::new(),
989 };
990
991 let mut extra = HashMap::new();
992 extra.insert("org_id".to_string(), serde_json::json!("org-7"));
993
994 let token = manager
995 .issue_id_token_with_extra(
996 identity,
997 "client-1",
998 Some("nonce123".to_string()),
999 3600,
1000 extra,
1001 )
1002 .unwrap();
1003 let claims = manager.validate_token(&token, None).unwrap();
1004
1005 assert_eq!(
1006 claims.extra.get("org_id"),
1007 Some(&serde_json::json!("org-7"))
1008 );
1009 assert_eq!(
1010 claims.extra.get("nonce"),
1011 Some(&serde_json::json!("nonce123"))
1012 );
1013 }
1014
1015 #[test]
1016 fn test_issue_id_token_with_extra_explicit_nonce_wins_over_extra_nonce() {
1017 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
1021 let identity = Identity {
1022 provider_id: "mock".to_string(),
1023 external_id: "user123".to_string(),
1024 email: None,
1025 username: None,
1026 attributes: HashMap::new(),
1027 };
1028
1029 let mut extra = HashMap::new();
1030 extra.insert(
1031 "nonce".to_string(),
1032 serde_json::json!("attacker-supplied-nonce"),
1033 );
1034
1035 let token = manager
1036 .issue_id_token_with_extra(
1037 identity,
1038 "client-1",
1039 Some("real-nonce".to_string()),
1040 3600,
1041 extra,
1042 )
1043 .unwrap();
1044 let claims = manager.validate_token(&token, None).unwrap();
1045
1046 assert_eq!(
1047 claims.extra.get("nonce"),
1048 Some(&serde_json::json!("real-nonce"))
1049 );
1050 }
1051
1052 #[test]
1056 fn test_issue_id_token_with_extra_jti_override_round_trips() {
1057 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
1058 let identity = Identity {
1059 provider_id: "mock".to_string(),
1060 external_id: "user123".to_string(),
1061 email: None,
1062 username: None,
1063 attributes: HashMap::new(),
1064 };
1065
1066 let mut extra = HashMap::new();
1067 extra.insert(
1068 "jti".to_string(),
1069 serde_json::json!("caller-supplied-cuid2"),
1070 );
1071
1072 let token = manager
1073 .issue_id_token_with_extra(identity, "client-1", None, 3600, extra)
1074 .unwrap();
1075 let claims = manager
1076 .validate_token(&token, None)
1077 .expect("TokenManager must be able to decode the token it just issued");
1078
1079 assert_eq!(claims.jti, Some("caller-supplied-cuid2".to_string()));
1080 assert!(!claims.extra.contains_key("jti"));
1081 }
1082
1083 #[test]
1084 fn test_token_manager_ed25519_issuance() {
1085 let manager = TokenManager::new_ed25519(
1086 TEST_ED25519_PRIVATE_KEY_PEM,
1087 Some("issuer".to_string()),
1088 Some("my-ed25519-kid".to_string()),
1089 )
1090 .unwrap();
1091
1092 let identity = Identity {
1093 provider_id: "mock".to_string(),
1094 external_id: "user123".to_string(),
1095 email: None,
1096 username: None,
1097 attributes: HashMap::new(),
1098 };
1099
1100 let token = manager
1101 .issue_user_token(identity, 3600, None, None)
1102 .unwrap();
1103
1104 let header = jsonwebtoken::decode_header(&token).unwrap();
1105 assert_eq!(header.alg, Algorithm::EdDSA);
1106 assert_eq!(header.kid.as_deref(), Some("my-ed25519-kid"));
1107 }
1108
1109 #[test]
1116 fn test_ed25519_token_round_trips_via_published_jwk() {
1117 let manager = TokenManager::new_ed25519(
1118 TEST_ED25519_PRIVATE_KEY_PEM,
1119 Some("issuer".to_string()),
1120 Some("my-ed25519-kid".to_string()),
1121 )
1122 .unwrap();
1123
1124 let identity = Identity {
1125 provider_id: "mock".to_string(),
1126 external_id: "user123".to_string(),
1127 email: None,
1128 username: None,
1129 attributes: HashMap::new(),
1130 };
1131
1132 let token = manager
1133 .issue_user_token(identity, 3600, None, None)
1134 .unwrap();
1135
1136 let jwk = manager.public_jwk().unwrap();
1137 assert_eq!(jwk.kid.as_deref(), Some("my-ed25519-kid"));
1138
1139 let decoding_key = jwk.to_decoding_key().unwrap();
1140 let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::EdDSA);
1141 validation.set_issuer(&["issuer"]);
1142
1143 let token_data =
1144 jsonwebtoken::decode::<Claims>(&token, &decoding_key, &validation).unwrap();
1145 assert_eq!(token_data.claims.sub, "user123");
1146 assert_eq!(token_data.header.kid.as_deref(), Some("my-ed25519-kid"));
1147 }
1148
1149 #[test]
1153 fn test_ed25519_manager_validates_its_own_tokens() {
1154 let manager = TokenManager::new_ed25519(
1155 TEST_ED25519_PRIVATE_KEY_PEM,
1156 Some("issuer".to_string()),
1157 Some("my-ed25519-kid".to_string()),
1158 )
1159 .unwrap();
1160
1161 let identity = Identity {
1162 provider_id: "mock".to_string(),
1163 external_id: "user123".to_string(),
1164 email: None,
1165 username: None,
1166 attributes: HashMap::new(),
1167 };
1168
1169 let token = manager
1170 .issue_user_token(identity, 3600, None, None)
1171 .unwrap();
1172
1173 let claims = manager.validate_token(&token, None).unwrap();
1174 assert_eq!(claims.sub, "user123");
1175 assert_eq!(claims.iss, Some("issuer".to_string()));
1176 }
1177
1178 #[test]
1183 fn test_ed25519_public_jwk_is_spec_correct_okp_json() {
1184 let manager = TokenManager::new_ed25519(
1185 TEST_ED25519_PRIVATE_KEY_PEM,
1186 Some("issuer".to_string()),
1187 Some("my-ed25519-kid".to_string()),
1188 )
1189 .unwrap();
1190
1191 let jwk = manager.public_jwk().unwrap();
1192 assert_eq!(jwk.kty, "OKP");
1193 assert_eq!(jwk.alg.as_deref(), Some("EdDSA"));
1194 assert_eq!(jwk.crv.as_deref(), Some("Ed25519"));
1195 assert!(jwk.n.is_none());
1196 assert!(jwk.e.is_none());
1197
1198 let x = jwk.x.as_ref().expect("OKP JWK must have 'x'");
1199 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
1200 let decoded = URL_SAFE_NO_PAD
1201 .decode(x)
1202 .expect("'x' must be valid base64url (unpadded)");
1203 assert_eq!(decoded.len(), 32, "Ed25519 public key must be 32 bytes");
1204
1205 let value = serde_json::to_value(&jwk).unwrap();
1206 let obj = value.as_object().unwrap();
1207 assert_eq!(obj.get("kty").unwrap(), "OKP");
1208 assert_eq!(obj.get("crv").unwrap(), "Ed25519");
1209 assert_eq!(obj.get("alg").unwrap(), "EdDSA");
1210 assert!(obj.contains_key("x"));
1211 assert!(
1212 !obj.contains_key("n"),
1213 "OKP JWK must not serialize the RSA 'n' field, got: {}",
1214 value
1215 );
1216 assert!(
1217 !obj.contains_key("e"),
1218 "OKP JWK must not serialize the RSA 'e' field, got: {}",
1219 value
1220 );
1221 }
1222
1223 #[test]
1226 fn test_rsa_public_jwk_still_omits_okp_fields() {
1227 let manager = TokenManager::new_asymmetric(
1228 TEST_RSA_PRIVATE_KEY_PEM,
1229 Some("issuer".to_string()),
1230 Some("my-kid-123".to_string()),
1231 )
1232 .unwrap();
1233
1234 let jwk = manager.public_jwk().unwrap();
1235 let value = serde_json::to_value(&jwk).unwrap();
1236 let obj = value.as_object().unwrap();
1237 assert_eq!(obj.get("kty").unwrap(), "RSA");
1238 assert!(obj.contains_key("n"));
1239 assert!(obj.contains_key("e"));
1240 assert!(
1241 !obj.contains_key("crv"),
1242 "RSA JWK must not serialize the OKP 'crv' field, got: {}",
1243 value
1244 );
1245 assert!(
1246 !obj.contains_key("x"),
1247 "RSA JWK must not serialize the OKP 'x' field, got: {}",
1248 value
1249 );
1250 }
1251
1252 #[test]
1257 fn test_ed25519_token_rejected_by_wrong_key_jwk() {
1258 let signer = TokenManager::new_ed25519(
1259 TEST_ED25519_PRIVATE_KEY_PEM,
1260 Some("issuer".to_string()),
1261 Some("kid-a".to_string()),
1262 )
1263 .unwrap();
1264 let other = TokenManager::new_ed25519(
1265 TEST_ED25519_PRIVATE_KEY_PEM_B,
1266 Some("issuer".to_string()),
1267 Some("kid-b".to_string()),
1268 )
1269 .unwrap();
1270
1271 let identity = Identity {
1272 provider_id: "mock".to_string(),
1273 external_id: "user123".to_string(),
1274 email: None,
1275 username: None,
1276 attributes: HashMap::new(),
1277 };
1278
1279 let token = signer.issue_user_token(identity, 3600, None, None).unwrap();
1280
1281 assert!(signer.validate_token(&token, None).is_ok());
1283
1284 let err = other.validate_token(&token, None).unwrap_err();
1286 assert!(err.to_string().contains("InvalidSignature"));
1287
1288 let wrong_jwk = other.public_jwk().unwrap();
1291 let decoding_key = wrong_jwk.to_decoding_key().unwrap();
1292 let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::EdDSA);
1293 validation.set_issuer(&["issuer"]);
1294 let err = jsonwebtoken::decode::<Claims>(&token, &decoding_key, &validation).unwrap_err();
1295 assert_eq!(
1296 err.kind(),
1297 &jsonwebtoken::errors::ErrorKind::InvalidSignature
1298 );
1299 }
1300
1301 #[test]
1305 fn test_ed25519_tampered_token_rejected() {
1306 let manager = TokenManager::new_ed25519(
1307 TEST_ED25519_PRIVATE_KEY_PEM,
1308 Some("issuer".to_string()),
1309 Some("my-ed25519-kid".to_string()),
1310 )
1311 .unwrap();
1312
1313 let identity = Identity {
1314 provider_id: "mock".to_string(),
1315 external_id: "user123".to_string(),
1316 email: None,
1317 username: None,
1318 attributes: HashMap::new(),
1319 };
1320
1321 let token = manager
1322 .issue_user_token(identity, 3600, None, None)
1323 .unwrap();
1324
1325 let mut parts: Vec<&str> = token.split('.').collect();
1326 assert_eq!(parts.len(), 3);
1327 let mut payload = parts[1].to_string();
1329 let last = payload.pop().unwrap();
1330 let replacement = if last == 'A' { 'B' } else { 'A' };
1331 payload.push(replacement);
1332 parts[1] = &payload;
1333 let tampered = parts.join(".");
1334
1335 let err = manager.validate_token(&tampered, None).unwrap_err();
1336 assert!(
1337 err.to_string().contains("InvalidSignature") || err.to_string().contains("Json"),
1338 "unexpected error for tampered token: {err}"
1339 );
1340 }
1341
1342 #[test]
1350 fn test_validate_token_accepts_array_aud() {
1351 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
1352
1353 let raw_claims = serde_json::json!({
1354 "iss": "issuer",
1355 "sub": "user123",
1356 "aud": ["client-1", "client-2"],
1357 "exp": (chrono::Utc::now().timestamp() as usize) + 3600,
1358 "iat": chrono::Utc::now().timestamp() as usize,
1359 });
1360 let token = jsonwebtoken::encode(
1361 &jsonwebtoken::Header::new(jsonwebtoken::Algorithm::HS256),
1362 &raw_claims,
1363 &jsonwebtoken::EncodingKey::from_secret(b"secret"),
1364 )
1365 .unwrap();
1366
1367 let claims = manager
1368 .validate_token(&token, None)
1369 .expect("multi-audience subject token must deserialize");
1370 let aud = claims.aud.expect("aud claim must be present");
1371 assert!(aud.contains("client-1"));
1372 assert!(aud.contains("client-2"));
1373 assert!(!aud.contains("client-3"));
1374 }
1375
1376 #[test]
1381 fn test_validate_token_still_accepts_string_aud() {
1382 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
1383 let identity = Identity {
1384 provider_id: "mock".to_string(),
1385 external_id: "user123".to_string(),
1386 email: None,
1387 username: None,
1388 attributes: HashMap::new(),
1389 };
1390
1391 let token = manager
1392 .issue_id_token(identity, "client-1", None, 3600)
1393 .unwrap();
1394
1395 let claims = manager.validate_token(&token, None).unwrap();
1396 let aud = claims.aud.expect("aud claim must be present");
1397 assert!(aud.contains("client-1"));
1398 assert!(!aud.contains("client-2"));
1399 }
1400
1401 #[test]
1406 fn test_audience_single_round_trips_as_bare_string() {
1407 let single = Audience::Single("client-1".to_string());
1408 let serialized = serde_json::to_string(&single).unwrap();
1409 assert_eq!(serialized, "\"client-1\"");
1410
1411 let deserialized: Audience = serde_json::from_str(&serialized).unwrap();
1412 assert_eq!(deserialized, single);
1413
1414 let multiple = Audience::Multiple(vec!["client-1".to_string(), "client-2".to_string()]);
1415 let serialized_multi = serde_json::to_string(&multiple).unwrap();
1416 assert_eq!(serialized_multi, "[\"client-1\",\"client-2\"]");
1417
1418 let deserialized_multi: Audience = serde_json::from_str(&serialized_multi).unwrap();
1419 assert_eq!(deserialized_multi, multiple);
1420 }
1421}
1422pub mod jwk;