use core::fmt;
use core::time::Duration;
use std::collections::BTreeSet;
use std::time::{SystemTime, UNIX_EPOCH};
use serde::de::DeserializeOwned;
use super::{KeyReference, KeyReferenceError, KeyResolutionError, KeyResolver, KeyRole};
use crate::jose::{
CompactJws, DefaultVerifier, Jwk, JwsAlgorithm, JwsError, JwsHeader, JwsVerifier, JwtClaims,
OneOrMany, VerificationError,
};
pub struct JwtValidator<R, V = DefaultVerifier> {
resolver: R,
verifier: V,
}
impl<R: KeyResolver, V: JwsVerifier> JwtValidator<R, V> {
pub fn new(resolver: R, verifier: V) -> Self {
Self { resolver, verifier }
}
pub fn resolver(&self) -> &R {
&self.resolver
}
pub fn verifier(&self) -> &V {
&self.verifier
}
pub async fn validate(
&self,
token: &str,
options: &JwtValidationOptions,
) -> Result<ValidatedJwt, JwtValidationError> {
self.validate_as(token, options).await
}
pub async fn validate_as<T: DeserializeOwned>(
&self,
token: &str,
options: &JwtValidationOptions,
) -> Result<ValidatedJwt<T>, JwtValidationError> {
let jws = CompactJws::decode(token)?;
let header = jws.header();
if !options.allows(header.alg) {
return Err(JwtValidationError::DisallowedAlgorithm(header.alg));
}
if let Some(expected) = &options.typ
&& !header
.typ
.as_deref()
.is_some_and(|typ| same_typ(typ, expected))
{
return Err(JwtValidationError::UnexpectedTyp {
expected: expected.clone(),
found: header.typ.clone(),
});
}
let claims: JwtClaims =
serde_json::from_slice(jws.payload()).map_err(JwtValidationError::Payload)?;
let reference = KeyReference::from_jws(&jws, claims.iss.as_deref())?;
let key = self
.resolver
.resolve_key(&reference, options.role)
.await
.map_err(|source| JwtValidationError::KeyResolution {
key: Box::new(reference.clone()),
source,
})?;
jws.verify(&self.verifier, &key)?;
if options.issuer_binding == IssuerBinding::KeyOfIssuer
&& !reference.binds_to(claims.iss.as_deref(), &key)
{
return Err(JwtValidationError::KeyNotIssuers {
issuer: claims.iss.clone(),
key: Box::new(reference),
});
}
if let Some(expected) = &options.issuer
&& claims.iss.as_ref() != Some(expected)
{
return Err(JwtValidationError::IssuerMismatch {
expected: expected.clone(),
found: claims.iss.clone(),
});
}
if let Some(expected) = &options.audience
&& !claims.audiences().any(|audience| audience == expected)
{
return Err(JwtValidationError::AudienceMismatch {
expected: expected.clone(),
found: claims.aud.clone(),
});
}
options.check_times(&claims)?;
for claim in &options.required_claims {
if !has_claim(&claims, claim) {
return Err(JwtValidationError::MissingClaim(claim.clone()));
}
}
let typed = serde_json::from_slice(jws.payload()).map_err(JwtValidationError::Claims)?;
Ok(ValidatedJwt {
header: header.clone(),
claims: typed,
key,
key_reference: reference,
})
}
}
impl<R: fmt::Debug, V: fmt::Debug> fmt::Debug for JwtValidator<R, V> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("JwtValidator")
.field("resolver", &self.resolver)
.field("verifier", &self.verifier)
.finish()
}
}
fn same_typ(found: &str, expected: &str) -> bool {
fn bare(typ: &str) -> &str {
match typ.get(..12) {
Some(prefix) if prefix.eq_ignore_ascii_case("application/") => &typ[12..],
_ => typ,
}
}
bare(found).eq_ignore_ascii_case(bare(expected))
}
fn has_claim(claims: &JwtClaims, name: &str) -> bool {
match name {
"iss" => claims.iss.is_some(),
"sub" => claims.sub.is_some(),
"aud" => claims.aud.is_some(),
"exp" => claims.exp.is_some(),
"nbf" => claims.nbf.is_some(),
"iat" => claims.iat.is_some(),
"jti" => claims.jti.is_some(),
other => claims.additional.contains_key(other),
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ValidatedJwt<T = JwtClaims> {
pub header: JwsHeader,
pub claims: T,
pub key: Jwk,
pub key_reference: KeyReference,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum IssuerBinding {
#[default]
KeyOfIssuer,
Any,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct JwtValidationOptions {
pub algorithms: BTreeSet<JwsAlgorithm>,
pub typ: Option<String>,
pub issuer: Option<String>,
pub audience: Option<String>,
pub role: KeyRole,
pub issuer_binding: IssuerBinding,
pub required_claims: BTreeSet<String>,
pub now: Option<i64>,
pub leeway: Duration,
}
impl JwtValidationOptions {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_algorithm(mut self, alg: JwsAlgorithm) -> Self {
self.algorithms.insert(alg);
self
}
#[must_use]
pub fn with_typ(mut self, typ: impl Into<String>) -> Self {
self.typ = Some(typ.into());
self
}
#[must_use]
pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
self.issuer = Some(issuer.into());
self
}
#[must_use]
pub fn with_audience(mut self, audience: impl Into<String>) -> Self {
self.audience = Some(audience.into());
self
}
#[must_use]
pub fn with_role(mut self, role: KeyRole) -> Self {
self.role = role;
self
}
#[must_use]
pub fn with_issuer_binding(mut self, binding: IssuerBinding) -> Self {
self.issuer_binding = binding;
self
}
#[must_use]
pub fn with_required_claims<I, S>(mut self, claims: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.required_claims
.extend(claims.into_iter().map(Into::into));
self
}
#[must_use]
pub fn at(mut self, now: i64) -> Self {
self.now = Some(now);
self
}
#[must_use]
pub fn with_leeway(mut self, leeway: Duration) -> Self {
self.leeway = leeway;
self
}
pub fn now(&self) -> i64 {
self.now.unwrap_or_else(|| {
let since_epoch = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO);
i64::try_from(since_epoch.as_secs()).unwrap_or(i64::MAX)
})
}
fn allows(&self, alg: JwsAlgorithm) -> bool {
self.algorithms.is_empty() || self.algorithms.contains(&alg)
}
fn check_times(&self, claims: &JwtClaims) -> Result<(), JwtValidationError> {
let now = self.now();
let leeway = i64::try_from(self.leeway.as_secs()).unwrap_or(i64::MAX);
let latest = now.saturating_add(leeway);
let earliest = now.saturating_sub(leeway);
if let Some(expired_at) = claims.exp
&& expired_at <= earliest
{
return Err(JwtValidationError::Expired { expired_at, now });
}
if let Some(not_before) = claims.nbf
&& not_before > latest
{
return Err(JwtValidationError::NotYetValid { not_before, now });
}
if let Some(issued_at) = claims.iat
&& issued_at > latest
{
return Err(JwtValidationError::IssuedInFuture { issued_at, now });
}
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum JwtValidationError {
#[error("invalid JWS: {0}")]
Jws(#[from] JwsError),
#[error("algorithm `{0}` is not accepted")]
DisallowedAlgorithm(JwsAlgorithm),
#[error("header `typ` is {found:?}, not `{expected}`")]
UnexpectedTyp {
expected: String,
found: Option<String>,
},
#[error("the payload is not a JWT claims set: {0}")]
Payload(#[source] serde_json::Error),
#[error(transparent)]
KeyReference(#[from] KeyReferenceError),
#[error("resolving key {key}: {source}")]
KeyResolution {
key: Box<KeyReference>,
#[source]
source: KeyResolutionError,
},
#[error("signature: {0}")]
Signature(#[from] VerificationError),
#[error("signing key {key} does not belong to issuer {issuer:?}")]
KeyNotIssuers {
issuer: Option<String>,
key: Box<KeyReference>,
},
#[error("issuer {found:?} is not {expected:?}")]
IssuerMismatch {
expected: String,
found: Option<String>,
},
#[error("audience {found:?} does not include {expected:?}")]
AudienceMismatch {
expected: String,
found: Option<OneOrMany<String>>,
},
#[error("expired at {expired_at} (now is {now})")]
Expired {
expired_at: i64,
now: i64,
},
#[error("not valid before {not_before} (now is {now})")]
NotYetValid {
not_before: i64,
now: i64,
},
#[error("issued at {issued_at}, after now ({now})")]
IssuedInFuture {
issued_at: i64,
now: i64,
},
#[error("missing claim `{0}`")]
MissingClaim(String),
#[error("the claims do not fit the expected type: {0}")]
Claims(#[source] serde_json::Error),
}