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
7#[derive(Debug, Serialize, Deserialize, Clone)]
8pub struct Claims {
9 pub iss: Option<String>,
11 pub sub: String,
12 pub aud: Option<String>,
13 pub exp: usize,
14 pub iat: usize,
15 pub nbf: Option<usize>,
16 pub jti: Option<String>,
17
18 pub scope: Option<String>,
20 #[serde(skip_serializing_if = "Option::is_none")]
23 pub identity: Option<Identity>,
24
25 #[serde(flatten)]
27 pub extra: HashMap<String, serde_json::Value>,
28}
29
30#[derive(Clone)]
31pub struct TokenManager {
32 encoding_key: EncodingKey,
33 decoding_key: DecodingKey,
34 issuer: Option<String>,
35 kid: Option<String>,
36 alg: Algorithm,
37 public_jwk: Option<crate::token::jwk::Jwk>,
38}
39
40impl TokenManager {
41 pub fn new(secret: &[u8], issuer: Option<String>) -> Self {
43 Self {
44 encoding_key: EncodingKey::from_secret(secret),
45 decoding_key: DecodingKey::from_secret(secret),
46 issuer,
47 kid: None,
48 alg: Algorithm::HS256,
49 public_jwk: None,
50 }
51 }
52
53 pub fn new_asymmetric(
58 private_key_pem: &[u8],
59 issuer: Option<String>,
60 kid: Option<String>,
61 ) -> Result<Self, AuthError> {
62 let encoding_key = EncodingKey::from_rsa_pem(private_key_pem)
63 .map_err(|e| AuthError::Token(e.to_string()))?;
64
65 let pem_str = std::str::from_utf8(private_key_pem)
66 .map_err(|_| AuthError::Token("Invalid PEM UTF-8".into()))?;
67
68 use rsa::pkcs1::DecodeRsaPrivateKey;
69 use rsa::pkcs8::DecodePrivateKey;
70 let rsa_key = rsa::RsaPrivateKey::from_pkcs8_pem(pem_str)
71 .or_else(|_| rsa::RsaPrivateKey::from_pkcs1_pem(pem_str))
72 .map_err(|e| AuthError::Token(format!("Failed to parse RSA key: {}", e)))?;
73
74 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
75 use rsa::traits::PublicKeyParts;
76
77 let n = URL_SAFE_NO_PAD.encode(rsa_key.n().to_bytes_be());
78 let e = URL_SAFE_NO_PAD.encode(rsa_key.e().to_bytes_be());
79
80 let kid_val = kid.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
81
82 let jwk = crate::token::jwk::Jwk {
83 kid: Some(kid_val.clone()),
84 kty: "RSA".to_string(),
85 alg: Some("RS256".to_string()),
86 n: Some(n),
87 e: Some(e),
88 };
89
90 let decoding_key = jwk.to_decoding_key()?;
95
96 Ok(Self {
97 encoding_key,
98 decoding_key,
99 issuer,
100 kid: Some(kid_val),
101 alg: Algorithm::RS256,
102 public_jwk: Some(jwk),
103 })
104 }
105
106 pub fn public_jwk(&self) -> Option<crate::token::jwk::Jwk> {
107 self.public_jwk.clone()
108 }
109
110 pub fn with_issuer(mut self, issuer: String) -> Self {
111 self.issuer = Some(issuer);
112 self
113 }
114
115 pub fn issue_user_token(
117 &self,
118 identity: Identity,
119 expires_in_secs: u64,
120 scope: Option<String>,
121 aud: Option<String>,
122 ) -> Result<String, AuthError> {
123 self.issue_user_token_with_extra(identity, expires_in_secs, scope, aud, HashMap::new())
124 }
125
126 pub fn issue_user_token_with_extra(
138 &self,
139 identity: Identity,
140 expires_in_secs: u64,
141 scope: Option<String>,
142 aud: Option<String>,
143 extra: HashMap<String, serde_json::Value>,
144 ) -> Result<String, AuthError> {
145 let now = chrono::Utc::now().timestamp() as usize;
146 let expiration = now + expires_in_secs as usize;
147
148 let claims = Claims {
149 iss: self.issuer.clone(),
150 sub: identity.external_id.clone(),
151 aud,
152 exp: expiration,
153 iat: now,
154 nbf: Some(now),
155 jti: Some(uuid::Uuid::new_v4().to_string()),
156 scope,
157 identity: Some(identity),
158 extra,
159 };
160
161 let mut header = Header::new(self.alg);
162 if let Some(ref kid) = self.kid {
163 header.kid = Some(kid.clone());
164 }
165
166 encode(&header, &claims, &self.encoding_key).map_err(|e| AuthError::Token(e.to_string()))
167 }
168
169 pub fn issue_id_token(
171 &self,
172 identity: Identity,
173 client_id: &str,
174 nonce: Option<String>,
175 expires_in_secs: u64,
176 ) -> Result<String, AuthError> {
177 self.issue_id_token_with_extra(identity, client_id, nonce, expires_in_secs, HashMap::new())
178 }
179
180 pub fn issue_id_token_with_extra(
191 &self,
192 identity: Identity,
193 client_id: &str,
194 nonce: Option<String>,
195 expires_in_secs: u64,
196 extra: HashMap<String, serde_json::Value>,
197 ) -> Result<String, AuthError> {
198 let now = chrono::Utc::now().timestamp() as usize;
199 let expiration = now + expires_in_secs as usize;
200
201 let mut claims = Claims {
202 iss: self.issuer.clone(),
203 sub: identity.external_id.clone(),
204 aud: Some(client_id.to_string()),
205 exp: expiration,
206 iat: now,
207 nbf: Some(now),
208 jti: Some(uuid::Uuid::new_v4().to_string()),
209 scope: None,
210 identity: Some(identity),
211 extra,
212 };
213
214 if let Some(n) = nonce {
215 claims
216 .extra
217 .insert("nonce".to_string(), serde_json::Value::String(n));
218 }
219
220 let mut header = Header::new(self.alg);
221 if let Some(ref kid) = self.kid {
222 header.kid = Some(kid.clone());
223 }
224
225 encode(&header, &claims, &self.encoding_key).map_err(|e| AuthError::Token(e.to_string()))
226 }
227
228 pub fn issue_client_token(
230 &self,
231 client_id: &str,
232 expires_in_secs: u64,
233 scope: Option<String>,
234 aud: Option<String>,
235 ) -> Result<String, AuthError> {
236 self.issue_client_token_with_extra(client_id, expires_in_secs, scope, aud, HashMap::new())
237 }
238
239 pub fn issue_client_token_with_extra(
243 &self,
244 client_id: &str,
245 expires_in_secs: u64,
246 scope: Option<String>,
247 aud: Option<String>,
248 extra: HashMap<String, serde_json::Value>,
249 ) -> Result<String, AuthError> {
250 let now = chrono::Utc::now().timestamp() as usize;
251 let expiration = now + expires_in_secs as usize;
252
253 let claims = Claims {
254 iss: self.issuer.clone(),
255 sub: client_id.to_string(),
256 aud,
257 exp: expiration,
258 iat: now,
259 nbf: Some(now),
260 jti: Some(uuid::Uuid::new_v4().to_string()),
261 scope,
262 identity: None,
263 extra,
264 };
265
266 let mut header = Header::new(self.alg);
267 if let Some(ref kid) = self.kid {
268 header.kid = Some(kid.clone());
269 }
270
271 encode(&header, &claims, &self.encoding_key).map_err(|e| AuthError::Token(e.to_string()))
272 }
273
274 pub fn issue_custom_token(
282 &self,
283 sub: String,
284 expires_in_secs: u64,
285 typ: &str,
286 extra: HashMap<String, serde_json::Value>,
287 ) -> Result<String, AuthError> {
288 let now = chrono::Utc::now().timestamp() as usize;
289 let claims = Claims {
290 iss: self.issuer.clone(),
291 sub,
292 aud: None,
293 exp: now + expires_in_secs as usize,
294 iat: now,
295 nbf: Some(now),
296 jti: Some(uuid::Uuid::new_v4().to_string()),
297 scope: None,
298 identity: None,
299 extra,
300 };
301
302 let mut header = Header::new(self.alg);
303 header.typ = Some(typ.to_string());
304 if let Some(ref kid) = self.kid {
305 header.kid = Some(kid.clone());
306 }
307
308 encode(&header, &claims, &self.encoding_key).map_err(|e| AuthError::Token(e.to_string()))
309 }
310
311 pub fn validate_token(
312 &self,
313 token: &str,
314 expected_aud: Option<&str>,
315 ) -> Result<Claims, AuthError> {
316 let mut validation = Validation::new(self.alg);
317 if let Some(aud) = expected_aud {
318 validation.set_audience(&[aud]);
319 } else {
320 validation.validate_aud = false;
321 }
322 if let Some(ref iss) = self.issuer {
323 validation.set_issuer(&[iss]);
324 }
325
326 let token_data = decode::<Claims>(token, &self.decoding_key, &validation)
327 .map_err(|e| AuthError::Token(e.to_string()))?;
328
329 Ok(token_data.claims)
330 }
331}
332
333#[cfg(test)]
334mod tests {
335
336 use super::*;
337 use crate::auth::state::Identity;
338 use std::collections::HashMap;
339
340 const TEST_RSA_PRIVATE_KEY_PEM: &[u8] = b"-----BEGIN PRIVATE KEY-----
342MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDA5hJIcQ+2rxMz
343VM8ZH5WAmguCr0xmNDAdy0IzzsUeFLG7BebB7izOkU36J4t8t5tUaQwrBMnx2Fvt
344VqJjbdE242UDpvWF/8m9zJ2HR5298cbwT5cGMKLB0HWzDMahugs+Bbh2lCgwyLZk
345Tr3Diwxp5SwFew/Wb+Ke9cNG9Hu5IFH3BCuJ839d9hfqisIeYrBPfb52xxckM37R
3467zSGu/eDP/HZAeLkQuptZJW4A3u7xni14u4qyqXDqsHsYFNgJaxMSAwWgBRY6HNu
347TnvBArTXCiVfL+F73B2L6mdYr64g+QS9nK9v97MlJu/E3mSduz54pren4mpCHc9m
348/S2+VjCZAgMBAAECggEAASC9qQbGnL7XuExRDOIn/m4bWx92ehjo0lCTibhpY3LW
349umbSbpfbhmmuSj3CjW9VZsaM3hBTgSjoTX72lbY/eIUXD7c0memUK5pV4XcEIrQw
350AZlPIye6ckx4I7ZGnKasO8FoAel9dd7DXw36AuBK3LBzJwtzkEFsBc0e3/wixqmG
351UJBbbt/+5ya7CxyjuePaQhKtkLD5R6DpvN2XnCYq5nHJNJdvSVg1pOzsTHYIf+Ee
3522Rz42fGsfFKqeEQCcBFRZaGb/ELeP4c6UZdktZAvmHb1p1fursVZc6X9JXmiJ2OJ
353Kv2H2tMKuysP8L0fXFOMgkH2SVt6rcdHkO6xhlhWsQKBgQDqR8rAJeEE5BFoXA8T
354VVW6CLMlW51x4ey7PEGOaYh39dTG2Q+GZQBZ9G+SZk3f5Y85UCACSyc//4qaz/c3
3550nWsegZ+JPyymmuc79wzIAFFvXB7pL6wyn0Ed1P620kOZTtA8iBcXrsuxL+KP7iu
356MXfWmU1QiZpbndILtyDnY+70uwKBgQDSyCljWkydQCaPU+fiAXLxP8CvcJTSSNQD
357mVUlwJ+OpHnU+Alsi1rBavMgUtLlYbFqzH7NmYrLC8Yadq3ZOwLt0VEK0r8qstAL
3587QCDUD2WNuQjpZupRnXuMUl3iXB96i2gb+VQKGuUAJvVWjdIbYa4+Gu+sBMfcDcX
359dBihDLuEuwKBgAgX4tEwfc2Fc3R/eaXZVNTQaB/qQk4k1+C//CPHUYeTXn5gEUE7
360S//PiesszZPmgkQgmHp7zidP1KH0fT3Yb2g97ut8q54f54fMYXcCrAiUusYKsuu4
361kwkMdkI8QRHWPW3I74VBYIYFFfjYqrCZ1OH8+cbGeiagFRmCggh8U0zxAoGAVW3u
3626Ge22Z0gg8LcHsu7jG/sZq7Ygool8/d3fT+e669Z+ak2GJo6hF4WgClRdMqtn72W
363PzpV+ImjFyK2v26dd0n48MwN0v56N/ss1Av3iiRhPtlmR6tZLNspDZvUzhPVvkrb
364xCs9vtSoVEamVWKe0eVNthGjDoDqs0TInq2MavUCgYB6REavSJs/CLkSS7iimjxZ
365G7g5YQi9/p1lXLOEUDiwEmvRr0XTwzzxUsIc535IXhh/ZUYpthenW+qBBzn85pEC
366TowIqciHu5redqlQ8rITA8/AOY98vaDIhppDg1rfpnHHaZHFbXD/keYAEbhBtbvf
367a0QMqKUcs8+YTy5R5K6qtw==
368-----END PRIVATE KEY-----";
369
370 #[test]
371 fn test_claims_serialization() {
372 let mut extra = HashMap::new();
373 extra.insert(
374 "custom".to_string(),
375 serde_json::Value::String("value".to_string()),
376 );
377
378 let claims = Claims {
379 iss: Some("issuer".to_string()),
380 sub: "user123".to_string(),
381 aud: Some("audience".to_string()),
382 exp: 1000,
383 iat: 500,
384 nbf: Some(500),
385 jti: Some("jti".to_string()),
386 scope: Some("openid profile".to_string()),
387 identity: Some(Identity {
388 provider_id: "google".to_string(),
389 external_id: "user123".to_string(),
390 email: Some("user@example.com".to_string()),
391 username: Some("user".to_string()),
392 attributes: HashMap::new(),
393 }),
394 extra,
395 };
396
397 let serialized = serde_json::to_string(&claims).unwrap();
398 let deserialized: Claims = serde_json::from_str(&serialized).unwrap();
399
400 assert_eq!(deserialized.iss, claims.iss);
401 assert_eq!(deserialized.sub, claims.sub);
402 assert_eq!(deserialized.extra.get("custom").unwrap(), "value");
403 }
404
405 #[test]
406 fn test_token_manager_issuance() {
407 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
408 let identity = Identity {
409 provider_id: "mock".to_string(),
410 external_id: "user123".to_string(),
411 email: None,
412 username: None,
413 attributes: HashMap::new(),
414 };
415
416 let token = manager
417 .issue_user_token(identity, 3600, None, None)
418 .unwrap();
419 let claims = manager.validate_token(&token, None).unwrap();
420
421 assert_eq!(claims.iss, Some("issuer".to_string()));
422 assert_eq!(claims.sub, "user123");
423 assert!(claims.jti.is_some());
424 assert!(claims.nbf.is_some());
425 }
426
427 #[test]
428 fn test_token_manager_asymmetric_issuance() {
429 let manager = TokenManager::new_asymmetric(
430 TEST_RSA_PRIVATE_KEY_PEM,
431 Some("issuer".to_string()),
432 Some("my-kid-123".to_string()),
433 )
434 .unwrap();
435
436 let identity = Identity {
437 provider_id: "mock".to_string(),
438 external_id: "user123".to_string(),
439 email: None,
440 username: None,
441 attributes: HashMap::new(),
442 };
443
444 let token = manager
445 .issue_user_token(identity, 3600, None, None)
446 .unwrap();
447
448 let jwk = manager.public_jwk().unwrap();
450 assert_eq!(jwk.kid.as_deref(), Some("my-kid-123"));
451
452 let decoding_key = jwk.to_decoding_key().unwrap();
453 let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::RS256);
454 validation.set_issuer(&["issuer"]);
455
456 let token_data =
457 jsonwebtoken::decode::<Claims>(&token, &decoding_key, &validation).unwrap();
458 assert_eq!(token_data.claims.sub, "user123");
459 assert_eq!(token_data.header.kid.as_deref(), Some("my-kid-123"));
460 }
461
462 #[test]
471 fn test_asymmetric_manager_validates_its_own_tokens() {
472 let manager = TokenManager::new_asymmetric(
473 TEST_RSA_PRIVATE_KEY_PEM,
474 Some("issuer".to_string()),
475 Some("my-kid-123".to_string()),
476 )
477 .unwrap();
478
479 let identity = Identity {
480 provider_id: "mock".to_string(),
481 external_id: "user123".to_string(),
482 email: None,
483 username: None,
484 attributes: HashMap::new(),
485 };
486
487 let token = manager
488 .issue_user_token(identity, 3600, None, None)
489 .unwrap();
490
491 let claims = manager.validate_token(&token, None).unwrap();
492 assert_eq!(claims.sub, "user123");
493 assert_eq!(claims.iss, Some("issuer".to_string()));
494
495 let attestation = manager
497 .issue_custom_token(
498 "device-1".to_string(),
499 60,
500 "webank-attest+jws",
501 HashMap::new(),
502 )
503 .unwrap();
504
505 let attest_claims = manager.validate_token(&attestation, None).unwrap();
506 assert_eq!(attest_claims.sub, "device-1");
507 }
508
509 #[test]
510 fn test_issue_id_token() {
511 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
512 let identity = Identity {
513 provider_id: "mock".to_string(),
514 external_id: "user123".to_string(),
515 email: None,
516 username: None,
517 attributes: HashMap::new(),
518 };
519
520 let token = manager
521 .issue_id_token(identity, "client-1", Some("nonce123".to_string()), 3600)
522 .unwrap();
523
524 let claims = manager.validate_token(&token, None).unwrap();
525
526 assert_eq!(claims.iss, Some("issuer".to_string()));
527 assert_eq!(claims.sub, "user123");
528 assert_eq!(claims.aud, Some("client-1".to_string()));
529 assert_eq!(claims.extra.get("nonce").unwrap(), "nonce123");
530 }
531 #[test]
532 fn test_token_manager_audience_validation() {
533 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
534 let identity = Identity {
535 provider_id: "mock".to_string(),
536 external_id: "user123".to_string(),
537 email: None,
538 username: None,
539 attributes: HashMap::new(),
540 };
541
542 let token = manager
544 .issue_id_token(identity, "client-1", None, 3600)
545 .unwrap();
546
547 let claims = manager.validate_token(&token, Some("client-1")).unwrap();
549 assert_eq!(claims.aud, Some("client-1".to_string()));
550
551 let err = manager
553 .validate_token(&token, Some("client-2"))
554 .unwrap_err();
555 assert!(err.to_string().contains("InvalidAudience"));
556 }
557
558 #[test]
559 fn test_issue_custom_token_sets_typ_and_no_aud() {
560 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
561
562 let mut extra = HashMap::new();
563 extra.insert("cnf".to_string(), serde_json::json!({"jkt": "abc"}));
564
565 let token = manager
566 .issue_custom_token("device-1".to_string(), 60, "webank-attest+jws", extra)
567 .unwrap();
568
569 let header = jsonwebtoken::decode_header(&token).unwrap();
570 assert_eq!(header.typ.as_deref(), Some("webank-attest+jws"));
571
572 let claims = manager.validate_token(&token, None).unwrap();
573 assert_eq!(claims.sub, "device-1");
574 assert_eq!(claims.aud, None);
575 assert_eq!(claims.extra.get("cnf").unwrap()["jkt"], "abc");
576 }
577
578 #[test]
579 fn test_issue_user_token_with_extra_round_trips_custom_claims() {
580 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
581 let identity = Identity {
582 provider_id: "mock".to_string(),
583 external_id: "user123".to_string(),
584 email: None,
585 username: None,
586 attributes: HashMap::new(),
587 };
588
589 let mut extra = HashMap::new();
590 extra.insert("api_key_id".to_string(), serde_json::json!("key-abc"));
591 extra.insert("project_id".to_string(), serde_json::json!("proj-42"));
592
593 let token = manager
594 .issue_user_token_with_extra(identity, 3600, None, None, extra)
595 .unwrap();
596 let claims = manager.validate_token(&token, None).unwrap();
597
598 assert_eq!(
599 claims.extra.get("api_key_id"),
600 Some(&serde_json::json!("key-abc"))
601 );
602 assert_eq!(
603 claims.extra.get("project_id"),
604 Some(&serde_json::json!("proj-42"))
605 );
606 }
607
608 #[test]
609 fn test_issue_user_token_wrapper_still_has_empty_extra() {
610 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
611 let identity = Identity {
612 provider_id: "mock".to_string(),
613 external_id: "user123".to_string(),
614 email: None,
615 username: None,
616 attributes: HashMap::new(),
617 };
618
619 let token = manager
620 .issue_user_token(identity, 3600, None, None)
621 .unwrap();
622 let claims = manager.validate_token(&token, None).unwrap();
623
624 assert!(claims.extra.is_empty());
625 }
626
627 #[test]
628 fn test_issue_client_token_with_extra_round_trips_custom_claims() {
629 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
630
631 let mut extra = HashMap::new();
632 extra.insert("roles".to_string(), serde_json::json!(["admin", "billing"]));
633
634 let token = manager
635 .issue_client_token_with_extra("client-1", 3600, None, None, extra)
636 .unwrap();
637 let claims = manager.validate_token(&token, None).unwrap();
638
639 assert_eq!(
640 claims.extra.get("roles"),
641 Some(&serde_json::json!(["admin", "billing"]))
642 );
643 }
644
645 #[test]
646 fn test_issue_client_token_wrapper_still_has_empty_extra() {
647 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
648
649 let token = manager
650 .issue_client_token("client-1", 3600, None, None)
651 .unwrap();
652 let claims = manager.validate_token(&token, None).unwrap();
653
654 assert!(claims.extra.is_empty());
655 }
656
657 #[test]
658 fn test_issue_id_token_with_extra_round_trips_custom_claims() {
659 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
660 let identity = Identity {
661 provider_id: "mock".to_string(),
662 external_id: "user123".to_string(),
663 email: None,
664 username: None,
665 attributes: HashMap::new(),
666 };
667
668 let mut extra = HashMap::new();
669 extra.insert("org_id".to_string(), serde_json::json!("org-7"));
670
671 let token = manager
672 .issue_id_token_with_extra(
673 identity,
674 "client-1",
675 Some("nonce123".to_string()),
676 3600,
677 extra,
678 )
679 .unwrap();
680 let claims = manager.validate_token(&token, None).unwrap();
681
682 assert_eq!(
683 claims.extra.get("org_id"),
684 Some(&serde_json::json!("org-7"))
685 );
686 assert_eq!(
687 claims.extra.get("nonce"),
688 Some(&serde_json::json!("nonce123"))
689 );
690 }
691
692 #[test]
693 fn test_issue_id_token_with_extra_explicit_nonce_wins_over_extra_nonce() {
694 let manager = TokenManager::new(b"secret", Some("issuer".to_string()));
698 let identity = Identity {
699 provider_id: "mock".to_string(),
700 external_id: "user123".to_string(),
701 email: None,
702 username: None,
703 attributes: HashMap::new(),
704 };
705
706 let mut extra = HashMap::new();
707 extra.insert(
708 "nonce".to_string(),
709 serde_json::json!("attacker-supplied-nonce"),
710 );
711
712 let token = manager
713 .issue_id_token_with_extra(
714 identity,
715 "client-1",
716 Some("real-nonce".to_string()),
717 3600,
718 extra,
719 )
720 .unwrap();
721 let claims = manager.validate_token(&token, None).unwrap();
722
723 assert_eq!(
724 claims.extra.get("nonce"),
725 Some(&serde_json::json!("real-nonce"))
726 );
727 }
728}
729pub mod jwk;