use std::{collections::HashSet, sync::Arc};
use bon::Builder;
use serde::Deserialize;
use snafu::{ResultExt as _, Snafu, ensure};
use crate::{
crypto::verifier::{JwsVerifier, KeyMatch, VerifyError},
jwt::{JtiUniquenessChecker, JwsParseError, ParsedJws, parse_compact_jws},
platform::{Duration, SystemTime},
};
mod checks;
mod claim_check;
mod validated_jwt;
use checks::{check_aud, check_str_claim, check_temporal, check_typ};
pub use checks::{within_max_age, within_max_age_secs};
pub use claim_check::ClaimCheck;
pub use validated_jwt::{ValidatedJwt, ValidatedJwtBuilder};
#[allow(clippy::struct_excessive_bools)]
#[allow(clippy::should_implement_trait)] #[derive(Debug, Builder)]
pub struct JwtValidator {
#[builder(with = |verifier: impl JwsVerifier + 'static| Arc::new(verifier) as Arc<dyn JwsVerifier>)]
verifier: Arc<dyn JwsVerifier>,
#[builder(default, into)]
iss: ClaimCheck,
#[builder(default, into)]
sub: ClaimCheck,
#[builder(default, into)]
aud: ClaimCheck,
#[builder(default, into)]
typ: ClaimCheck,
#[builder(default)]
require_exp: bool,
#[builder(default)]
require_iat: bool,
#[builder(default)]
require_jti: bool,
#[builder(default = 255)]
max_jti_len: usize,
#[builder(with = |checker: impl JtiUniquenessChecker + 'static| Arc::new(checker) as Arc<dyn JtiUniquenessChecker>)]
jti_checker: Option<Arc<dyn JtiUniquenessChecker>>,
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>>,
}
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<SystemTime>,
iat: Option<SystemTime>,
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(),
)?;
if let Some(max_token_age) = self.max_token_age {
let issued_at = parsed_jwt
.claims
.iat
.ok_or_else(|| RequiredClaimMissingSnafu { claim: "iat" }.build())?;
ensure!(
within_max_age(now, issued_at, 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,
)?;
self.validate_jti(parsed_jwt.claims.jti.as_deref()).await?;
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,
expiration: parsed_jwt.claims.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)
}
})
}
}
#[derive(Debug, Snafu)]
pub enum JwtValidationError {
Parse {
source: JwsParseError,
},
Signature {
source: VerifyError,
},
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: crate::error::Error,
},
ExtraClaims {
source: serde_json::Error,
},
}
#[cfg(test)]
mod tests;