use snafu::ensure;
use super::{
ClaimCheck, ClaimMismatchSnafu, ExpiredSnafu, InvalidTokenTypeSnafu, IssuedInFutureSnafu,
JwtValidationError, NotYetValidSnafu, RequiredClaimMissingSnafu,
};
use crate::platform::{Duration, SystemTime};
pub(super) fn normalize_typ(typ: &str) -> &str {
match typ.split_at_checked(12) {
Some((prefix, rest)) if !rest.is_empty() && prefix.eq_ignore_ascii_case("application/") => {
rest
}
_ => typ,
}
}
pub(super) 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(())
}
pub(super) 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(())
}
pub(super) 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(())
}
pub(super) fn check_temporal(
now: SystemTime,
clock_leeway: Duration,
exp: Option<SystemTime>,
nbf: Option<SystemTime>,
iat: Option<SystemTime>,
) -> Result<(), JwtValidationError> {
if let Some(expiration) = exp {
ensure!(
!now.duration_since(expiration)
.is_ok_and(|past| past > clock_leeway),
ExpiredSnafu { expiration, now }
);
}
if let Some(not_before) = nbf {
ensure!(
!not_before
.duration_since(now)
.is_ok_and(|ahead| ahead > clock_leeway),
NotYetValidSnafu { not_before, now }
);
}
if let Some(issued_at) = iat {
ensure!(
!issued_at
.duration_since(now)
.is_ok_and(|ahead| ahead > clock_leeway),
IssuedInFutureSnafu { issued_at, now }
);
}
Ok(())
}