use std::collections::HashSet;
use bon::Builder;
use serde::Deserialize;
use snafu::{ResultExt as _, Snafu, ensure};
use crate::{
BoxedError,
crypto::verifier::{BoxedJwsVerifier, JwsVerifier, KeyMatch, VerifyError},
jwt::{
BoxedJtiUniquenessChecker, ConfirmationClaim, JtiUniquenessChecker, JwsParseError,
ParsedJws, parse_compact_jws,
},
platform::{Duration, SystemTime},
};
#[non_exhaustive]
#[derive(Debug, Clone, Default)]
pub enum ClaimCheck {
IfPresent(String),
RequireAny(Vec<String>),
RequiredValue(String),
Present,
#[default]
NoCheck,
}
impl ClaimCheck {
pub fn if_present(value: impl Into<String>) -> Self {
Self::IfPresent(value.into())
}
pub fn require_any(values: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self::RequireAny(values.into_iter().map(Into::into).collect())
}
pub fn required_value(value: impl Into<String>) -> Self {
Self::RequiredValue(value.into())
}
#[must_use]
pub fn present() -> Self {
Self::Present
}
}
#[allow(clippy::struct_excessive_bools)]
#[allow(clippy::should_implement_trait)] #[derive(Debug, Builder)]
pub struct JwtValidator {
verifier: BoxedJwsVerifier,
#[builder(default)]
iss: ClaimCheck,
#[builder(default)]
sub: ClaimCheck,
#[builder(default)]
aud: ClaimCheck,
#[builder(default)]
typ: ClaimCheck,
#[builder(default)]
require_exp: bool,
#[builder(default)]
require_iat: bool,
#[builder(default)]
require_jti: bool,
#[builder(default = 255)]
max_jti_len: usize,
jti_checker: Option<BoxedJtiUniquenessChecker>,
max_token_age: Option<Duration>,
#[builder(default)]
clock_leeway: Duration,
#[builder(default, with = FromIterator::from_iter)]
allowed_crit: HashSet<String>,
#[builder(with = FromIterator::from_iter)]
allowed_algorithms: Option<HashSet<String>>,
}
fn normalize_typ(typ: &str) -> &str {
if typ.len() > 12 && typ[..12].eq_ignore_ascii_case("application/") {
&typ[12..]
} else {
typ
}
}
fn check_str_claim(
claim: &'static str,
check: &ClaimCheck,
value: Option<&str>,
) -> Result<(), JwtValidationError> {
match check {
ClaimCheck::Present => {
if value.is_none() {
return Err(RequiredClaimMissingSnafu { claim }.build());
}
}
ClaimCheck::RequiredValue(v) => match value {
Some(val) if val == v.as_str() => {}
Some(val) => {
return Err(ClaimMismatchSnafu {
claim,
expected: v.clone(),
actual: val,
}
.build());
}
None => return Err(RequiredClaimMissingSnafu { claim }.build()),
},
ClaimCheck::RequireAny(vs) => match value {
Some(val) if vs.iter().any(|x| val == x.as_str()) => {}
Some(val) => {
return Err(ClaimMismatchSnafu {
claim,
expected: vs.join(", "),
actual: val,
}
.build());
}
None => return Err(RequiredClaimMissingSnafu { claim }.build()),
},
ClaimCheck::IfPresent(v) => {
if let Some(val) = value
&& val != v.as_str()
{
return Err(ClaimMismatchSnafu {
claim,
expected: v.clone(),
actual: val,
}
.build());
}
}
ClaimCheck::NoCheck => {}
}
Ok(())
}
fn check_aud(check: &ClaimCheck, aud: &[String]) -> Result<(), JwtValidationError> {
match check {
ClaimCheck::Present => ensure!(!aud.is_empty(), RequiredClaimMissingSnafu { claim: "aud" }),
ClaimCheck::RequiredValue(v) => ensure!(
aud.contains(v),
ClaimMismatchSnafu {
claim: "aud",
expected: v.clone(),
actual: aud.join(", "),
}
),
ClaimCheck::RequireAny(vs) => ensure!(
vs.iter().any(|v| aud.contains(v)),
ClaimMismatchSnafu {
claim: "aud",
expected: vs.join(", "),
actual: aud.join(", "),
}
),
ClaimCheck::IfPresent(v) => {
if !aud.is_empty() {
ensure!(
aud.contains(v),
ClaimMismatchSnafu {
claim: "aud",
expected: v.clone(),
actual: aud.join(", "),
}
);
}
}
ClaimCheck::NoCheck => {}
}
Ok(())
}
fn check_typ(check: &ClaimCheck, typ: Option<&str>) -> Result<(), JwtValidationError> {
match check {
ClaimCheck::IfPresent(t) => ensure!(
typ.is_none_or(|v| normalize_typ(v).eq_ignore_ascii_case(normalize_typ(t))),
InvalidTokenTypeSnafu {
typ: typ.map(Into::into)
}
),
ClaimCheck::RequireAny(allowed) => match typ {
None => return RequiredClaimMissingSnafu { claim: "typ" }.fail(),
Some(v)
if allowed
.iter()
.any(|t| normalize_typ(v).eq_ignore_ascii_case(normalize_typ(t))) => {}
Some(v) => {
return InvalidTokenTypeSnafu {
typ: Some(v.into()),
}
.fail();
}
},
ClaimCheck::RequiredValue(t) => match typ {
None => return RequiredClaimMissingSnafu { claim: "typ" }.fail(),
Some(v) if normalize_typ(v).eq_ignore_ascii_case(normalize_typ(t)) => {}
Some(v) => {
return InvalidTokenTypeSnafu {
typ: Some(v.into()),
}
.fail();
}
},
ClaimCheck::Present => {
ensure!(typ.is_some(), RequiredClaimMissingSnafu { claim: "typ" });
}
ClaimCheck::NoCheck => {}
}
Ok(())
}
fn check_temporal(
now: SystemTime,
clock_leeway: Duration,
exp: Option<u64>,
nbf: Option<u64>,
iat: Option<u64>,
) -> Result<(), JwtValidationError> {
if let Some(exp) = exp {
let expiration = SystemTime::UNIX_EPOCH + Duration::from_secs(exp);
ensure!(
expiration + clock_leeway >= now,
ExpiredSnafu { expiration, now }
);
}
if let Some(nbf) = nbf {
let not_before = SystemTime::UNIX_EPOCH + Duration::from_secs(nbf);
ensure!(
not_before <= now + clock_leeway,
NotYetValidSnafu { not_before, now }
);
}
if let Some(iat) = iat {
let issued_at = SystemTime::UNIX_EPOCH + Duration::from_secs(iat);
ensure!(
issued_at <= now + clock_leeway,
IssuedInFutureSnafu { issued_at, now }
);
}
Ok(())
}
impl JwtValidator {
fn validate_header(&self, alg: &str, crit: &[String]) -> Result<(), JwtValidationError> {
ensure!(alg != "none", UnsignedTokenSnafu);
if let Some(allowed) = &self.allowed_algorithms {
ensure!(
allowed.contains(alg),
DisallowedAlgorithmSnafu {
alg: alg.to_string()
}
);
}
ensure!(
crit.iter().all(|v| self.allowed_crit.contains(v)),
UnrecognizedCriticalHeaderSnafu {
params: crit.to_vec()
}
);
Ok(())
}
async fn validate_jti(&self, jti: Option<&str>) -> Result<(), JwtValidationError> {
if let Some(jti) = jti {
ensure!(
jti.len() <= self.max_jti_len,
JtiTooLongSnafu {
len: jti.len(),
max_len: self.max_jti_len,
}
);
if let Some(jti_checker) = self.jti_checker.as_ref() {
ensure!(
!jti_checker
.check_and_mark_seen(jti)
.await
.context(JtiCheckSnafu)?,
JtiNotUniqueSnafu
);
}
}
Ok(())
}
fn check_required_claims(
&self,
exp: Option<u64>,
iat: Option<u64>,
jti: Option<&str>,
) -> Result<(), JwtValidationError> {
if self.require_exp {
ensure!(exp.is_some(), RequiredClaimMissingSnafu { claim: "exp" });
}
if self.require_iat {
ensure!(iat.is_some(), RequiredClaimMissingSnafu { claim: "iat" });
}
if self.require_jti {
ensure!(jti.is_some(), RequiredClaimMissingSnafu { claim: "jti" });
}
Ok(())
}
pub async fn validate_parsed_jws<C: for<'de> Deserialize<'de> + Clone + 'static>(
&self,
parsed_jwt: ParsedJws<(), C>,
) -> Result<ValidatedJwt<C>, JwtValidationError> {
let now = SystemTime::now();
self.validate_header(&parsed_jwt.header.alg, &parsed_jwt.header.crit)?;
let key_match = KeyMatch {
alg: &parsed_jwt.header.alg,
kid: parsed_jwt.header.kid.as_deref(),
};
self.verifier
.verify(&parsed_jwt.signing_input, &parsed_jwt.signature, &key_match)
.await
.context(SignatureSnafu)?;
check_aud(&self.aud, &parsed_jwt.claims.aud)?;
self.check_required_claims(
parsed_jwt.claims.exp,
parsed_jwt.claims.iat,
parsed_jwt.claims.jti.as_deref(),
)?;
self.validate_jti(parsed_jwt.claims.jti.as_deref()).await?;
if let Some(max_token_age) = self.max_token_age {
let iat = parsed_jwt
.claims
.iat
.ok_or_else(|| RequiredClaimMissingSnafu { claim: "iat" }.build())?;
let issued_at = SystemTime::UNIX_EPOCH + Duration::from_secs(iat);
ensure!(
now.duration_since(issued_at)
.is_ok_and(|d| d <= max_token_age + self.clock_leeway),
TokenTooOldSnafu {
issued_at,
max_token_age
}
);
}
check_typ(&self.typ, parsed_jwt.header.typ.as_deref())?;
check_str_claim("iss", &self.iss, parsed_jwt.claims.iss.as_deref())?;
check_str_claim("sub", &self.sub, parsed_jwt.claims.sub.as_deref())?;
check_temporal(
now,
self.clock_leeway,
parsed_jwt.claims.exp,
parsed_jwt.claims.nbf,
parsed_jwt.claims.iat,
)?;
Ok(ValidatedJwt {
issuer: parsed_jwt.claims.iss.map(Into::into),
subject: parsed_jwt.claims.sub.map(Into::into),
audience: parsed_jwt.claims.aud.iter().map(Into::into).collect(),
issued_at: parsed_jwt
.claims
.iat
.map(|iat| SystemTime::UNIX_EPOCH + Duration::from_secs(iat)),
expiration: parsed_jwt
.claims
.exp
.map(|exp| SystemTime::UNIX_EPOCH + Duration::from_secs(exp)),
jti: parsed_jwt.claims.jti.map(Into::into),
cnf: parsed_jwt.claims.cnf,
claims: match parsed_jwt.claims.claims {
std::borrow::Cow::Borrowed(c) => c.clone(),
std::borrow::Cow::Owned(c) => c,
},
})
}
pub async fn validate<C: Clone + for<'de> Deserialize<'de> + 'static>(
&self,
token: &str,
) -> Result<ValidatedJwt<C>, JwtValidationError> {
let parsed_jwt = parse_compact_jws::<(), serde_json::Value>(token).context(ParseSnafu)?;
let validated = self.validate_parsed_jws(parsed_jwt).await?;
validated.try_map_claims(|value| {
if std::any::TypeId::of::<C>() == std::any::TypeId::of::<()>() {
serde_json::from_value(serde_json::Value::Null).context(ExtraClaimsSnafu)
} else {
serde_json::from_value(value).context(ExtraClaimsSnafu)
}
})
}
}
#[non_exhaustive]
#[derive(Debug, Builder)]
pub struct ValidatedJwt<Claims> {
pub issuer: Option<String>,
pub subject: Option<String>,
pub audience: Vec<String>,
pub jti: Option<String>,
pub issued_at: Option<SystemTime>,
pub expiration: Option<SystemTime>,
pub cnf: Option<ConfirmationClaim>,
pub claims: Claims,
}
impl<Claims> ValidatedJwt<Claims> {
pub fn map_claims<C1, F>(self, f: F) -> ValidatedJwt<C1>
where
F: FnOnce(Claims) -> C1,
{
ValidatedJwt {
issuer: self.issuer,
subject: self.subject,
audience: self.audience,
jti: self.jti,
issued_at: self.issued_at,
expiration: self.expiration,
cnf: self.cnf,
claims: f(self.claims),
}
}
pub fn try_map_claims<C1, E, F>(self, f: F) -> Result<ValidatedJwt<C1>, E>
where
F: FnOnce(Claims) -> Result<C1, E>,
{
Ok(ValidatedJwt {
issuer: self.issuer,
subject: self.subject,
audience: self.audience,
jti: self.jti,
issued_at: self.issued_at,
expiration: self.expiration,
cnf: self.cnf,
claims: f(self.claims)?,
})
}
}
#[derive(Debug, Snafu)]
pub enum JwtValidationError {
Parse {
source: JwsParseError,
},
Signature {
source: VerifyError<BoxedError>,
},
UnsignedToken,
DisallowedAlgorithm {
alg: String,
},
UnrecognizedCriticalHeader {
params: Vec<String>,
},
Expired {
expiration: SystemTime,
now: SystemTime,
},
NotYetValid {
not_before: SystemTime,
now: SystemTime,
},
IssuedInFuture {
issued_at: SystemTime,
now: SystemTime,
},
TokenTooOld {
issued_at: SystemTime,
max_token_age: Duration,
},
InvalidTokenType {
typ: Option<String>,
},
ClaimMismatch {
claim: &'static str,
expected: String,
actual: String,
},
RequiredClaimMissing {
claim: &'static str,
},
JtiTooLong {
len: usize,
max_len: usize,
},
JtiNotUnique,
JtiCheck {
source: BoxedError,
},
ExtraClaims {
source: serde_json::Error,
},
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::{
KeyMatchStrength,
verifier::{JwsVerifier, KeyMatch, VerifyError},
};
#[derive(Debug)]
struct MockVerifier;
impl JwsVerifier for MockVerifier {
type Error = crate::BoxedError;
fn key_match(&self, _key_match: &KeyMatch<'_>) -> Option<KeyMatchStrength> {
Some(KeyMatchStrength::ByAlgorithm)
}
async fn verify(
&self,
_input: &[u8],
_signature: &[u8],
_key: &KeyMatch<'_>,
) -> Result<(), VerifyError<Self::Error>> {
Ok(())
}
}
#[tokio::test]
async fn test_validate_unit_claims_with_extra_fields() {
let token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJqb2UiLCJmb28iOiJiYXIifQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
let validator = JwtValidator::builder()
.verifier(BoxedJwsVerifier::new(MockVerifier))
.iss(ClaimCheck::required_value("joe"))
.build();
let result = validator.validate::<()>(token).await;
assert!(result.is_ok(), "Expected Ok, got {:?}", result.err());
}
#[tokio::test]
async fn test_validate_custom_claims_success() {
#[derive(Debug, Clone, Deserialize, PartialEq)]
struct MyClaims {
foo: String,
}
let token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJqb2UiLCJmb28iOiJiYXIifQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
let validator = JwtValidator::builder()
.verifier(BoxedJwsVerifier::new(MockVerifier))
.iss(ClaimCheck::required_value("joe"))
.build();
let result = validator.validate::<MyClaims>(token).await.unwrap();
assert_eq!(
result.claims,
MyClaims {
foo: "bar".to_string()
}
);
}
#[tokio::test]
async fn test_validate_custom_claims_failure() {
#[derive(Debug, Clone, Deserialize, PartialEq)]
struct MyClaims {
missing: String,
}
let token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJqb2UiLCJmb28iOiJiYXIifQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
let validator = JwtValidator::builder()
.verifier(BoxedJwsVerifier::new(MockVerifier))
.iss(ClaimCheck::required_value("joe"))
.build();
let result = validator.validate::<MyClaims>(token).await;
assert!(matches!(
result,
Err(JwtValidationError::ExtraClaims { .. })
));
}
fn make_parsed_jws(
header: serde_json::Value,
claims: serde_json::Value,
) -> ParsedJws<(), serde_json::Value> {
use crate::jwt::structure::{JwtClaims, JwtHeader};
let header: JwtHeader<'static, ()> = serde_json::from_value(header).unwrap();
let claims: JwtClaims<'static, serde_json::Value> = serde_json::from_value(claims).unwrap();
ParsedJws {
header,
claims,
signing_input: b"dummy.input".to_vec(),
signature: vec![0x00],
}
}
fn default_validator() -> JwtValidator {
JwtValidator::builder()
.verifier(BoxedJwsVerifier::new(MockVerifier))
.build()
}
#[tokio::test]
async fn reject_alg_none() {
let parsed = make_parsed_jws(serde_json::json!({"alg": "none"}), serde_json::json!({}));
let result = default_validator()
.validate_parsed_jws::<serde_json::Value>(parsed)
.await;
assert!(matches!(result, Err(JwtValidationError::UnsignedToken)));
}
#[tokio::test]
async fn reject_disallowed_algorithm() {
let validator = JwtValidator::builder()
.verifier(BoxedJwsVerifier::new(MockVerifier))
.allowed_algorithms(["ES256".to_string()])
.build();
let parsed = make_parsed_jws(serde_json::json!({"alg": "RS256"}), serde_json::json!({}));
let result = validator
.validate_parsed_jws::<serde_json::Value>(parsed)
.await;
assert!(matches!(
result,
Err(JwtValidationError::DisallowedAlgorithm { .. })
));
}
#[tokio::test]
async fn reject_unrecognized_crit() {
let parsed = make_parsed_jws(
serde_json::json!({"alg": "RS256", "crit": ["unknown-ext"]}),
serde_json::json!({}),
);
let result = default_validator()
.validate_parsed_jws::<serde_json::Value>(parsed)
.await;
assert!(matches!(
result,
Err(JwtValidationError::UnrecognizedCriticalHeader { .. })
));
}
#[tokio::test]
async fn reject_expired_token() {
let parsed = make_parsed_jws(
serde_json::json!({"alg": "RS256"}),
serde_json::json!({"exp": 1}), );
let result = default_validator()
.validate_parsed_jws::<serde_json::Value>(parsed)
.await;
assert!(matches!(result, Err(JwtValidationError::Expired { .. })));
}
#[tokio::test]
async fn reject_not_yet_valid() {
let far_future = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs()
+ 999_999;
let parsed = make_parsed_jws(
serde_json::json!({"alg": "RS256"}),
serde_json::json!({"nbf": far_future}),
);
let result = default_validator()
.validate_parsed_jws::<serde_json::Value>(parsed)
.await;
assert!(matches!(
result,
Err(JwtValidationError::NotYetValid { .. })
));
}
#[tokio::test]
async fn reject_issued_in_future() {
let far_future = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs()
+ 999_999;
let parsed = make_parsed_jws(
serde_json::json!({"alg": "RS256"}),
serde_json::json!({"iat": far_future}),
);
let result = default_validator()
.validate_parsed_jws::<serde_json::Value>(parsed)
.await;
assert!(matches!(
result,
Err(JwtValidationError::IssuedInFuture { .. })
));
}
#[tokio::test]
async fn clock_leeway_allows_slightly_expired() {
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
let validator = JwtValidator::builder()
.verifier(BoxedJwsVerifier::new(MockVerifier))
.clock_leeway(Duration::from_secs(10))
.build();
let parsed = make_parsed_jws(
serde_json::json!({"alg": "RS256"}),
serde_json::json!({"exp": now - 5}),
);
let result = validator
.validate_parsed_jws::<serde_json::Value>(parsed)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn require_exp_missing() {
let validator = JwtValidator::builder()
.verifier(BoxedJwsVerifier::new(MockVerifier))
.require_exp(true)
.build();
let parsed = make_parsed_jws(serde_json::json!({"alg": "RS256"}), serde_json::json!({}));
let result = validator
.validate_parsed_jws::<serde_json::Value>(parsed)
.await;
assert!(matches!(
result,
Err(JwtValidationError::RequiredClaimMissing { claim: "exp" })
));
}
#[tokio::test]
async fn require_iat_missing() {
let validator = JwtValidator::builder()
.verifier(BoxedJwsVerifier::new(MockVerifier))
.require_iat(true)
.build();
let parsed = make_parsed_jws(serde_json::json!({"alg": "RS256"}), serde_json::json!({}));
let result = validator
.validate_parsed_jws::<serde_json::Value>(parsed)
.await;
assert!(matches!(
result,
Err(JwtValidationError::RequiredClaimMissing { claim: "iat" })
));
}
#[tokio::test]
async fn require_jti_missing() {
let validator = JwtValidator::builder()
.verifier(BoxedJwsVerifier::new(MockVerifier))
.require_jti(true)
.build();
let parsed = make_parsed_jws(serde_json::json!({"alg": "RS256"}), serde_json::json!({}));
let result = validator
.validate_parsed_jws::<serde_json::Value>(parsed)
.await;
assert!(matches!(
result,
Err(JwtValidationError::RequiredClaimMissing { claim: "jti" })
));
}
#[tokio::test]
async fn iss_required_value_mismatch() {
let validator = JwtValidator::builder()
.verifier(BoxedJwsVerifier::new(MockVerifier))
.iss(ClaimCheck::required_value("expected-issuer"))
.build();
let parsed = make_parsed_jws(
serde_json::json!({"alg": "RS256"}),
serde_json::json!({"iss": "wrong-issuer"}),
);
let result = validator
.validate_parsed_jws::<serde_json::Value>(parsed)
.await;
assert!(matches!(
result,
Err(JwtValidationError::ClaimMismatch { claim: "iss", .. })
));
}
#[tokio::test]
async fn iss_require_any_mismatch() {
let validator = JwtValidator::builder()
.verifier(BoxedJwsVerifier::new(MockVerifier))
.iss(ClaimCheck::require_any(["a", "b"]))
.build();
let parsed = make_parsed_jws(
serde_json::json!({"alg": "RS256"}),
serde_json::json!({"iss": "c"}),
);
let result = validator
.validate_parsed_jws::<serde_json::Value>(parsed)
.await;
assert!(matches!(
result,
Err(JwtValidationError::ClaimMismatch { claim: "iss", .. })
));
}
#[tokio::test]
async fn iss_present_missing() {
let validator = JwtValidator::builder()
.verifier(BoxedJwsVerifier::new(MockVerifier))
.iss(ClaimCheck::present())
.build();
let parsed = make_parsed_jws(serde_json::json!({"alg": "RS256"}), serde_json::json!({}));
let result = validator
.validate_parsed_jws::<serde_json::Value>(parsed)
.await;
assert!(matches!(
result,
Err(JwtValidationError::RequiredClaimMissing { claim: "iss" })
));
}
#[tokio::test]
async fn iss_if_present_mismatch() {
let validator = JwtValidator::builder()
.verifier(BoxedJwsVerifier::new(MockVerifier))
.iss(ClaimCheck::if_present("expected"))
.build();
let parsed = make_parsed_jws(
serde_json::json!({"alg": "RS256"}),
serde_json::json!({"iss": "wrong"}),
);
let result = validator
.validate_parsed_jws::<serde_json::Value>(parsed)
.await;
assert!(matches!(
result,
Err(JwtValidationError::ClaimMismatch { claim: "iss", .. })
));
}
#[tokio::test]
async fn iss_if_present_absent_ok() {
let validator = JwtValidator::builder()
.verifier(BoxedJwsVerifier::new(MockVerifier))
.iss(ClaimCheck::if_present("expected"))
.build();
let parsed = make_parsed_jws(serde_json::json!({"alg": "RS256"}), serde_json::json!({}));
let result = validator
.validate_parsed_jws::<serde_json::Value>(parsed)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn aud_required_value_not_in_list() {
let validator = JwtValidator::builder()
.verifier(BoxedJwsVerifier::new(MockVerifier))
.aud(ClaimCheck::required_value("expected-aud"))
.build();
let parsed = make_parsed_jws(
serde_json::json!({"alg": "RS256"}),
serde_json::json!({"aud": "wrong-aud"}),
);
let result = validator
.validate_parsed_jws::<serde_json::Value>(parsed)
.await;
assert!(matches!(
result,
Err(JwtValidationError::ClaimMismatch { claim: "aud", .. })
));
}
#[tokio::test]
async fn aud_empty_when_required() {
let validator = JwtValidator::builder()
.verifier(BoxedJwsVerifier::new(MockVerifier))
.aud(ClaimCheck::present())
.build();
let parsed = make_parsed_jws(serde_json::json!({"alg": "RS256"}), serde_json::json!({}));
let result = validator
.validate_parsed_jws::<serde_json::Value>(parsed)
.await;
assert!(matches!(
result,
Err(JwtValidationError::RequiredClaimMissing { claim: "aud" })
));
}
#[tokio::test]
async fn typ_required_value_mismatch() {
let validator = JwtValidator::builder()
.verifier(BoxedJwsVerifier::new(MockVerifier))
.typ(ClaimCheck::required_value("at+jwt"))
.build();
let parsed = make_parsed_jws(
serde_json::json!({"alg": "RS256", "typ": "JWT"}),
serde_json::json!({}),
);
let result = validator
.validate_parsed_jws::<serde_json::Value>(parsed)
.await;
assert!(matches!(
result,
Err(JwtValidationError::InvalidTokenType { .. })
));
}
#[tokio::test]
async fn typ_application_prefix_normalization() {
let validator = JwtValidator::builder()
.verifier(BoxedJwsVerifier::new(MockVerifier))
.typ(ClaimCheck::required_value("at+jwt"))
.build();
let parsed = make_parsed_jws(
serde_json::json!({"alg": "RS256", "typ": "application/at+jwt"}),
serde_json::json!({}),
);
let result = validator
.validate_parsed_jws::<serde_json::Value>(parsed)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn typ_case_insensitive() {
let validator = JwtValidator::builder()
.verifier(BoxedJwsVerifier::new(MockVerifier))
.typ(ClaimCheck::required_value("AT+JWT"))
.build();
let parsed = make_parsed_jws(
serde_json::json!({"alg": "RS256", "typ": "at+jwt"}),
serde_json::json!({}),
);
let result = validator
.validate_parsed_jws::<serde_json::Value>(parsed)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn max_token_age_old_token() {
let validator = JwtValidator::builder()
.verifier(BoxedJwsVerifier::new(MockVerifier))
.max_token_age(Duration::from_mins(1))
.build();
let parsed = make_parsed_jws(
serde_json::json!({"alg": "RS256"}),
serde_json::json!({"iat": 1}), );
let result = validator
.validate_parsed_jws::<serde_json::Value>(parsed)
.await;
assert!(matches!(
result,
Err(JwtValidationError::TokenTooOld { .. })
));
}
#[tokio::test]
async fn max_token_age_missing_iat() {
let validator = JwtValidator::builder()
.verifier(BoxedJwsVerifier::new(MockVerifier))
.max_token_age(Duration::from_mins(1))
.build();
let parsed = make_parsed_jws(serde_json::json!({"alg": "RS256"}), serde_json::json!({}));
let result = validator
.validate_parsed_jws::<serde_json::Value>(parsed)
.await;
assert!(matches!(
result,
Err(JwtValidationError::RequiredClaimMissing { claim: "iat" })
));
}
#[tokio::test]
async fn jti_too_long() {
let validator = JwtValidator::builder()
.verifier(BoxedJwsVerifier::new(MockVerifier))
.max_jti_len(5)
.build();
let parsed = make_parsed_jws(
serde_json::json!({"alg": "RS256"}),
serde_json::json!({"jti": "toolong"}),
);
let result = validator
.validate_parsed_jws::<serde_json::Value>(parsed)
.await;
assert!(matches!(result, Err(JwtValidationError::JtiTooLong { .. })));
}
}