use time::OffsetDateTime; use uuid::Uuid; use crate::claims::{AccessClaims, RefreshClaims}; use crate::error::JwtError; use crate::grace; use crate::keys::CachedKey; use crate::signer::{encode_access_token, encode_refresh_token}; fn ed25519_key() -> (jsonwebtoken::EncodingKey, jsonwebtoken::DecodingKey) { use ed25519_dalek::SigningKey; use ed25519_dalek::pkcs8::EncodePrivateKey; use rand_core::OsRng; let sk = SigningKey::generate(&mut OsRng); let vk = sk.verifying_key(); let encoding = jsonwebtoken::EncodingKey::from_ed_der(sk.to_pkcs8_der().unwrap().as_bytes()); let decoding = jsonwebtoken::DecodingKey::from_ed_der(&vk.to_bytes()); (encoding, decoding) } fn access_claims(kid: &str) -> AccessClaims { AccessClaims::new( Uuid::new_v4(), "alice".into(), true, false, 1, 900, "test-issuer", "test-aud", kid, ) } #[test] fn access_roundtrip() { let (enc, dec) = ed25519_key(); let kid = "kid-1"; let claims = access_claims(kid); let token = encode_access_token(&claims, &enc, jsonwebtoken::Algorithm::EdDSA).unwrap(); let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::EdDSA); validation.set_issuer(&["test-issuer".to_string()]); validation.set_audience(&["test-aud".to_string()]); let data = jsonwebtoken::decode::(&token, &dec, &validation).unwrap(); assert_eq!(data.claims.sub, claims.sub); assert_eq!(data.claims.kid, kid); assert!(data.claims.is_admin); } #[test] fn refresh_roundtrip() { let (enc, dec) = ed25519_key(); let kid = "kid-1"; let family = Uuid::new_v4(); let claims = RefreshClaims::new( Uuid::new_v4(), family, None, 1, 2592000, "test-issuer", "test-aud", kid, ); let token = encode_refresh_token(&claims, &enc, jsonwebtoken::Algorithm::EdDSA).unwrap(); let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::EdDSA); validation.set_issuer(&["test-issuer".to_string()]); validation.set_audience(&["test-aud".to_string()]); let data = jsonwebtoken::decode::(&token, &dec, &validation).unwrap(); assert_eq!(data.claims.family_id, family); assert_eq!(data.claims.kid, kid); } #[test] fn expired_token_rejected() { let (enc, dec) = ed25519_key(); let mut claims = access_claims("kid-1"); claims.exp = 0; // already expired let token = encode_access_token(&claims, &enc, jsonwebtoken::Algorithm::EdDSA).unwrap(); let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::EdDSA); validation.set_issuer(&["test-issuer".to_string()]); validation.set_audience(&["test-aud".to_string()]); let res = jsonwebtoken::decode::(&token, &dec, &validation); assert!(res.is_err()); let err = JwtError::from(res.unwrap_err()); assert!(matches!(err, JwtError::Expired)); } #[test] fn wrong_audience_rejected() { let (enc, dec) = ed25519_key(); let claims = access_claims("kid-1"); let token = encode_access_token(&claims, &enc, jsonwebtoken::Algorithm::EdDSA).unwrap(); let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::EdDSA); validation.set_issuer(&["test-issuer".to_string()]); validation.set_audience(&["wrong-aud".to_string()]); let res = jsonwebtoken::decode::(&token, &dec, &validation); assert!(res.is_err()); let err = JwtError::from(res.unwrap_err()); assert!(matches!(err, JwtError::InvalidAudience)); } #[test] fn wrong_key_rejected() { let (enc, _dec) = ed25519_key(); let (_, dec2) = ed25519_key(); let claims = access_claims("kid-1"); let token = encode_access_token(&claims, &enc, jsonwebtoken::Algorithm::EdDSA).unwrap(); let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::EdDSA); validation.set_issuer(&["test-issuer".to_string()]); validation.set_audience(&["test-aud".to_string()]); let res = jsonwebtoken::decode::(&token, &dec2, &validation); assert!(res.is_err()); let err = JwtError::from(res.unwrap_err()); assert!(matches!(err, JwtError::InvalidSignature)); } fn cached_key(rotating: bool, grace_ends_at: Option) -> CachedKey { let (_, dec) = ed25519_key(); CachedKey { decoding: dec, algorithm: jsonwebtoken::Algorithm::EdDSA, is_rotating: rotating, grace_ends_at, expires_at: None, } } #[test] fn active_key_usable_for_verify() { let key = cached_key(false, None); assert!(grace::is_key_usable_for_verify( &key, OffsetDateTime::now_utc() )); } #[test] fn rotating_key_usable_inside_grace() { let key = cached_key( true, Some(OffsetDateTime::now_utc() + time::Duration::seconds(60)), ); assert!(grace::is_key_usable_for_verify( &key, OffsetDateTime::now_utc() )); } #[test] fn rotating_key_unusable_after_grace() { let key = cached_key( true, Some(OffsetDateTime::now_utc() - time::Duration::seconds(1)), ); assert!(!grace::is_key_usable_for_verify( &key, OffsetDateTime::now_utc() )); assert!(grace::ensure_key_usable(&key).is_err()); } #[test] fn expired_key_unusable() { let mut key = cached_key(false, None); key.expires_at = Some(OffsetDateTime::now_utc() - time::Duration::seconds(1)); assert!(!grace::is_key_usable_for_verify( &key, OffsetDateTime::now_utc() )); } #[test] fn replay_rejected_outside_grace() { let now = OffsetDateTime::now_utc(); assert!(grace::reject_replay(None, now)); assert!(grace::reject_replay( Some(now - time::Duration::seconds(1)), now )); assert!(!grace::reject_replay( Some(now + time::Duration::seconds(60)), now )); } #[test] fn unknown_kid_returns_error() { let kid = Uuid::new_v4(); let cache = crate::keys::SigningKeyCache::new(); assert!(cache.get(&kid).is_none()); assert!(!cache.in_grace_window(&kid)); } #[test] fn kid_from_token_roundtrip() { let (enc, _dec) = ed25519_key(); let claims = access_claims("kid-xyz"); let token = encode_access_token(&claims, &enc, jsonwebtoken::Algorithm::EdDSA).unwrap(); let kid = crate::signer::kid_from_token(&token); assert_eq!(kid.as_deref(), Some("kid-xyz")); }