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 From<String> for Audience {
40 fn from(value: String) -> Self {
41 Audience::Single(value)
42 }
43}
44
45impl From<&str> for Audience {
46 fn from(value: &str) -> Self {
47 Audience::Single(value.to_string())
48 }
49}
50
51fn take_jti(extra: &mut HashMap<String, serde_json::Value>) -> String {
68 match extra.remove("jti") {
69 Some(serde_json::Value::String(jti)) => jti,
70 _ => uuid::Uuid::new_v4().to_string(),
71 }
72}
73
74#[derive(Debug, Serialize, Deserialize, Clone)]
75pub struct Claims {
76 pub iss: Option<String>,
78 pub sub: String,
79 pub aud: Option<Audience>,
80 pub exp: usize,
81 pub iat: usize,
82 pub nbf: Option<usize>,
83 pub jti: Option<String>,
84
85 pub scope: Option<String>,
87 #[serde(skip_serializing_if = "Option::is_none")]
90 pub identity: Option<Identity>,
91
92 #[serde(flatten)]
94 pub extra: HashMap<String, serde_json::Value>,
95}
96
97#[derive(Clone)]
98pub struct TokenManager {
99 encoding_key: EncodingKey,
100 decoding_key: DecodingKey,
101 issuer: Option<String>,
102 kid: Option<String>,
103 alg: Algorithm,
104 public_jwk: Option<crate::token::jwk::Jwk>,
105}
106
107impl TokenManager {
108 pub fn new(secret: &[u8], issuer: Option<String>) -> Self {
110 Self {
111 encoding_key: EncodingKey::from_secret(secret),
112 decoding_key: DecodingKey::from_secret(secret),
113 issuer,
114 kid: None,
115 alg: Algorithm::HS256,
116 public_jwk: None,
117 }
118 }
119
120 pub fn new_asymmetric(
125 private_key_pem: &[u8],
126 issuer: Option<String>,
127 kid: Option<String>,
128 ) -> Result<Self, AuthError> {
129 let encoding_key = EncodingKey::from_rsa_pem(private_key_pem)
130 .map_err(|e| AuthError::Token(e.to_string()))?;
131
132 let pem_str = std::str::from_utf8(private_key_pem)
133 .map_err(|_| AuthError::Token("Invalid PEM UTF-8".into()))?;
134
135 use rsa::pkcs1::DecodeRsaPrivateKey;
136 use rsa::pkcs8::DecodePrivateKey;
137 let rsa_key = rsa::RsaPrivateKey::from_pkcs8_pem(pem_str)
138 .or_else(|_| rsa::RsaPrivateKey::from_pkcs1_pem(pem_str))
139 .map_err(|e| AuthError::Token(format!("Failed to parse RSA key: {}", e)))?;
140
141 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
142 use rsa::traits::PublicKeyParts;
143
144 let n = URL_SAFE_NO_PAD.encode(rsa_key.n().to_bytes_be());
145 let e = URL_SAFE_NO_PAD.encode(rsa_key.e().to_bytes_be());
146
147 let kid_val = kid.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
148
149 let jwk = crate::token::jwk::Jwk {
150 kid: Some(kid_val.clone()),
151 kty: "RSA".to_string(),
152 alg: Some("RS256".to_string()),
153 n: Some(n),
154 e: Some(e),
155 crv: None,
156 x: None,
157 };
158
159 let decoding_key = jwk.to_decoding_key()?;
164
165 Ok(Self {
166 encoding_key,
167 decoding_key,
168 issuer,
169 kid: Some(kid_val),
170 alg: Algorithm::RS256,
171 public_jwk: Some(jwk),
172 })
173 }
174
175 pub fn new_ed25519(
187 private_key_pem: &[u8],
188 issuer: Option<String>,
189 kid: Option<String>,
190 ) -> Result<Self, AuthError> {
191 let encoding_key = EncodingKey::from_ed_pem(private_key_pem)
192 .map_err(|e| AuthError::Token(e.to_string()))?;
193
194 let pem_str = std::str::from_utf8(private_key_pem)
195 .map_err(|_| AuthError::Token("Invalid PEM UTF-8".into()))?;
196
197 use ed25519_dalek::pkcs8::DecodePrivateKey;
198 let signing_key = ed25519_dalek::SigningKey::from_pkcs8_pem(pem_str)
199 .map_err(|e| AuthError::Token(format!("Failed to parse Ed25519 key: {}", e)))?;
200
201 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
202 let x = URL_SAFE_NO_PAD.encode(signing_key.verifying_key().to_bytes());
203
204 let kid_val = kid.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
205
206 let jwk = crate::token::jwk::Jwk {
207 kid: Some(kid_val.clone()),
208 kty: "OKP".to_string(),
209 alg: Some("EdDSA".to_string()),
210 n: None,
211 e: None,
212 crv: Some("Ed25519".to_string()),
213 x: Some(x),
214 };
215
216 let decoding_key = jwk.to_decoding_key()?;
222
223 Ok(Self {
224 encoding_key,
225 decoding_key,
226 issuer,
227 kid: Some(kid_val),
228 alg: Algorithm::EdDSA,
229 public_jwk: Some(jwk),
230 })
231 }
232
233 pub fn public_jwk(&self) -> Option<crate::token::jwk::Jwk> {
234 self.public_jwk.clone()
235 }
236
237 pub fn with_issuer(mut self, issuer: String) -> Self {
238 self.issuer = Some(issuer);
239 self
240 }
241
242 pub fn issue_user_token(
244 &self,
245 identity: Identity,
246 expires_in_secs: u64,
247 scope: Option<String>,
248 aud: Option<String>,
249 ) -> Result<String, AuthError> {
250 self.issue_user_token_with_extra(identity, expires_in_secs, scope, aud, HashMap::new())
251 }
252
253 pub fn issue_user_token_with_extra(
271 &self,
272 identity: Identity,
273 expires_in_secs: u64,
274 scope: Option<String>,
275 aud: Option<String>,
276 mut extra: HashMap<String, serde_json::Value>,
277 ) -> Result<String, AuthError> {
278 let now = chrono::Utc::now().timestamp() as usize;
279 let expiration = now + expires_in_secs as usize;
280 let jti = take_jti(&mut extra);
281
282 let claims = Claims {
283 iss: self.issuer.clone(),
284 sub: identity.external_id.clone(),
285 aud: aud.map(Audience::from),
286 exp: expiration,
287 iat: now,
288 nbf: Some(now),
289 jti: Some(jti),
290 scope,
291 identity: Some(identity),
292 extra,
293 };
294
295 let mut header = Header::new(self.alg);
296 if let Some(ref kid) = self.kid {
297 header.kid = Some(kid.clone());
298 }
299
300 encode(&header, &claims, &self.encoding_key).map_err(|e| AuthError::Token(e.to_string()))
301 }
302
303 pub fn issue_id_token(
305 &self,
306 identity: Identity,
307 client_id: &str,
308 nonce: Option<String>,
309 expires_in_secs: u64,
310 ) -> Result<String, AuthError> {
311 self.issue_id_token_with_extra(identity, client_id, nonce, expires_in_secs, HashMap::new())
312 }
313
314 pub fn issue_id_token_with_extra(
330 &self,
331 identity: Identity,
332 client_id: &str,
333 nonce: Option<String>,
334 expires_in_secs: u64,
335 mut extra: HashMap<String, serde_json::Value>,
336 ) -> Result<String, AuthError> {
337 let now = chrono::Utc::now().timestamp() as usize;
338 let expiration = now + expires_in_secs as usize;
339 let jti = take_jti(&mut extra);
340
341 let mut claims = Claims {
342 iss: self.issuer.clone(),
343 sub: identity.external_id.clone(),
344 aud: Some(Audience::from(client_id)),
345 exp: expiration,
346 iat: now,
347 nbf: Some(now),
348 jti: Some(jti),
349 scope: None,
350 identity: Some(identity),
351 extra,
352 };
353
354 if let Some(n) = nonce {
355 claims
356 .extra
357 .insert("nonce".to_string(), serde_json::Value::String(n));
358 }
359
360 let mut header = Header::new(self.alg);
361 if let Some(ref kid) = self.kid {
362 header.kid = Some(kid.clone());
363 }
364
365 encode(&header, &claims, &self.encoding_key).map_err(|e| AuthError::Token(e.to_string()))
366 }
367
368 pub fn issue_client_token(
370 &self,
371 client_id: &str,
372 expires_in_secs: u64,
373 scope: Option<String>,
374 aud: Option<String>,
375 ) -> Result<String, AuthError> {
376 self.issue_client_token_with_extra(client_id, expires_in_secs, scope, aud, HashMap::new())
377 }
378
379 pub fn issue_client_token_with_extra(
384 &self,
385 client_id: &str,
386 expires_in_secs: u64,
387 scope: Option<String>,
388 aud: Option<String>,
389 mut extra: HashMap<String, serde_json::Value>,
390 ) -> Result<String, AuthError> {
391 let now = chrono::Utc::now().timestamp() as usize;
392 let expiration = now + expires_in_secs as usize;
393 let jti = take_jti(&mut extra);
394
395 let claims = Claims {
396 iss: self.issuer.clone(),
397 sub: client_id.to_string(),
398 aud: aud.map(Audience::from),
399 exp: expiration,
400 iat: now,
401 nbf: Some(now),
402 jti: Some(jti),
403 scope,
404 identity: None,
405 extra,
406 };
407
408 let mut header = Header::new(self.alg);
409 if let Some(ref kid) = self.kid {
410 header.kid = Some(kid.clone());
411 }
412
413 encode(&header, &claims, &self.encoding_key).map_err(|e| AuthError::Token(e.to_string()))
414 }
415
416 pub fn issue_custom_token(
425 &self,
426 sub: String,
427 expires_in_secs: u64,
428 typ: &str,
429 mut extra: HashMap<String, serde_json::Value>,
430 ) -> Result<String, AuthError> {
431 let now = chrono::Utc::now().timestamp() as usize;
432 let jti = take_jti(&mut extra);
433 let claims = Claims {
434 iss: self.issuer.clone(),
435 sub,
436 aud: None,
437 exp: now + expires_in_secs as usize,
438 iat: now,
439 nbf: Some(now),
440 jti: Some(jti),
441 scope: None,
442 identity: None,
443 extra,
444 };
445
446 let mut header = Header::new(self.alg);
447 header.typ = Some(typ.to_string());
448 if let Some(ref kid) = self.kid {
449 header.kid = Some(kid.clone());
450 }
451
452 encode(&header, &claims, &self.encoding_key).map_err(|e| AuthError::Token(e.to_string()))
453 }
454
455 pub fn validate_token(
456 &self,
457 token: &str,
458 expected_aud: Option<&str>,
459 ) -> Result<Claims, AuthError> {
460 let mut validation = Validation::new(self.alg);
461 if let Some(aud) = expected_aud {
462 validation.set_audience(&[aud]);
463 } else {
464 validation.validate_aud = false;
465 }
466 if let Some(ref iss) = self.issuer {
467 validation.set_issuer(&[iss]);
468 }
469
470 let token_data = decode::<Claims>(token, &self.decoding_key, &validation)
471 .map_err(|e| AuthError::Token(e.to_string()))?;
472
473 Ok(token_data.claims)
474 }
475}
476
477#[cfg(test)]
478mod tests {
479
480 use super::*;
481 use crate::auth::state::Identity;
482 use std::collections::HashMap;
483
484 const TEST_RSA_PRIVATE_KEY_PEM: &[u8] = b"-----BEGIN PRIVATE KEY-----
486MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDA5hJIcQ+2rxMz
487VM8ZH5WAmguCr0xmNDAdy0IzzsUeFLG7BebB7izOkU36J4t8t5tUaQwrBMnx2Fvt
488VqJjbdE242UDpvWF/8m9zJ2HR5298cbwT5cGMKLB0HWzDMahugs+Bbh2lCgwyLZk
489Tr3Diwxp5SwFew/Wb+Ke9cNG9Hu5IFH3BCuJ839d9hfqisIeYrBPfb52xxckM37R
4907zSGu/eDP/HZAeLkQuptZJW4A3u7xni14u4qyqXDqsHsYFNgJaxMSAwWgBRY6HNu
491TnvBArTXCiVfL+F73B2L6mdYr64g+QS9nK9v97MlJu/E3mSduz54pren4mpCHc9m
492/S2+VjCZAgMBAAECggEAASC9qQbGnL7XuExRDOIn/m4bWx92ehjo0lCTibhpY3LW
493umbSbpfbhmmuSj3CjW9VZsaM3hBTgSjoTX72lbY/eIUXD7c0memUK5pV4XcEIrQw
494AZlPIye6ckx4I7ZGnKasO8FoAel9dd7DXw36AuBK3LBzJwtzkEFsBc0e3/wixqmG
495UJBbbt/+5ya7CxyjuePaQhKtkLD5R6DpvN2XnCYq5nHJNJdvSVg1pOzsTHYIf+Ee
4962Rz42fGsfFKqeEQCcBFRZaGb/ELeP4c6UZdktZAvmHb1p1fursVZc6X9JXmiJ2OJ
497Kv2H2tMKuysP8L0fXFOMgkH2SVt6rcdHkO6xhlhWsQKBgQDqR8rAJeEE5BFoXA8T
498VVW6CLMlW51x4ey7PEGOaYh39dTG2Q+GZQBZ9G+SZk3f5Y85UCACSyc//4qaz/c3
4990nWsegZ+JPyymmuc79wzIAFFvXB7pL6wyn0Ed1P620kOZTtA8iBcXrsuxL+KP7iu
500MXfWmU1QiZpbndILtyDnY+70uwKBgQDSyCljWkydQCaPU+fiAXLxP8CvcJTSSNQD
501mVUlwJ+OpHnU+Alsi1rBavMgUtLlYbFqzH7NmYrLC8Yadq3ZOwLt0VEK0r8qstAL
5027QCDUD2WNuQjpZupRnXuMUl3iXB96i2gb+VQKGuUAJvVWjdIbYa4+Gu+sBMfcDcX
503dBihDLuEuwKBgAgX4tEwfc2Fc3R/eaXZVNTQaB/qQk4k1+C//CPHUYeTXn5gEUE7
504S//PiesszZPmgkQgmHp7zidP1KH0fT3Yb2g97ut8q54f54fMYXcCrAiUusYKsuu4
505kwkMdkI8QRHWPW3I74VBYIYFFfjYqrCZ1OH8+cbGeiagFRmCggh8U0zxAoGAVW3u
5066Ge22Z0gg8LcHsu7jG/sZq7Ygool8/d3fT+e669Z+ak2GJo6hF4WgClRdMqtn72W
507PzpV+ImjFyK2v26dd0n48MwN0v56N/ss1Av3iiRhPtlmR6tZLNspDZvUzhPVvkrb
508xCs9vtSoVEamVWKe0eVNthGjDoDqs0TInq2MavUCgYB6REavSJs/CLkSS7iimjxZ
509G7g5YQi9/p1lXLOEUDiwEmvRr0XTwzzxUsIc535IXhh/ZUYpthenW+qBBzn85pEC
510TowIqciHu5redqlQ8rITA8/AOY98vaDIhppDg1rfpnHHaZHFbXD/keYAEbhBtbvf
511a0QMqKUcs8+YTy5R5K6qtw==
512-----END PRIVATE KEY-----";
513
514 const TEST_ED25519_PRIVATE_KEY_PEM: &[u8] = b"-----BEGIN PRIVATE KEY-----
517MC4CAQAwBQYDK2VwBCIEIKIPR2jojpdobYr1M/pjIRuMONpZGYQ+y5yxSqKX9T9/
518-----END PRIVATE KEY-----";
519
520 const TEST_ED25519_PRIVATE_KEY_PEM_B: &[u8] = b"-----BEGIN PRIVATE KEY-----
523MC4CAQAwBQYDK2VwBCIEIPlsnSfvh53rJ+Tlbo8e7cgq2mIkWQ1NCM5paVeinUh8
524-----END PRIVATE KEY-----";
525
526 #[test]
527 fn test_claims_serialization() {
528 let mut extra = HashMap::new();
529 extra.insert(
530 "custom".to_string(),
531 serde_json::Value::String("value".to_string()),
532 );
533
534 let claims = Claims {
535 iss: Some("issuer".to_string()),
536 sub: "user123".to_string(),
537 aud: Some(Audience::from("audience")),
538 exp: 1000,
539 iat: 500,
540 nbf: Some(500),
541 jti: Some("jti".to_string()),
542 scope: Some("openid profile".to_string()),
543 identity: Some(Identity {
544 provider_id: "google".to_string(),
545 external_id: "user123".to_string(),
546 email: Some("user@example.com".to_string()),
547 username: Some("user".to_string()),
548 attributes: HashMap::new(),
549 }),
550 extra,
551 };
552
553 let serialized = serde_json::to_string(&claims).unwrap();
554 let deserialized: Claims = serde_json::from_str(&serialized).unwrap();
555
556 assert_eq!(deserialized.iss, claims.iss);
557 assert_eq!(deserialized.sub, claims.sub);
558 assert_eq!(deserialized.extra.get("custom").unwrap(), "value");
559 }
560
561 #[test]
562 fn test_token_manager_issuance() {
563 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
564 let identity = Identity {
565 provider_id: "mock".to_string(),
566 external_id: "user123".to_string(),
567 email: None,
568 username: None,
569 attributes: HashMap::new(),
570 };
571
572 let token = manager
573 .issue_user_token(identity, 3600, None, None)
574 .unwrap();
575 let claims = manager.validate_token(&token, None).unwrap();
576
577 assert_eq!(claims.iss, Some("issuer".to_string()));
578 assert_eq!(claims.sub, "user123");
579 assert!(claims.jti.is_some());
580 assert!(claims.nbf.is_some());
581 }
582
583 #[test]
584 fn test_token_manager_asymmetric_issuance() {
585 let manager = TokenManager::new_asymmetric(
586 TEST_RSA_PRIVATE_KEY_PEM,
587 Some("issuer".to_string()),
588 Some("my-kid-123".to_string()),
589 )
590 .unwrap();
591
592 let identity = Identity {
593 provider_id: "mock".to_string(),
594 external_id: "user123".to_string(),
595 email: None,
596 username: None,
597 attributes: HashMap::new(),
598 };
599
600 let token = manager
601 .issue_user_token(identity, 3600, None, None)
602 .unwrap();
603
604 let jwk = manager.public_jwk().unwrap();
606 assert_eq!(jwk.kid.as_deref(), Some("my-kid-123"));
607
608 let decoding_key = jwk.to_decoding_key().unwrap();
609 let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::RS256);
610 validation.set_issuer(&["issuer"]);
611
612 let token_data =
613 jsonwebtoken::decode::<Claims>(&token, &decoding_key, &validation).unwrap();
614 assert_eq!(token_data.claims.sub, "user123");
615 assert_eq!(token_data.header.kid.as_deref(), Some("my-kid-123"));
616 }
617
618 #[test]
627 fn test_asymmetric_manager_validates_its_own_tokens() {
628 let manager = TokenManager::new_asymmetric(
629 TEST_RSA_PRIVATE_KEY_PEM,
630 Some("issuer".to_string()),
631 Some("my-kid-123".to_string()),
632 )
633 .unwrap();
634
635 let identity = Identity {
636 provider_id: "mock".to_string(),
637 external_id: "user123".to_string(),
638 email: None,
639 username: None,
640 attributes: HashMap::new(),
641 };
642
643 let token = manager
644 .issue_user_token(identity, 3600, None, None)
645 .unwrap();
646
647 let claims = manager.validate_token(&token, None).unwrap();
648 assert_eq!(claims.sub, "user123");
649 assert_eq!(claims.iss, Some("issuer".to_string()));
650
651 let attestation = manager
653 .issue_custom_token(
654 "device-1".to_string(),
655 60,
656 "webank-attest+jws",
657 HashMap::new(),
658 )
659 .unwrap();
660
661 let attest_claims = manager.validate_token(&attestation, None).unwrap();
662 assert_eq!(attest_claims.sub, "device-1");
663 }
664
665 #[test]
666 fn test_issue_id_token() {
667 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
668 let identity = Identity {
669 provider_id: "mock".to_string(),
670 external_id: "user123".to_string(),
671 email: None,
672 username: None,
673 attributes: HashMap::new(),
674 };
675
676 let token = manager
677 .issue_id_token(identity, "client-1", Some("nonce123".to_string()), 3600)
678 .unwrap();
679
680 let claims = manager.validate_token(&token, None).unwrap();
681
682 assert_eq!(claims.iss, Some("issuer".to_string()));
683 assert_eq!(claims.sub, "user123");
684 assert_eq!(claims.aud, Some(Audience::from("client-1")));
685 assert_eq!(claims.extra.get("nonce").unwrap(), "nonce123");
686 }
687 #[test]
688 fn test_token_manager_audience_validation() {
689 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
690 let identity = Identity {
691 provider_id: "mock".to_string(),
692 external_id: "user123".to_string(),
693 email: None,
694 username: None,
695 attributes: HashMap::new(),
696 };
697
698 let token = manager
700 .issue_id_token(identity, "client-1", None, 3600)
701 .unwrap();
702
703 let claims = manager.validate_token(&token, Some("client-1")).unwrap();
705 assert_eq!(claims.aud, Some(Audience::from("client-1")));
706
707 let err = manager
709 .validate_token(&token, Some("client-2"))
710 .unwrap_err();
711 assert!(err.to_string().contains("InvalidAudience"));
712 }
713
714 #[test]
715 fn test_issue_custom_token_sets_typ_and_no_aud() {
716 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
717
718 let mut extra = HashMap::new();
719 extra.insert("cnf".to_string(), serde_json::json!({"jkt": "abc"}));
720
721 let token = manager
722 .issue_custom_token("device-1".to_string(), 60, "webank-attest+jws", extra)
723 .unwrap();
724
725 let header = jsonwebtoken::decode_header(&token).unwrap();
726 assert_eq!(header.typ.as_deref(), Some("webank-attest+jws"));
727
728 let claims = manager.validate_token(&token, None).unwrap();
729 assert_eq!(claims.sub, "device-1");
730 assert_eq!(claims.aud, None);
731 assert_eq!(claims.extra.get("cnf").unwrap()["jkt"], "abc");
732 }
733
734 #[test]
738 fn test_issue_custom_token_jti_override_round_trips() {
739 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
740
741 let mut extra = HashMap::new();
742 extra.insert(
743 "jti".to_string(),
744 serde_json::json!("caller-supplied-cuid2"),
745 );
746
747 let token = manager
748 .issue_custom_token("device-1".to_string(), 60, "webank-attest+jws", extra)
749 .unwrap();
750 let claims = manager
751 .validate_token(&token, None)
752 .expect("TokenManager must be able to decode the token it just issued");
753
754 assert_eq!(claims.jti, Some("caller-supplied-cuid2".to_string()));
755 assert!(!claims.extra.contains_key("jti"));
756 }
757
758 #[test]
759 fn test_issue_user_token_with_extra_round_trips_custom_claims() {
760 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
761 let identity = Identity {
762 provider_id: "mock".to_string(),
763 external_id: "user123".to_string(),
764 email: None,
765 username: None,
766 attributes: HashMap::new(),
767 };
768
769 let mut extra = HashMap::new();
770 extra.insert("api_key_id".to_string(), serde_json::json!("key-abc"));
771 extra.insert("project_id".to_string(), serde_json::json!("proj-42"));
772
773 let token = manager
774 .issue_user_token_with_extra(identity, 3600, None, None, extra)
775 .unwrap();
776 let claims = manager.validate_token(&token, None).unwrap();
777
778 assert_eq!(
779 claims.extra.get("api_key_id"),
780 Some(&serde_json::json!("key-abc"))
781 );
782 assert_eq!(
783 claims.extra.get("project_id"),
784 Some(&serde_json::json!("proj-42"))
785 );
786 }
787
788 #[test]
789 fn test_issue_user_token_wrapper_still_has_empty_extra() {
790 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
791 let identity = Identity {
792 provider_id: "mock".to_string(),
793 external_id: "user123".to_string(),
794 email: None,
795 username: None,
796 attributes: HashMap::new(),
797 };
798
799 let token = manager
800 .issue_user_token(identity, 3600, None, None)
801 .unwrap();
802 let claims = manager.validate_token(&token, None).unwrap();
803
804 assert!(claims.extra.is_empty());
805 }
806
807 #[test]
815 fn test_issue_user_token_with_extra_jti_override_no_duplicate_key() {
816 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
817 let identity = Identity {
818 provider_id: "mock".to_string(),
819 external_id: "user123".to_string(),
820 email: None,
821 username: None,
822 attributes: HashMap::new(),
823 };
824
825 let mut extra = HashMap::new();
826 extra.insert(
827 "jti".to_string(),
828 serde_json::json!("caller-supplied-cuid2"),
829 );
830
831 let token = manager
832 .issue_user_token_with_extra(identity, 3600, None, None, extra)
833 .unwrap();
834
835 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
836 let payload_b64 = token.split('.').nth(1).unwrap();
837 let payload_json = String::from_utf8(URL_SAFE_NO_PAD.decode(payload_b64).unwrap()).unwrap();
838 assert_eq!(
839 payload_json.matches("\"jti\"").count(),
840 1,
841 "wire payload must contain exactly one jti key, got: {payload_json}"
842 );
843
844 let claims = manager
845 .validate_token(&token, None)
846 .expect("TokenManager must be able to decode the token it just issued");
847 assert_eq!(claims.jti, Some("caller-supplied-cuid2".to_string()));
848 }
849
850 #[test]
853 fn test_issue_user_token_with_extra_default_jti_is_still_generated_uuid() {
854 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
855 let identity = Identity {
856 provider_id: "mock".to_string(),
857 external_id: "user123".to_string(),
858 email: None,
859 username: None,
860 attributes: HashMap::new(),
861 };
862
863 let token = manager
864 .issue_user_token_with_extra(identity, 3600, None, None, HashMap::new())
865 .unwrap();
866 let claims = manager.validate_token(&token, None).unwrap();
867
868 let jti = claims.jti.expect("jti must still be generated by default");
869 assert!(
870 uuid::Uuid::parse_str(&jti).is_ok(),
871 "default-path jti must still be a UUID, got: {jti}"
872 );
873 }
874
875 #[test]
876 fn test_issue_client_token_with_extra_round_trips_custom_claims() {
877 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
878
879 let mut extra = HashMap::new();
880 extra.insert("roles".to_string(), serde_json::json!(["admin", "billing"]));
881
882 let token = manager
883 .issue_client_token_with_extra("client-1", 3600, None, None, extra)
884 .unwrap();
885 let claims = manager.validate_token(&token, None).unwrap();
886
887 assert_eq!(
888 claims.extra.get("roles"),
889 Some(&serde_json::json!(["admin", "billing"]))
890 );
891 }
892
893 #[test]
894 fn test_issue_client_token_wrapper_still_has_empty_extra() {
895 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
896
897 let token = manager
898 .issue_client_token("client-1", 3600, None, None)
899 .unwrap();
900 let claims = manager.validate_token(&token, None).unwrap();
901
902 assert!(claims.extra.is_empty());
903 }
904
905 #[test]
909 fn test_issue_client_token_with_extra_jti_override_round_trips() {
910 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
911
912 let mut extra = HashMap::new();
913 extra.insert(
914 "jti".to_string(),
915 serde_json::json!("caller-supplied-cuid2"),
916 );
917
918 let token = manager
919 .issue_client_token_with_extra("client-1", 3600, None, None, extra)
920 .unwrap();
921 let claims = manager
922 .validate_token(&token, None)
923 .expect("TokenManager must be able to decode the token it just issued");
924
925 assert_eq!(claims.jti, Some("caller-supplied-cuid2".to_string()));
926 assert!(!claims.extra.contains_key("jti"));
927 }
928
929 #[test]
930 fn test_issue_id_token_with_extra_round_trips_custom_claims() {
931 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
932 let identity = Identity {
933 provider_id: "mock".to_string(),
934 external_id: "user123".to_string(),
935 email: None,
936 username: None,
937 attributes: HashMap::new(),
938 };
939
940 let mut extra = HashMap::new();
941 extra.insert("org_id".to_string(), serde_json::json!("org-7"));
942
943 let token = manager
944 .issue_id_token_with_extra(
945 identity,
946 "client-1",
947 Some("nonce123".to_string()),
948 3600,
949 extra,
950 )
951 .unwrap();
952 let claims = manager.validate_token(&token, None).unwrap();
953
954 assert_eq!(
955 claims.extra.get("org_id"),
956 Some(&serde_json::json!("org-7"))
957 );
958 assert_eq!(
959 claims.extra.get("nonce"),
960 Some(&serde_json::json!("nonce123"))
961 );
962 }
963
964 #[test]
965 fn test_issue_id_token_with_extra_explicit_nonce_wins_over_extra_nonce() {
966 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
970 let identity = Identity {
971 provider_id: "mock".to_string(),
972 external_id: "user123".to_string(),
973 email: None,
974 username: None,
975 attributes: HashMap::new(),
976 };
977
978 let mut extra = HashMap::new();
979 extra.insert(
980 "nonce".to_string(),
981 serde_json::json!("attacker-supplied-nonce"),
982 );
983
984 let token = manager
985 .issue_id_token_with_extra(
986 identity,
987 "client-1",
988 Some("real-nonce".to_string()),
989 3600,
990 extra,
991 )
992 .unwrap();
993 let claims = manager.validate_token(&token, None).unwrap();
994
995 assert_eq!(
996 claims.extra.get("nonce"),
997 Some(&serde_json::json!("real-nonce"))
998 );
999 }
1000
1001 #[test]
1005 fn test_issue_id_token_with_extra_jti_override_round_trips() {
1006 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
1007 let identity = Identity {
1008 provider_id: "mock".to_string(),
1009 external_id: "user123".to_string(),
1010 email: None,
1011 username: None,
1012 attributes: HashMap::new(),
1013 };
1014
1015 let mut extra = HashMap::new();
1016 extra.insert(
1017 "jti".to_string(),
1018 serde_json::json!("caller-supplied-cuid2"),
1019 );
1020
1021 let token = manager
1022 .issue_id_token_with_extra(identity, "client-1", None, 3600, extra)
1023 .unwrap();
1024 let claims = manager
1025 .validate_token(&token, None)
1026 .expect("TokenManager must be able to decode the token it just issued");
1027
1028 assert_eq!(claims.jti, Some("caller-supplied-cuid2".to_string()));
1029 assert!(!claims.extra.contains_key("jti"));
1030 }
1031
1032 #[test]
1033 fn test_token_manager_ed25519_issuance() {
1034 let manager = TokenManager::new_ed25519(
1035 TEST_ED25519_PRIVATE_KEY_PEM,
1036 Some("issuer".to_string()),
1037 Some("my-ed25519-kid".to_string()),
1038 )
1039 .unwrap();
1040
1041 let identity = Identity {
1042 provider_id: "mock".to_string(),
1043 external_id: "user123".to_string(),
1044 email: None,
1045 username: None,
1046 attributes: HashMap::new(),
1047 };
1048
1049 let token = manager
1050 .issue_user_token(identity, 3600, None, None)
1051 .unwrap();
1052
1053 let header = jsonwebtoken::decode_header(&token).unwrap();
1054 assert_eq!(header.alg, Algorithm::EdDSA);
1055 assert_eq!(header.kid.as_deref(), Some("my-ed25519-kid"));
1056 }
1057
1058 #[test]
1065 fn test_ed25519_token_round_trips_via_published_jwk() {
1066 let manager = TokenManager::new_ed25519(
1067 TEST_ED25519_PRIVATE_KEY_PEM,
1068 Some("issuer".to_string()),
1069 Some("my-ed25519-kid".to_string()),
1070 )
1071 .unwrap();
1072
1073 let identity = Identity {
1074 provider_id: "mock".to_string(),
1075 external_id: "user123".to_string(),
1076 email: None,
1077 username: None,
1078 attributes: HashMap::new(),
1079 };
1080
1081 let token = manager
1082 .issue_user_token(identity, 3600, None, None)
1083 .unwrap();
1084
1085 let jwk = manager.public_jwk().unwrap();
1086 assert_eq!(jwk.kid.as_deref(), Some("my-ed25519-kid"));
1087
1088 let decoding_key = jwk.to_decoding_key().unwrap();
1089 let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::EdDSA);
1090 validation.set_issuer(&["issuer"]);
1091
1092 let token_data =
1093 jsonwebtoken::decode::<Claims>(&token, &decoding_key, &validation).unwrap();
1094 assert_eq!(token_data.claims.sub, "user123");
1095 assert_eq!(token_data.header.kid.as_deref(), Some("my-ed25519-kid"));
1096 }
1097
1098 #[test]
1102 fn test_ed25519_manager_validates_its_own_tokens() {
1103 let manager = TokenManager::new_ed25519(
1104 TEST_ED25519_PRIVATE_KEY_PEM,
1105 Some("issuer".to_string()),
1106 Some("my-ed25519-kid".to_string()),
1107 )
1108 .unwrap();
1109
1110 let identity = Identity {
1111 provider_id: "mock".to_string(),
1112 external_id: "user123".to_string(),
1113 email: None,
1114 username: None,
1115 attributes: HashMap::new(),
1116 };
1117
1118 let token = manager
1119 .issue_user_token(identity, 3600, None, None)
1120 .unwrap();
1121
1122 let claims = manager.validate_token(&token, None).unwrap();
1123 assert_eq!(claims.sub, "user123");
1124 assert_eq!(claims.iss, Some("issuer".to_string()));
1125 }
1126
1127 #[test]
1132 fn test_ed25519_public_jwk_is_spec_correct_okp_json() {
1133 let manager = TokenManager::new_ed25519(
1134 TEST_ED25519_PRIVATE_KEY_PEM,
1135 Some("issuer".to_string()),
1136 Some("my-ed25519-kid".to_string()),
1137 )
1138 .unwrap();
1139
1140 let jwk = manager.public_jwk().unwrap();
1141 assert_eq!(jwk.kty, "OKP");
1142 assert_eq!(jwk.alg.as_deref(), Some("EdDSA"));
1143 assert_eq!(jwk.crv.as_deref(), Some("Ed25519"));
1144 assert!(jwk.n.is_none());
1145 assert!(jwk.e.is_none());
1146
1147 let x = jwk.x.as_ref().expect("OKP JWK must have 'x'");
1148 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
1149 let decoded = URL_SAFE_NO_PAD
1150 .decode(x)
1151 .expect("'x' must be valid base64url (unpadded)");
1152 assert_eq!(decoded.len(), 32, "Ed25519 public key must be 32 bytes");
1153
1154 let value = serde_json::to_value(&jwk).unwrap();
1155 let obj = value.as_object().unwrap();
1156 assert_eq!(obj.get("kty").unwrap(), "OKP");
1157 assert_eq!(obj.get("crv").unwrap(), "Ed25519");
1158 assert_eq!(obj.get("alg").unwrap(), "EdDSA");
1159 assert!(obj.contains_key("x"));
1160 assert!(
1161 !obj.contains_key("n"),
1162 "OKP JWK must not serialize the RSA 'n' field, got: {}",
1163 value
1164 );
1165 assert!(
1166 !obj.contains_key("e"),
1167 "OKP JWK must not serialize the RSA 'e' field, got: {}",
1168 value
1169 );
1170 }
1171
1172 #[test]
1175 fn test_rsa_public_jwk_still_omits_okp_fields() {
1176 let manager = TokenManager::new_asymmetric(
1177 TEST_RSA_PRIVATE_KEY_PEM,
1178 Some("issuer".to_string()),
1179 Some("my-kid-123".to_string()),
1180 )
1181 .unwrap();
1182
1183 let jwk = manager.public_jwk().unwrap();
1184 let value = serde_json::to_value(&jwk).unwrap();
1185 let obj = value.as_object().unwrap();
1186 assert_eq!(obj.get("kty").unwrap(), "RSA");
1187 assert!(obj.contains_key("n"));
1188 assert!(obj.contains_key("e"));
1189 assert!(
1190 !obj.contains_key("crv"),
1191 "RSA JWK must not serialize the OKP 'crv' field, got: {}",
1192 value
1193 );
1194 assert!(
1195 !obj.contains_key("x"),
1196 "RSA JWK must not serialize the OKP 'x' field, got: {}",
1197 value
1198 );
1199 }
1200
1201 #[test]
1206 fn test_ed25519_token_rejected_by_wrong_key_jwk() {
1207 let signer = TokenManager::new_ed25519(
1208 TEST_ED25519_PRIVATE_KEY_PEM,
1209 Some("issuer".to_string()),
1210 Some("kid-a".to_string()),
1211 )
1212 .unwrap();
1213 let other = TokenManager::new_ed25519(
1214 TEST_ED25519_PRIVATE_KEY_PEM_B,
1215 Some("issuer".to_string()),
1216 Some("kid-b".to_string()),
1217 )
1218 .unwrap();
1219
1220 let identity = Identity {
1221 provider_id: "mock".to_string(),
1222 external_id: "user123".to_string(),
1223 email: None,
1224 username: None,
1225 attributes: HashMap::new(),
1226 };
1227
1228 let token = signer.issue_user_token(identity, 3600, None, None).unwrap();
1229
1230 assert!(signer.validate_token(&token, None).is_ok());
1232
1233 let err = other.validate_token(&token, None).unwrap_err();
1235 assert!(err.to_string().contains("InvalidSignature"));
1236
1237 let wrong_jwk = other.public_jwk().unwrap();
1240 let decoding_key = wrong_jwk.to_decoding_key().unwrap();
1241 let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::EdDSA);
1242 validation.set_issuer(&["issuer"]);
1243 let err = jsonwebtoken::decode::<Claims>(&token, &decoding_key, &validation).unwrap_err();
1244 assert_eq!(
1245 err.kind(),
1246 &jsonwebtoken::errors::ErrorKind::InvalidSignature
1247 );
1248 }
1249
1250 #[test]
1254 fn test_ed25519_tampered_token_rejected() {
1255 let manager = TokenManager::new_ed25519(
1256 TEST_ED25519_PRIVATE_KEY_PEM,
1257 Some("issuer".to_string()),
1258 Some("my-ed25519-kid".to_string()),
1259 )
1260 .unwrap();
1261
1262 let identity = Identity {
1263 provider_id: "mock".to_string(),
1264 external_id: "user123".to_string(),
1265 email: None,
1266 username: None,
1267 attributes: HashMap::new(),
1268 };
1269
1270 let token = manager
1271 .issue_user_token(identity, 3600, None, None)
1272 .unwrap();
1273
1274 let mut parts: Vec<&str> = token.split('.').collect();
1275 assert_eq!(parts.len(), 3);
1276 let mut payload = parts[1].to_string();
1278 let last = payload.pop().unwrap();
1279 let replacement = if last == 'A' { 'B' } else { 'A' };
1280 payload.push(replacement);
1281 parts[1] = &payload;
1282 let tampered = parts.join(".");
1283
1284 let err = manager.validate_token(&tampered, None).unwrap_err();
1285 assert!(
1286 err.to_string().contains("InvalidSignature") || err.to_string().contains("Json"),
1287 "unexpected error for tampered token: {err}"
1288 );
1289 }
1290
1291 #[test]
1299 fn test_validate_token_accepts_array_aud() {
1300 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
1301
1302 let raw_claims = serde_json::json!({
1303 "iss": "issuer",
1304 "sub": "user123",
1305 "aud": ["client-1", "client-2"],
1306 "exp": (chrono::Utc::now().timestamp() as usize) + 3600,
1307 "iat": chrono::Utc::now().timestamp() as usize,
1308 });
1309 let token = jsonwebtoken::encode(
1310 &jsonwebtoken::Header::new(jsonwebtoken::Algorithm::HS256),
1311 &raw_claims,
1312 &jsonwebtoken::EncodingKey::from_secret(b"secret"),
1313 )
1314 .unwrap();
1315
1316 let claims = manager
1317 .validate_token(&token, None)
1318 .expect("multi-audience subject token must deserialize");
1319 let aud = claims.aud.expect("aud claim must be present");
1320 assert!(aud.contains("client-1"));
1321 assert!(aud.contains("client-2"));
1322 assert!(!aud.contains("client-3"));
1323 }
1324
1325 #[test]
1330 fn test_validate_token_still_accepts_string_aud() {
1331 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
1332 let identity = Identity {
1333 provider_id: "mock".to_string(),
1334 external_id: "user123".to_string(),
1335 email: None,
1336 username: None,
1337 attributes: HashMap::new(),
1338 };
1339
1340 let token = manager
1341 .issue_id_token(identity, "client-1", None, 3600)
1342 .unwrap();
1343
1344 let claims = manager.validate_token(&token, None).unwrap();
1345 let aud = claims.aud.expect("aud claim must be present");
1346 assert!(aud.contains("client-1"));
1347 assert!(!aud.contains("client-2"));
1348 }
1349
1350 #[test]
1355 fn test_audience_single_round_trips_as_bare_string() {
1356 let single = Audience::Single("client-1".to_string());
1357 let serialized = serde_json::to_string(&single).unwrap();
1358 assert_eq!(serialized, "\"client-1\"");
1359
1360 let deserialized: Audience = serde_json::from_str(&serialized).unwrap();
1361 assert_eq!(deserialized, single);
1362
1363 let multiple = Audience::Multiple(vec!["client-1".to_string(), "client-2".to_string()]);
1364 let serialized_multi = serde_json::to_string(&multiple).unwrap();
1365 assert_eq!(serialized_multi, "[\"client-1\",\"client-2\"]");
1366
1367 let deserialized_multi: Audience = serde_json::from_str(&serialized_multi).unwrap();
1368 assert_eq!(deserialized_multi, multiple);
1369 }
1370}
1371pub mod jwk;