1use std::collections::HashMap;
23
24use std::sync::Arc;
25
26use dashmap::DashMap;
27use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, TokenData, Validation};
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#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct JwtClaims {
124 #[serde(default)]
126 pub sub: Option<String>,
127 #[serde(default)]
129 pub iss: Option<String>,
130 #[serde(default)]
132 pub aud: Option<JwtAudience>,
133 #[serde(default)]
135 pub exp: Option<u64>,
136 #[serde(default)]
138 pub iat: Option<u64>,
139 #[serde(default)]
141 pub email: Option<String>,
142 #[serde(default)]
144 pub jti: Option<String>,
145 #[serde(flatten)]
147 pub extra: HashMap<String, serde_json::Value>,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
152#[serde(untagged)]
153pub enum JwtAudience {
154 Single(String),
156 Multiple(Vec<String>),
158}
159
160#[derive(Clone)]
166pub struct JwtValidator {
167 config: JwtConfig,
168 decoding_key: Option<DecodingKey>,
169 validation: Validation,
170 jwks_cache: Option<Arc<JwksCache>>,
172 jti_cache: Arc<DashMap<String, u64>>,
176}
177
178impl std::fmt::Debug for JwtValidator {
179 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180 f.debug_struct("JwtValidator")
181 .field("config", &self.config)
182 .field("has_decoding_key", &self.decoding_key.is_some())
183 .field("has_jwks_cache", &self.jwks_cache.is_some())
184 .finish()
185 }
186}
187
188impl JwtValidator {
189 pub fn new(config: JwtConfig) -> Result<Self, AuthError> {
196 let algorithm = match config.algorithm.as_deref() {
197 Some(s) => parse_algorithm(s)?,
198 None => Algorithm::HS256,
199 };
200
201 let mut validation = Validation::new(algorithm);
202
203 if let Some(ref iss) = config.issuer {
204 validation.set_issuer(&[iss]);
205 }
206
207 if let Some(ref aud) = config.audience {
208 validation.set_audience(&[aud]);
209 } else if config.require_audience {
210 return Err(AuthError::Config(
211 "audience is required but not configured; set `audience` or \
212 set `require_audience = false` to opt out"
213 .into(),
214 ));
215 } else {
216 validation.validate_aud = false;
217 }
218
219 validation.set_required_spec_claims(&["exp"]);
221
222 validation.validate_nbf = true;
226
227 let hmac_family = matches!(
244 algorithm,
245 Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512
246 );
247 if hmac_family && config.public_key_pem.is_some() {
248 return Err(AuthError::Config(format!(
249 "{algorithm:?} is an HMAC algorithm and takes `secret`, not `public_key_pem`"
250 )));
251 }
252 if !hmac_family && config.secret.is_some() && config.public_key_pem.is_none() {
253 return Err(AuthError::Config(format!(
254 "{algorithm:?} verifies with a public key; set `public_key_pem` \
255 (a `secret` cannot verify an asymmetric signature)"
256 )));
257 }
258
259 let decoding_key = match (
260 algorithm,
261 config.secret.as_ref(),
262 config.public_key_pem.as_ref(),
263 ) {
264 (_, None, None) => None,
265 (Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512, Some(secret), _) => {
266 if secret.len() < 32 {
268 return Err(AuthError::Config(
269 "HMAC secret must be at least 32 bytes".into(),
270 ));
271 }
272 Some(DecodingKey::from_secret(secret.as_bytes()))
273 }
274 (
275 Algorithm::RS256
276 | Algorithm::RS384
277 | Algorithm::RS512
278 | Algorithm::PS256
279 | Algorithm::PS384
280 | Algorithm::PS512,
281 _,
282 Some(pem),
283 ) => Some(DecodingKey::from_rsa_pem(pem.as_bytes()).map_err(|e| {
284 AuthError::Config(format!(
285 "{algorithm:?} public key is not valid RSA PEM: {e}"
286 ))
287 })?),
288 (Algorithm::ES256 | Algorithm::ES384, _, Some(pem)) => {
289 Some(DecodingKey::from_ec_pem(pem.as_bytes()).map_err(|e| {
290 AuthError::Config(format!("{algorithm:?} public key is not valid EC PEM: {e}"))
291 })?)
292 }
293 (Algorithm::EdDSA, _, Some(pem)) => {
294 Some(DecodingKey::from_ed_pem(pem.as_bytes()).map_err(|e| {
295 AuthError::Config(format!("EdDSA public key is not valid PEM: {e}"))
296 })?)
297 }
298 (other, _, _) => {
299 return Err(AuthError::Config(format!(
300 "JWT algorithm {other:?} is not supported"
301 )))
302 }
303 };
304
305 Ok(Self {
306 config,
307 decoding_key,
308 validation,
309 jwks_cache: None,
310 jti_cache: Arc::new(DashMap::new()),
311 })
312 }
313
314 #[must_use]
319 pub fn with_jwks_cache(mut self, cache: Arc<JwksCache>) -> Self {
320 self.jwks_cache = Some(cache);
321 self
322 }
323
324 #[must_use]
326 pub fn jwks_cache(&self) -> Option<&Arc<JwksCache>> {
327 self.jwks_cache.as_ref()
328 }
329
330 pub fn validate(&self, token: &str) -> Result<JwtClaims, AuthError> {
332 let key = self
333 .decoding_key
334 .as_ref()
335 .ok_or_else(|| AuthError::Config("no decoding key configured".into()))?;
336
337 let token_data: TokenData<JwtClaims> =
338 decode(token, key, &self.validation).map_err(|e| match e.kind() {
339 jsonwebtoken::errors::ErrorKind::ExpiredSignature => AuthError::ExpiredJwt,
340 _ => AuthError::InvalidJwt(e.to_string()),
341 })?;
342
343 self.check_jti_replay(&token_data.claims)?;
345
346 Ok(token_data.claims)
347 }
348
349 fn check_jti_replay(&self, claims: &JwtClaims) -> Result<(), AuthError> {
355 let now = std::time::SystemTime::now()
357 .duration_since(std::time::UNIX_EPOCH)
358 .unwrap_or_default()
359 .as_secs();
360 self.jti_cache.retain(|_, exp| *exp > now);
361
362 if let Some(ref jti) = claims.jti {
363 const MAX_JTI_CACHE: usize = 100_000;
367 if self.jti_cache.len() >= MAX_JTI_CACHE {
368 tracing::error!(
369 cache_size = self.jti_cache.len(),
370 "jti replay cache exhausted — rejecting token (fail-closed)"
371 );
372 return Err(AuthError::InvalidJwt(
373 "replay cache exhausted — try again later".into(),
374 ));
375 }
376 let exp = claims.exp.unwrap_or(now + 3600);
377 if self.jti_cache.insert(jti.clone(), exp).is_some() {
378 return Err(AuthError::InvalidJwt(
379 "token replay detected (duplicate jti)".into(),
380 ));
381 }
382 }
383 Ok(())
384 }
385
386 pub async fn validate_async(&self, token: &str) -> Result<JwtClaims, AuthError> {
394 if self.decoding_key.is_some() {
396 match self.validate(token) {
397 ok @ Ok(_) => return ok,
398 Err(e) => {
399 if self.jwks_cache.is_none() || matches!(e, AuthError::ExpiredJwt) {
402 return Err(e);
403 }
404 tracing::debug!(error = %e, "static key validation failed, falling back to JWKS");
405 }
406 }
407 }
408
409 let cache = self
411 .jwks_cache
412 .as_ref()
413 .ok_or_else(|| AuthError::Config("no decoding key or JWKS cache configured".into()))?;
414
415 let header = decode_header(token)
417 .map_err(|e| AuthError::InvalidJwt(format!("failed to decode JWT header: {e}")))?;
418
419 if let Some(kid) = &header.kid {
420 let cached = cache.get_key(kid).await?;
421 let mut validation = self.validation.clone();
422 validation.algorithms = vec![cached.algorithm];
423 let token_data: TokenData<JwtClaims> = decode(token, &cached.key, &validation)
424 .map_err(|e| match e.kind() {
425 jsonwebtoken::errors::ErrorKind::ExpiredSignature => AuthError::ExpiredJwt,
426 _ => AuthError::InvalidJwt(e.to_string()),
427 })?;
428 return Ok(token_data.claims);
429 }
430
431 let all_keys = cache.get_all_keys().await?;
433 let mut last_err = AuthError::Config("no JWKS keys available".into());
434 for cached in &all_keys {
435 let mut validation = self.validation.clone();
436 validation.algorithms = vec![cached.algorithm];
437 match decode::<JwtClaims>(token, &cached.key, &validation) {
438 Ok(td) => return Ok(td.claims),
439 Err(e) => {
440 match e.kind() {
441 jsonwebtoken::errors::ErrorKind::ExpiredSignature => {
442 return Err(AuthError::ExpiredJwt);
448 }
449 _ => {
450 last_err = AuthError::InvalidJwt(e.to_string());
451 }
452 }
453 }
454 }
455 }
456
457 Err(last_err)
458 }
459
460 pub fn extract_principal(&self, claims: &JwtClaims) -> Option<String> {
462 let claim_name = self.config.subject_claim.as_deref().unwrap_or("sub");
463
464 match claim_name {
465 "sub" => claims.sub.clone(),
466 "email" => claims.email.clone(),
467 other => claims
468 .extra
469 .get(other)
470 .and_then(|v| v.as_str())
471 .map(String::from),
472 }
473 }
474}
475
476fn parse_algorithm(s: &str) -> Result<Algorithm, AuthError> {
481 match s.to_uppercase().as_str() {
482 "HS256" => Ok(Algorithm::HS256),
483 "HS384" => Ok(Algorithm::HS384),
484 "HS512" => Ok(Algorithm::HS512),
485 "RS256" => Ok(Algorithm::RS256),
486 "RS384" => Ok(Algorithm::RS384),
487 "RS512" => Ok(Algorithm::RS512),
488 "ES256" => Ok(Algorithm::ES256),
489 "ES384" => Ok(Algorithm::ES384),
490 "PS256" => Ok(Algorithm::PS256),
491 "PS384" => Ok(Algorithm::PS384),
492 "PS512" => Ok(Algorithm::PS512),
493 "EDDSA" => Ok(Algorithm::EdDSA),
494 unknown => Err(AuthError::Config(format!(
495 "unrecognized JWT algorithm: {unknown}"
496 ))),
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503 use jsonwebtoken::{encode, EncodingKey, Header};
504
505 fn make_token(claims: &JwtClaims, secret: &str) -> String {
506 let header = Header::new(Algorithm::HS256);
507 let key = EncodingKey::from_secret(secret.as_bytes());
508 encode(&header, claims, &key).unwrap()
509 }
510
511 fn future_exp() -> u64 {
512 std::time::SystemTime::now()
513 .duration_since(std::time::UNIX_EPOCH)
514 .unwrap()
515 .as_secs()
516 + 3600
517 }
518
519 #[test]
520 fn reject_short_hmac_secret() {
521 let config = JwtConfig {
522 secret: Some(Zeroizing::new("too-short".into())),
523 ..Default::default()
524 };
525 let err = JwtValidator::new(config).unwrap_err();
526 assert!(matches!(err, AuthError::Config(_)));
527 }
528
529 #[test]
530 fn reject_missing_audience_when_required() {
531 let config = JwtConfig {
532 secret: Some(Zeroizing::new("test-secret-at-least-32-bytes!!!".into())),
533 require_audience: true,
534 audience: None,
535 ..Default::default()
536 };
537 let err = JwtValidator::new(config).unwrap_err();
538 assert!(matches!(err, AuthError::Config(_)));
539 }
540
541 #[test]
542 fn allow_missing_audience_when_not_required() {
543 let config = JwtConfig {
544 secret: Some(Zeroizing::new("test-secret-at-least-32-bytes!!!".into())),
545 require_audience: false,
546 audience: None,
547 ..Default::default()
548 };
549 assert!(JwtValidator::new(config).is_ok());
550 }
551
552 #[test]
553 fn validate_valid_token() {
554 let secret = "test-secret-key-for-unit-tests!!"; let config = JwtConfig {
556 secret: Some(Zeroizing::new(secret.into())),
557 ..Default::default()
558 };
559 let validator = JwtValidator::new(config).unwrap();
560
561 let claims = JwtClaims {
562 sub: Some("user-123".into()),
563 exp: Some(future_exp()),
564 iss: None,
565 aud: None,
566 iat: None,
567 email: None,
568 jti: None,
569 extra: HashMap::new(),
570 };
571
572 let token = make_token(&claims, secret);
573 let validated = validator.validate(&token).unwrap();
574 assert_eq!(validated.sub, Some("user-123".into()));
575 }
576
577 #[test]
578 fn reject_expired_token() {
579 let secret = "test-secret-at-least-32-bytes!!!";
580 let config = JwtConfig {
581 secret: Some(Zeroizing::new(secret.into())),
582 ..Default::default()
583 };
584 let validator = JwtValidator::new(config).unwrap();
585
586 let claims = JwtClaims {
587 sub: Some("user".into()),
588 exp: Some(0), iss: None,
590 aud: None,
591 iat: None,
592 email: None,
593 jti: None,
594 extra: HashMap::new(),
595 };
596
597 let token = make_token(&claims, secret);
598 let err = validator.validate(&token).unwrap_err();
599 assert!(matches!(err, AuthError::ExpiredJwt));
600 }
601
602 #[test]
603 fn reject_wrong_secret() {
604 let config = JwtConfig {
605 secret: Some(Zeroizing::new("correct-secret-at-least-32bytes!".into())),
606 ..Default::default()
607 };
608 let validator = JwtValidator::new(config).unwrap();
609
610 let claims = JwtClaims {
611 sub: Some("user".into()),
612 exp: Some(future_exp()),
613 iss: None,
614 aud: None,
615 iat: None,
616 email: None,
617 jti: None,
618 extra: HashMap::new(),
619 };
620
621 let token = make_token(&claims, "wrong-secret--at-least-32bytes!");
622 let err = validator.validate(&token).unwrap_err();
623 assert!(matches!(err, AuthError::InvalidJwt(_)));
624 }
625
626 #[test]
627 fn validate_with_issuer() {
628 let secret = "test-secret-at-least-32-bytes!!!";
629 let config = JwtConfig {
630 secret: Some(Zeroizing::new(secret.into())),
631 issuer: Some("https://auth.example.com".into()),
632 ..Default::default()
633 };
634 let validator = JwtValidator::new(config).unwrap();
635
636 let claims = JwtClaims {
637 sub: Some("user".into()),
638 iss: Some("https://auth.example.com".into()),
639 exp: Some(future_exp()),
640 aud: None,
641 iat: None,
642 email: None,
643 jti: None,
644 extra: HashMap::new(),
645 };
646
647 let token = make_token(&claims, secret);
648 validator.validate(&token).unwrap();
649 }
650
651 #[test]
652 fn reject_wrong_issuer() {
653 let secret = "test-secret-at-least-32-bytes!!!";
654 let config = JwtConfig {
655 secret: Some(Zeroizing::new(secret.into())),
656 issuer: Some("https://auth.example.com".into()),
657 ..Default::default()
658 };
659 let validator = JwtValidator::new(config).unwrap();
660
661 let claims = JwtClaims {
662 sub: Some("user".into()),
663 iss: Some("https://evil.com".into()),
664 exp: Some(future_exp()),
665 aud: None,
666 iat: None,
667 email: None,
668 jti: None,
669 extra: HashMap::new(),
670 };
671
672 let token = make_token(&claims, secret);
673 assert!(validator.validate(&token).is_err());
674 }
675
676 #[test]
677 fn extract_principal_from_sub() {
678 let config = JwtConfig::default();
679 let validator = JwtValidator::new(config).unwrap();
680
681 let claims = JwtClaims {
682 sub: Some("principal-1".into()),
683 email: Some("user@example.com".into()),
684 exp: None,
685 iss: None,
686 aud: None,
687 iat: None,
688 jti: None,
689 extra: HashMap::new(),
690 };
691
692 assert_eq!(
693 validator.extract_principal(&claims),
694 Some("principal-1".into())
695 );
696 }
697
698 #[test]
699 fn extract_principal_from_email() {
700 let config = JwtConfig {
701 subject_claim: Some("email".into()),
702 ..Default::default()
703 };
704 let validator = JwtValidator::new(config).unwrap();
705
706 let claims = JwtClaims {
707 sub: Some("user-id".into()),
708 email: Some("admin@chronix.io".into()),
709 exp: None,
710 iss: None,
711 aud: None,
712 iat: None,
713 jti: None,
714 extra: HashMap::new(),
715 };
716
717 assert_eq!(
718 validator.extract_principal(&claims),
719 Some("admin@chronix.io".into())
720 );
721 }
722
723 #[test]
724 fn extract_principal_from_custom_claim() {
725 let config = JwtConfig {
726 subject_claim: Some("username".into()),
727 ..Default::default()
728 };
729 let validator = JwtValidator::new(config).unwrap();
730
731 let mut extra = HashMap::new();
732 extra.insert("username".into(), serde_json::json!("admin"));
733
734 let claims = JwtClaims {
735 sub: None,
736 email: None,
737 exp: None,
738 iss: None,
739 aud: None,
740 iat: None,
741 jti: None,
742 extra,
743 };
744
745 assert_eq!(validator.extract_principal(&claims), Some("admin".into()));
746 }
747
748 fn make_token_with_kid(claims: &JwtClaims, secret: &str, kid: &str) -> String {
753 let mut header = Header::new(Algorithm::HS256);
754 header.kid = Some(kid.to_string());
755 let key = EncodingKey::from_secret(secret.as_bytes());
756 encode(&header, claims, &key).unwrap()
757 }
758
759 #[tokio::test]
760 async fn validate_async_with_static_key() {
761 let secret = "static-secret-at-least-32-bytes!";
762 let config = JwtConfig {
763 secret: Some(Zeroizing::new(secret.into())),
764 ..Default::default()
765 };
766 let validator = JwtValidator::new(config).unwrap();
767
768 let claims = JwtClaims {
769 sub: Some("user-static".into()),
770 exp: Some(future_exp()),
771 iss: None,
772 aud: None,
773 iat: None,
774 email: None,
775 jti: None,
776 extra: HashMap::new(),
777 };
778
779 let token = make_token(&claims, secret);
780 let validated = validator.validate_async(&token).await.unwrap();
781 assert_eq!(validated.sub, Some("user-static".into()));
782 }
783
784 #[tokio::test]
785 async fn validate_async_with_jwks_cache() {
786 let secret = "jwks-secret-at-least-32-bytes!!!";
787
788 let mut keys = std::collections::HashMap::new();
790 keys.insert(
791 "key-1".to_string(),
792 crate::auth::oidc::CachedKey {
793 key: DecodingKey::from_secret(secret.as_bytes()),
794 algorithm: Algorithm::HS256,
795 },
796 );
797 let cache = Arc::new(crate::auth::oidc::JwksCache::from_keys(keys));
798
799 let config = JwtConfig::default();
801 let validator = JwtValidator::new(config).unwrap().with_jwks_cache(cache);
802
803 let claims = JwtClaims {
804 sub: Some("oidc-user".into()),
805 exp: Some(future_exp()),
806 iss: None,
807 aud: None,
808 iat: None,
809 email: None,
810 jti: None,
811 extra: HashMap::new(),
812 };
813
814 let token = make_token_with_kid(&claims, secret, "key-1");
815 let validated = validator.validate_async(&token).await.unwrap();
816 assert_eq!(validated.sub, Some("oidc-user".into()));
817 }
818
819 #[tokio::test]
820 async fn validate_async_jwks_no_kid_tries_all() {
821 let secret = "no-kid-secret-at-least-32bytes!!";
822
823 let mut keys = std::collections::HashMap::new();
824 keys.insert(
825 "default".to_string(),
826 crate::auth::oidc::CachedKey {
827 key: DecodingKey::from_secret(secret.as_bytes()),
828 algorithm: Algorithm::HS256,
829 },
830 );
831 let cache = Arc::new(crate::auth::oidc::JwksCache::from_keys(keys));
832
833 let config = JwtConfig::default();
834 let validator = JwtValidator::new(config).unwrap().with_jwks_cache(cache);
835
836 let claims = JwtClaims {
837 sub: Some("anon".into()),
838 exp: Some(future_exp()),
839 iss: None,
840 aud: None,
841 iat: None,
842 email: None,
843 jti: None,
844 extra: HashMap::new(),
845 };
846
847 let token = make_token(&claims, secret);
849 let validated = validator.validate_async(&token).await.unwrap();
850 assert_eq!(validated.sub, Some("anon".into()));
851 }
852
853 #[tokio::test]
854 async fn validate_async_jwks_wrong_kid_fails() {
855 let secret = "wrong-kid-secret-at-least-32byte";
856
857 let mut keys = std::collections::HashMap::new();
858 keys.insert(
859 "real-kid".to_string(),
860 crate::auth::oidc::CachedKey {
861 key: DecodingKey::from_secret(secret.as_bytes()),
862 algorithm: Algorithm::HS256,
863 },
864 );
865 let cache = Arc::new(crate::auth::oidc::JwksCache::from_keys(keys));
866
867 let config = JwtConfig::default();
868 let validator = JwtValidator::new(config).unwrap().with_jwks_cache(cache);
869
870 let claims = JwtClaims {
871 sub: Some("user".into()),
872 exp: Some(future_exp()),
873 iss: None,
874 aud: None,
875 iat: None,
876 email: None,
877 jti: None,
878 extra: HashMap::new(),
879 };
880
881 let token = make_token_with_kid(&claims, secret, "unknown-kid");
882 let err = validator.validate_async(&token).await.unwrap_err();
883 assert!(matches!(err, AuthError::Config(_)));
884 }
885
886 #[tokio::test]
887 async fn validate_async_no_key_no_cache_fails() {
888 let config = JwtConfig::default();
889 let validator = JwtValidator::new(config).unwrap();
890
891 let claims = JwtClaims {
892 sub: Some("user".into()),
893 exp: Some(future_exp()),
894 iss: None,
895 aud: None,
896 iat: None,
897 email: None,
898 jti: None,
899 extra: HashMap::new(),
900 };
901
902 let token = make_token(&claims, "any-secret");
903 let err = validator.validate_async(&token).await.unwrap_err();
904 assert!(matches!(err, AuthError::Config(_)));
905 }
906}
907
908#[cfg(test)]
909mod algorithm_family_tests {
910 use super::*;
911 use jsonwebtoken::{encode, EncodingKey, Header};
912 use std::collections::HashMap;
913 use zeroize::Zeroizing;
914
915 const EC_PRIVATE_PEM: &str = "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgftsCo6PP58Bktonn\nIQQdJB0xhCZr5vaXReMirLL6vfmhRANCAATI63Cp8AxIz8oL4Sfzwp/1r5j8o5v5\nQ2O3+N41eKFkFT/bk1oF8zP6Zz6OaOU7nfw77Pcn+DZz9GFp3ISD9rJK\n-----END PRIVATE KEY-----";
918 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-----";
919
920 fn claims() -> JwtClaims {
921 JwtClaims {
922 sub: Some("ec-user".into()),
923 exp: Some(
924 std::time::SystemTime::now()
925 .duration_since(std::time::UNIX_EPOCH)
926 .unwrap()
927 .as_secs()
928 + 3600,
929 ),
930 iss: None,
931 aud: None,
932 iat: None,
933 email: None,
934 jti: None,
935 extra: HashMap::new(),
936 }
937 }
938
939 #[test]
943 fn es256_verifies_a_token_signed_with_its_key() {
944 let config = JwtConfig {
945 algorithm: Some("ES256".into()),
946 public_key_pem: Some(Zeroizing::new(EC_PUBLIC_PEM.to_string())),
947 ..Default::default()
948 };
949 let validator = JwtValidator::new(config).expect("ES256 config must build");
950
951 let token = encode(
952 &Header::new(Algorithm::ES256),
953 &claims(),
954 &EncodingKey::from_ec_pem(EC_PRIVATE_PEM.as_bytes()).unwrap(),
955 )
956 .unwrap();
957
958 let validated = validator.validate(&token).expect("a valid ES256 token");
959 assert_eq!(validated.sub, Some("ec-user".into()));
960 }
961
962 #[test]
966 fn an_asymmetric_algorithm_with_a_secret_is_a_startup_error() {
967 let config = JwtConfig {
968 algorithm: Some("RS256".into()),
969 secret: Some(Zeroizing::new("a-secret-that-is-at-least-32-bytes!".into())),
970 ..Default::default()
971 };
972 let err = JwtValidator::new(config).expect_err("a secret cannot verify an RSA signature");
973 assert!(
974 err.to_string().contains("public key"),
975 "the error must say what to set instead: {err}"
976 );
977 }
978
979 #[test]
982 fn an_hmac_algorithm_with_a_public_key_is_a_startup_error() {
983 let config = JwtConfig {
984 algorithm: Some("HS256".into()),
985 public_key_pem: Some(Zeroizing::new(EC_PUBLIC_PEM.to_string())),
986 ..Default::default()
987 };
988 assert!(JwtValidator::new(config).is_err());
989 }
990
991 #[test]
994 fn no_key_material_is_allowed_for_jwks() {
995 let config = JwtConfig {
996 algorithm: Some("RS256".into()),
997 ..Default::default()
998 };
999 assert!(JwtValidator::new(config).is_ok());
1000 }
1001}