1use std::collections::HashMap;
23
24use std::sync::Arc;
25
26use dashmap::DashMap;
27use jsonwebtoken::{Algorithm, DecodingKey, TokenData, Validation, decode, decode_header};
28use serde::{Deserialize, Serialize};
29use zeroize::Zeroizing;
30
31use crate::auth::error::AuthError;
32use crate::auth::oidc::JwksCache;
33
34fn ser_zeroizing_opt<S: serde::Serializer>(
36 val: &Option<Zeroizing<String>>,
37 ser: S,
38) -> Result<S::Ok, S::Error> {
39 match val {
40 Some(z) => ser.serialize_some(z.as_str()),
41 None => ser.serialize_none(),
42 }
43}
44
45fn de_zeroizing_opt<'de, D: serde::Deserializer<'de>>(
47 de: D,
48) -> Result<Option<Zeroizing<String>>, D::Error> {
49 let opt: Option<String> = Option::deserialize(de)?;
50 Ok(opt.map(Zeroizing::new))
51}
52
53fn default_true() -> bool {
55 true
56}
57
58#[derive(Clone, Default, Serialize, Deserialize)]
63pub struct JwtConfig {
64 #[serde(
66 serialize_with = "ser_zeroizing_opt",
67 deserialize_with = "de_zeroizing_opt",
68 default
69 )]
70 pub secret: Option<Zeroizing<String>>,
71 #[serde(
78 serialize_with = "ser_zeroizing_opt",
79 deserialize_with = "de_zeroizing_opt",
80 default
81 )]
82 pub public_key_pem: Option<Zeroizing<String>>,
83 pub issuer: Option<String>,
85 pub audience: Option<String>,
87 #[serde(default = "default_true")]
91 pub require_audience: bool,
92 pub jwks_url: Option<String>,
94 pub subject_claim: Option<String>,
96 pub algorithm: Option<String>,
98 pub role_claim: Option<String>,
101}
102
103impl std::fmt::Debug for JwtConfig {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 f.debug_struct("JwtConfig")
106 .field("secret", &self.secret.as_ref().map(|_| "[REDACTED]"))
107 .field(
108 "public_key_pem",
109 &self.public_key_pem.as_ref().map(|_| "[REDACTED]"),
110 )
111 .field("issuer", &self.issuer)
112 .field("audience", &self.audience)
113 .field("jwks_url", &self.jwks_url)
114 .field("subject_claim", &self.subject_claim)
115 .field("role_claim", &self.role_claim)
116 .field("algorithm", &self.algorithm)
117 .finish()
118 }
119}
120
121#[must_use]
127pub fn resolve_claim_path<'a>(
128 claims: &'a std::collections::HashMap<String, serde_json::Value>,
129 path: &str,
130) -> Option<&'a serde_json::Value> {
131 if !path.contains('.') || path.contains("://") {
132 return claims.get(path);
133 }
134 let segments: Vec<&str> = path.split('.').collect();
135 let mut current: &serde_json::Value = claims.get(*segments.first()?)?;
136 for segment in &segments[1..] {
137 current = current.as_object()?.get(*segment)?;
138 }
139 Some(current)
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct JwtClaims {
145 #[serde(default)]
147 pub sub: Option<String>,
148 #[serde(default)]
150 pub iss: Option<String>,
151 #[serde(default)]
153 pub aud: Option<JwtAudience>,
154 #[serde(default)]
156 pub exp: Option<u64>,
157 #[serde(default)]
159 pub iat: Option<u64>,
160 #[serde(default)]
162 pub email: Option<String>,
163 #[serde(default)]
165 pub jti: Option<String>,
166 #[serde(flatten)]
168 pub extra: HashMap<String, serde_json::Value>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173#[serde(untagged)]
174pub enum JwtAudience {
175 Single(String),
177 Multiple(Vec<String>),
179}
180
181#[derive(Clone)]
187pub struct JwtValidator {
188 config: JwtConfig,
189 decoding_key: Option<DecodingKey>,
190 validation: Validation,
191 jwks_cache: Option<Arc<JwksCache>>,
193 jti_cache: Arc<DashMap<String, u64>>,
197}
198
199impl std::fmt::Debug for JwtValidator {
200 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201 f.debug_struct("JwtValidator")
202 .field("config", &self.config)
203 .field("has_decoding_key", &self.decoding_key.is_some())
204 .field("has_jwks_cache", &self.jwks_cache.is_some())
205 .finish()
206 }
207}
208
209impl JwtValidator {
210 pub fn new(config: JwtConfig) -> Result<Self, AuthError> {
217 let algorithm = match config.algorithm.as_deref() {
218 Some(s) => parse_algorithm(s)?,
219 None => Algorithm::HS256,
220 };
221
222 let mut validation = Validation::new(algorithm);
223
224 if let Some(ref iss) = config.issuer {
225 validation.set_issuer(&[iss]);
226 }
227
228 if let Some(ref aud) = config.audience {
229 validation.set_audience(&[aud]);
230 } else if config.require_audience {
231 return Err(AuthError::Config(
232 "audience is required but not configured; set `audience` or \
233 set `require_audience = false` to opt out"
234 .into(),
235 ));
236 } else {
237 validation.validate_aud = false;
238 }
239
240 validation.set_required_spec_claims(&["exp"]);
242
243 validation.validate_nbf = true;
247
248 let hmac_family = matches!(
265 algorithm,
266 Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512
267 );
268 if hmac_family && config.public_key_pem.is_some() {
269 return Err(AuthError::Config(format!(
270 "{algorithm:?} is an HMAC algorithm and takes `secret`, not `public_key_pem`"
271 )));
272 }
273 if !hmac_family && config.secret.is_some() && config.public_key_pem.is_none() {
274 return Err(AuthError::Config(format!(
275 "{algorithm:?} verifies with a public key; set `public_key_pem` \
276 (a `secret` cannot verify an asymmetric signature)"
277 )));
278 }
279
280 let decoding_key = match (
281 algorithm,
282 config.secret.as_ref(),
283 config.public_key_pem.as_ref(),
284 ) {
285 (_, None, None) => None,
286 (Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512, Some(secret), _) => {
287 if secret.len() < 32 {
289 return Err(AuthError::Config(
290 "HMAC secret must be at least 32 bytes".into(),
291 ));
292 }
293 Some(DecodingKey::from_secret(secret.as_bytes()))
294 }
295 (
296 Algorithm::RS256
297 | Algorithm::RS384
298 | Algorithm::RS512
299 | Algorithm::PS256
300 | Algorithm::PS384
301 | Algorithm::PS512,
302 _,
303 Some(pem),
304 ) => Some(DecodingKey::from_rsa_pem(pem.as_bytes()).map_err(|e| {
305 AuthError::Config(format!(
306 "{algorithm:?} public key is not valid RSA PEM: {e}"
307 ))
308 })?),
309 (Algorithm::ES256 | Algorithm::ES384, _, Some(pem)) => {
310 Some(DecodingKey::from_ec_pem(pem.as_bytes()).map_err(|e| {
311 AuthError::Config(format!("{algorithm:?} public key is not valid EC PEM: {e}"))
312 })?)
313 }
314 (Algorithm::EdDSA, _, Some(pem)) => {
315 Some(DecodingKey::from_ed_pem(pem.as_bytes()).map_err(|e| {
316 AuthError::Config(format!("EdDSA public key is not valid PEM: {e}"))
317 })?)
318 }
319 (other, _, _) => {
320 return Err(AuthError::Config(format!(
321 "JWT algorithm {other:?} is not supported"
322 )));
323 }
324 };
325
326 Ok(Self {
327 config,
328 decoding_key,
329 validation,
330 jwks_cache: None,
331 jti_cache: Arc::new(DashMap::new()),
332 })
333 }
334
335 #[must_use]
340 pub fn with_jwks_cache(mut self, cache: Arc<JwksCache>) -> Self {
341 self.jwks_cache = Some(cache);
342 self
343 }
344
345 #[must_use]
347 pub fn jwks_cache(&self) -> Option<&Arc<JwksCache>> {
348 self.jwks_cache.as_ref()
349 }
350
351 pub fn validate(&self, token: &str) -> Result<JwtClaims, AuthError> {
353 let key = self
354 .decoding_key
355 .as_ref()
356 .ok_or_else(|| AuthError::Config("no decoding key configured".into()))?;
357
358 let token_data: TokenData<JwtClaims> =
359 decode(token, key, &self.validation).map_err(|e| match e.kind() {
360 jsonwebtoken::errors::ErrorKind::ExpiredSignature => AuthError::ExpiredJwt,
361 _ => AuthError::InvalidJwt(e.to_string()),
362 })?;
363
364 self.check_jti_replay(&token_data.claims)?;
366
367 Ok(token_data.claims)
368 }
369
370 fn check_jti_replay(&self, claims: &JwtClaims) -> Result<(), AuthError> {
376 let now = std::time::SystemTime::now()
378 .duration_since(std::time::UNIX_EPOCH)
379 .unwrap_or_default()
380 .as_secs();
381 self.jti_cache.retain(|_, exp| *exp > now);
382
383 if let Some(ref jti) = claims.jti {
384 const MAX_JTI_CACHE: usize = 100_000;
388 if self.jti_cache.len() >= MAX_JTI_CACHE {
389 tracing::error!(
390 cache_size = self.jti_cache.len(),
391 "jti replay cache exhausted — rejecting token (fail-closed)"
392 );
393 return Err(AuthError::InvalidJwt(
394 "replay cache exhausted — try again later".into(),
395 ));
396 }
397 let exp = claims.exp.unwrap_or(now + 3600);
398 if self.jti_cache.insert(jti.clone(), exp).is_some() {
399 return Err(AuthError::InvalidJwt(
400 "token replay detected (duplicate jti)".into(),
401 ));
402 }
403 }
404 Ok(())
405 }
406
407 pub async fn validate_async(&self, token: &str) -> Result<JwtClaims, AuthError> {
415 if self.decoding_key.is_some() {
417 match self.validate(token) {
418 ok @ Ok(_) => return ok,
419 Err(e) => {
420 if self.jwks_cache.is_none() || matches!(e, AuthError::ExpiredJwt) {
423 return Err(e);
424 }
425 tracing::debug!(error = %e, "static key validation failed, falling back to JWKS");
426 }
427 }
428 }
429
430 let cache = self
432 .jwks_cache
433 .as_ref()
434 .ok_or_else(|| AuthError::Config("no decoding key or JWKS cache configured".into()))?;
435
436 let header = decode_header(token)
438 .map_err(|e| AuthError::InvalidJwt(format!("failed to decode JWT header: {e}")))?;
439
440 if let Some(kid) = &header.kid {
441 let cached = cache.get_key(kid).await?;
442 let mut validation = self.validation.clone();
443 validation.algorithms = vec![cached.algorithm];
444 let token_data: TokenData<JwtClaims> = decode(token, &cached.key, &validation)
445 .map_err(|e| match e.kind() {
446 jsonwebtoken::errors::ErrorKind::ExpiredSignature => AuthError::ExpiredJwt,
447 _ => AuthError::InvalidJwt(e.to_string()),
448 })?;
449 return Ok(token_data.claims);
450 }
451
452 let all_keys = cache.get_all_keys().await?;
454 let mut last_err = AuthError::Config("no JWKS keys available".into());
455 for cached in &all_keys {
456 let mut validation = self.validation.clone();
457 validation.algorithms = vec![cached.algorithm];
458 match decode::<JwtClaims>(token, &cached.key, &validation) {
459 Ok(td) => return Ok(td.claims),
460 Err(e) => {
461 match e.kind() {
462 jsonwebtoken::errors::ErrorKind::ExpiredSignature => {
463 return Err(AuthError::ExpiredJwt);
469 }
470 _ => {
471 last_err = AuthError::InvalidJwt(e.to_string());
472 }
473 }
474 }
475 }
476 }
477
478 Err(last_err)
479 }
480
481 #[must_use]
484 pub fn role_claim(&self) -> &str {
485 self.config.role_claim.as_deref().unwrap_or("roles")
486 }
487
488 #[must_use]
500 pub fn extract_roles(&self, claims: &JwtClaims) -> Vec<String> {
501 match resolve_claim_path(&claims.extra, self.role_claim()) {
502 Some(serde_json::Value::Array(items)) => items
503 .iter()
504 .filter_map(|v| v.as_str().map(str::to_string))
505 .collect(),
506 Some(serde_json::Value::String(s)) => vec![s.clone()],
507 _ => Vec::new(),
508 }
509 }
510
511 #[must_use]
513 pub fn extract_principal(&self, claims: &JwtClaims) -> Option<String> {
514 let claim_name = self.config.subject_claim.as_deref().unwrap_or("sub");
515
516 match claim_name {
517 "sub" => claims.sub.clone(),
518 "email" => claims.email.clone(),
519 other => claims
520 .extra
521 .get(other)
522 .and_then(|v| v.as_str())
523 .map(String::from),
524 }
525 }
526}
527
528fn parse_algorithm(s: &str) -> Result<Algorithm, AuthError> {
533 match s.to_uppercase().as_str() {
534 "HS256" => Ok(Algorithm::HS256),
535 "HS384" => Ok(Algorithm::HS384),
536 "HS512" => Ok(Algorithm::HS512),
537 "RS256" => Ok(Algorithm::RS256),
538 "RS384" => Ok(Algorithm::RS384),
539 "RS512" => Ok(Algorithm::RS512),
540 "ES256" => Ok(Algorithm::ES256),
541 "ES384" => Ok(Algorithm::ES384),
542 "PS256" => Ok(Algorithm::PS256),
543 "PS384" => Ok(Algorithm::PS384),
544 "PS512" => Ok(Algorithm::PS512),
545 "EDDSA" => Ok(Algorithm::EdDSA),
546 unknown => Err(AuthError::Config(format!(
547 "unrecognized JWT algorithm: {unknown}"
548 ))),
549 }
550}
551
552#[cfg(test)]
553mod tests {
554 use super::*;
555 use jsonwebtoken::{EncodingKey, Header, encode};
556
557 fn make_token(claims: &JwtClaims, secret: &str) -> String {
558 let header = Header::new(Algorithm::HS256);
559 let key = EncodingKey::from_secret(secret.as_bytes());
560 encode(&header, claims, &key).unwrap()
561 }
562
563 fn future_exp() -> u64 {
564 std::time::SystemTime::now()
565 .duration_since(std::time::UNIX_EPOCH)
566 .unwrap()
567 .as_secs()
568 + 3600
569 }
570
571 #[test]
572 fn reject_short_hmac_secret() {
573 let config = JwtConfig {
574 secret: Some(Zeroizing::new("too-short".into())),
575 ..Default::default()
576 };
577 let err = JwtValidator::new(config).unwrap_err();
578 assert!(matches!(err, AuthError::Config(_)));
579 }
580
581 #[test]
582 fn reject_missing_audience_when_required() {
583 let config = JwtConfig {
584 secret: Some(Zeroizing::new("test-secret-at-least-32-bytes!!!".into())),
585 require_audience: true,
586 audience: None,
587 ..Default::default()
588 };
589 let err = JwtValidator::new(config).unwrap_err();
590 assert!(matches!(err, AuthError::Config(_)));
591 }
592
593 #[test]
594 fn allow_missing_audience_when_not_required() {
595 let config = JwtConfig {
596 secret: Some(Zeroizing::new("test-secret-at-least-32-bytes!!!".into())),
597 require_audience: false,
598 audience: None,
599 ..Default::default()
600 };
601 assert!(JwtValidator::new(config).is_ok());
602 }
603
604 #[test]
605 fn validate_valid_token() {
606 let secret = "test-secret-key-for-unit-tests!!"; let config = JwtConfig {
608 secret: Some(Zeroizing::new(secret.into())),
609 ..Default::default()
610 };
611 let validator = JwtValidator::new(config).unwrap();
612
613 let claims = JwtClaims {
614 sub: Some("user-123".into()),
615 exp: Some(future_exp()),
616 iss: None,
617 aud: None,
618 iat: None,
619 email: None,
620 jti: None,
621 extra: HashMap::new(),
622 };
623
624 let token = make_token(&claims, secret);
625 let validated = validator.validate(&token).unwrap();
626 assert_eq!(validated.sub, Some("user-123".into()));
627 }
628
629 #[test]
630 fn reject_expired_token() {
631 let secret = "test-secret-at-least-32-bytes!!!";
632 let config = JwtConfig {
633 secret: Some(Zeroizing::new(secret.into())),
634 ..Default::default()
635 };
636 let validator = JwtValidator::new(config).unwrap();
637
638 let claims = JwtClaims {
639 sub: Some("user".into()),
640 exp: Some(0), iss: None,
642 aud: None,
643 iat: None,
644 email: None,
645 jti: None,
646 extra: HashMap::new(),
647 };
648
649 let token = make_token(&claims, secret);
650 let err = validator.validate(&token).unwrap_err();
651 assert!(matches!(err, AuthError::ExpiredJwt));
652 }
653
654 #[test]
655 fn reject_wrong_secret() {
656 let config = JwtConfig {
657 secret: Some(Zeroizing::new("correct-secret-at-least-32bytes!".into())),
658 ..Default::default()
659 };
660 let validator = JwtValidator::new(config).unwrap();
661
662 let claims = JwtClaims {
663 sub: Some("user".into()),
664 exp: Some(future_exp()),
665 iss: None,
666 aud: None,
667 iat: None,
668 email: None,
669 jti: None,
670 extra: HashMap::new(),
671 };
672
673 let token = make_token(&claims, "wrong-secret--at-least-32bytes!");
674 let err = validator.validate(&token).unwrap_err();
675 assert!(matches!(err, AuthError::InvalidJwt(_)));
676 }
677
678 #[test]
679 fn validate_with_issuer() {
680 let secret = "test-secret-at-least-32-bytes!!!";
681 let config = JwtConfig {
682 secret: Some(Zeroizing::new(secret.into())),
683 issuer: Some("https://auth.example.com".into()),
684 ..Default::default()
685 };
686 let validator = JwtValidator::new(config).unwrap();
687
688 let claims = JwtClaims {
689 sub: Some("user".into()),
690 iss: Some("https://auth.example.com".into()),
691 exp: Some(future_exp()),
692 aud: None,
693 iat: None,
694 email: None,
695 jti: None,
696 extra: HashMap::new(),
697 };
698
699 let token = make_token(&claims, secret);
700 validator.validate(&token).unwrap();
701 }
702
703 #[test]
704 fn reject_wrong_issuer() {
705 let secret = "test-secret-at-least-32-bytes!!!";
706 let config = JwtConfig {
707 secret: Some(Zeroizing::new(secret.into())),
708 issuer: Some("https://auth.example.com".into()),
709 ..Default::default()
710 };
711 let validator = JwtValidator::new(config).unwrap();
712
713 let claims = JwtClaims {
714 sub: Some("user".into()),
715 iss: Some("https://evil.com".into()),
716 exp: Some(future_exp()),
717 aud: None,
718 iat: None,
719 email: None,
720 jti: None,
721 extra: HashMap::new(),
722 };
723
724 let token = make_token(&claims, secret);
725 assert!(validator.validate(&token).is_err());
726 }
727
728 #[test]
729 fn extract_principal_from_sub() {
730 let config = JwtConfig::default();
731 let validator = JwtValidator::new(config).unwrap();
732
733 let claims = JwtClaims {
734 sub: Some("principal-1".into()),
735 email: Some("user@example.com".into()),
736 exp: None,
737 iss: None,
738 aud: None,
739 iat: None,
740 jti: None,
741 extra: HashMap::new(),
742 };
743
744 assert_eq!(
745 validator.extract_principal(&claims),
746 Some("principal-1".into())
747 );
748 }
749
750 #[test]
751 fn extract_principal_from_email() {
752 let config = JwtConfig {
753 subject_claim: Some("email".into()),
754 ..Default::default()
755 };
756 let validator = JwtValidator::new(config).unwrap();
757
758 let claims = JwtClaims {
759 sub: Some("user-id".into()),
760 email: Some("admin@chronix.io".into()),
761 exp: None,
762 iss: None,
763 aud: None,
764 iat: None,
765 jti: None,
766 extra: HashMap::new(),
767 };
768
769 assert_eq!(
770 validator.extract_principal(&claims),
771 Some("admin@chronix.io".into())
772 );
773 }
774
775 #[test]
776 fn extract_principal_from_custom_claim() {
777 let config = JwtConfig {
778 subject_claim: Some("username".into()),
779 ..Default::default()
780 };
781 let validator = JwtValidator::new(config).unwrap();
782
783 let mut extra = HashMap::new();
784 extra.insert("username".into(), serde_json::json!("admin"));
785
786 let claims = JwtClaims {
787 sub: None,
788 email: None,
789 exp: None,
790 iss: None,
791 aud: None,
792 iat: None,
793 jti: None,
794 extra,
795 };
796
797 assert_eq!(validator.extract_principal(&claims), Some("admin".into()));
798 }
799
800 fn make_token_with_kid(claims: &JwtClaims, secret: &str, kid: &str) -> String {
805 let mut header = Header::new(Algorithm::HS256);
806 header.kid = Some(kid.to_string());
807 let key = EncodingKey::from_secret(secret.as_bytes());
808 encode(&header, claims, &key).unwrap()
809 }
810
811 #[tokio::test]
812 async fn validate_async_with_static_key() {
813 let secret = "static-secret-at-least-32-bytes!";
814 let config = JwtConfig {
815 secret: Some(Zeroizing::new(secret.into())),
816 ..Default::default()
817 };
818 let validator = JwtValidator::new(config).unwrap();
819
820 let claims = JwtClaims {
821 sub: Some("user-static".into()),
822 exp: Some(future_exp()),
823 iss: None,
824 aud: None,
825 iat: None,
826 email: None,
827 jti: None,
828 extra: HashMap::new(),
829 };
830
831 let token = make_token(&claims, secret);
832 let validated = validator.validate_async(&token).await.unwrap();
833 assert_eq!(validated.sub, Some("user-static".into()));
834 }
835
836 #[tokio::test]
837 async fn validate_async_with_jwks_cache() {
838 let secret = "jwks-secret-at-least-32-bytes!!!";
839
840 let mut keys = std::collections::HashMap::new();
842 keys.insert(
843 "key-1".to_string(),
844 crate::auth::oidc::CachedKey {
845 key: DecodingKey::from_secret(secret.as_bytes()),
846 algorithm: Algorithm::HS256,
847 },
848 );
849 let cache = Arc::new(crate::auth::oidc::JwksCache::from_keys(keys));
850
851 let config = JwtConfig::default();
853 let validator = JwtValidator::new(config).unwrap().with_jwks_cache(cache);
854
855 let claims = JwtClaims {
856 sub: Some("oidc-user".into()),
857 exp: Some(future_exp()),
858 iss: None,
859 aud: None,
860 iat: None,
861 email: None,
862 jti: None,
863 extra: HashMap::new(),
864 };
865
866 let token = make_token_with_kid(&claims, secret, "key-1");
867 let validated = validator.validate_async(&token).await.unwrap();
868 assert_eq!(validated.sub, Some("oidc-user".into()));
869 }
870
871 #[tokio::test]
872 async fn validate_async_jwks_no_kid_tries_all() {
873 let secret = "no-kid-secret-at-least-32bytes!!";
874
875 let mut keys = std::collections::HashMap::new();
876 keys.insert(
877 "default".to_string(),
878 crate::auth::oidc::CachedKey {
879 key: DecodingKey::from_secret(secret.as_bytes()),
880 algorithm: Algorithm::HS256,
881 },
882 );
883 let cache = Arc::new(crate::auth::oidc::JwksCache::from_keys(keys));
884
885 let config = JwtConfig::default();
886 let validator = JwtValidator::new(config).unwrap().with_jwks_cache(cache);
887
888 let claims = JwtClaims {
889 sub: Some("anon".into()),
890 exp: Some(future_exp()),
891 iss: None,
892 aud: None,
893 iat: None,
894 email: None,
895 jti: None,
896 extra: HashMap::new(),
897 };
898
899 let token = make_token(&claims, secret);
901 let validated = validator.validate_async(&token).await.unwrap();
902 assert_eq!(validated.sub, Some("anon".into()));
903 }
904
905 #[tokio::test]
906 async fn validate_async_jwks_wrong_kid_fails() {
907 let secret = "wrong-kid-secret-at-least-32byte";
908
909 let mut keys = std::collections::HashMap::new();
910 keys.insert(
911 "real-kid".to_string(),
912 crate::auth::oidc::CachedKey {
913 key: DecodingKey::from_secret(secret.as_bytes()),
914 algorithm: Algorithm::HS256,
915 },
916 );
917 let cache = Arc::new(crate::auth::oidc::JwksCache::from_keys(keys));
918
919 let config = JwtConfig::default();
920 let validator = JwtValidator::new(config).unwrap().with_jwks_cache(cache);
921
922 let claims = JwtClaims {
923 sub: Some("user".into()),
924 exp: Some(future_exp()),
925 iss: None,
926 aud: None,
927 iat: None,
928 email: None,
929 jti: None,
930 extra: HashMap::new(),
931 };
932
933 let token = make_token_with_kid(&claims, secret, "unknown-kid");
934 let err = validator.validate_async(&token).await.unwrap_err();
935 assert!(matches!(err, AuthError::Config(_)));
936 }
937
938 #[tokio::test]
939 async fn validate_async_no_key_no_cache_fails() {
940 let config = JwtConfig::default();
941 let validator = JwtValidator::new(config).unwrap();
942
943 let claims = JwtClaims {
944 sub: Some("user".into()),
945 exp: Some(future_exp()),
946 iss: None,
947 aud: None,
948 iat: None,
949 email: None,
950 jti: None,
951 extra: HashMap::new(),
952 };
953
954 let token = make_token(&claims, "any-secret");
955 let err = validator.validate_async(&token).await.unwrap_err();
956 assert!(matches!(err, AuthError::Config(_)));
957 }
958}
959
960#[cfg(test)]
961mod algorithm_family_tests {
962 use super::*;
963 use jsonwebtoken::{EncodingKey, Header, encode};
964 use std::collections::HashMap;
965 use zeroize::Zeroizing;
966
967 const EC_PRIVATE_PEM: &str = "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgftsCo6PP58Bktonn\nIQQdJB0xhCZr5vaXReMirLL6vfmhRANCAATI63Cp8AxIz8oL4Sfzwp/1r5j8o5v5\nQ2O3+N41eKFkFT/bk1oF8zP6Zz6OaOU7nfw77Pcn+DZz9GFp3ISD9rJK\n-----END PRIVATE KEY-----";
970 const EC_PUBLIC_PEM: &str = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyOtwqfAMSM/KC+En88Kf9a+Y/KOb\n+UNjt/jeNXihZBU/25NaBfMz+mc+jmjlO538O+z3J/g2c/RhadyEg/aySg==\n-----END PUBLIC KEY-----";
971
972 fn claims() -> JwtClaims {
973 JwtClaims {
974 sub: Some("ec-user".into()),
975 exp: Some(
976 std::time::SystemTime::now()
977 .duration_since(std::time::UNIX_EPOCH)
978 .unwrap()
979 .as_secs()
980 + 3600,
981 ),
982 iss: None,
983 aud: None,
984 iat: None,
985 email: None,
986 jti: None,
987 extra: HashMap::new(),
988 }
989 }
990
991 #[test]
995 fn es256_verifies_a_token_signed_with_its_key() {
996 let config = JwtConfig {
997 algorithm: Some("ES256".into()),
998 public_key_pem: Some(Zeroizing::new(EC_PUBLIC_PEM.to_string())),
999 ..Default::default()
1000 };
1001 let validator = JwtValidator::new(config).expect("ES256 config must build");
1002
1003 let token = encode(
1004 &Header::new(Algorithm::ES256),
1005 &claims(),
1006 &EncodingKey::from_ec_pem(EC_PRIVATE_PEM.as_bytes()).unwrap(),
1007 )
1008 .unwrap();
1009
1010 let validated = validator.validate(&token).expect("a valid ES256 token");
1011 assert_eq!(validated.sub, Some("ec-user".into()));
1012 }
1013
1014 #[test]
1018 fn an_asymmetric_algorithm_with_a_secret_is_a_startup_error() {
1019 let config = JwtConfig {
1020 algorithm: Some("RS256".into()),
1021 secret: Some(Zeroizing::new("a-secret-that-is-at-least-32-bytes!".into())),
1022 ..Default::default()
1023 };
1024 let err = JwtValidator::new(config).expect_err("a secret cannot verify an RSA signature");
1025 assert!(
1026 err.to_string().contains("public key"),
1027 "the error must say what to set instead: {err}"
1028 );
1029 }
1030
1031 #[test]
1034 fn an_hmac_algorithm_with_a_public_key_is_a_startup_error() {
1035 let config = JwtConfig {
1036 algorithm: Some("HS256".into()),
1037 public_key_pem: Some(Zeroizing::new(EC_PUBLIC_PEM.to_string())),
1038 ..Default::default()
1039 };
1040 assert!(JwtValidator::new(config).is_err());
1041 }
1042
1043 #[test]
1046 fn no_key_material_is_allowed_for_jwks() {
1047 let config = JwtConfig {
1048 algorithm: Some("RS256".into()),
1049 ..Default::default()
1050 };
1051 assert!(JwtValidator::new(config).is_ok());
1052 }
1053}