Skip to content
Clone
d5939db
202 lines 6.3 KB Rust
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::<AccessClaims>(&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::<RefreshClaims>(&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::<AccessClaims>(&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::<AccessClaims>(&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::<AccessClaims>(&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<OffsetDateTime>) -> 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"));
}