use serde_json::Value as JsonValue;
use super::{JwtError, JwtTemporalClaim};
const MAX_TEMPORAL_SKEW_SECONDS: u64 = 86_400;
#[derive(Debug, Clone, Copy)]
pub struct JwtTemporalValidationPolicy {
require_exp: bool,
require_nbf: bool,
require_iat: bool,
clock_skew_seconds: u64,
max_future_iat_skew_seconds: u64,
}
impl JwtTemporalValidationPolicy {
#[must_use]
pub const fn new(
require_exp: bool,
require_nbf: bool,
require_iat: bool,
clock_skew_seconds: u64,
max_future_iat_skew_seconds: u64,
) -> Self {
Self {
require_exp,
require_nbf,
require_iat,
clock_skew_seconds,
max_future_iat_skew_seconds,
}
}
#[must_use]
pub const fn strict() -> Self {
Self::new(true, false, false, 60, 60)
}
#[must_use]
pub const fn require_exp(&self) -> bool {
self.require_exp
}
#[must_use]
pub const fn require_nbf(&self) -> bool {
self.require_nbf
}
#[must_use]
pub const fn require_iat(&self) -> bool {
self.require_iat
}
#[must_use]
pub const fn clock_skew_seconds(&self) -> u64 {
self.clock_skew_seconds
}
#[must_use]
pub const fn max_future_iat_skew_seconds(&self) -> u64 {
self.max_future_iat_skew_seconds
}
const fn validate(&self) -> Result<(), JwtError> {
if self.clock_skew_seconds > MAX_TEMPORAL_SKEW_SECONDS {
return Err(JwtError::InvalidTemporalPolicy);
}
if self.max_future_iat_skew_seconds > MAX_TEMPORAL_SKEW_SECONDS {
return Err(JwtError::InvalidTemporalPolicy);
}
Ok(())
}
}
pub(super) fn validate_temporal_claims(
payload: &JsonValue,
now_unix: u64,
temporal_policy: JwtTemporalValidationPolicy,
) -> Result<(), JwtError> {
temporal_policy.validate()?;
let exp = parse_optional_numeric_date(payload, JwtTemporalClaim::Exp)?;
let nbf = parse_optional_numeric_date(payload, JwtTemporalClaim::Nbf)?;
let iat = parse_optional_numeric_date(payload, JwtTemporalClaim::Iat)?;
if temporal_policy.require_exp && exp.is_none() {
return Err(JwtError::MissingRequiredTemporalClaim(
JwtTemporalClaim::Exp,
));
}
if temporal_policy.require_nbf && nbf.is_none() {
return Err(JwtError::MissingRequiredTemporalClaim(
JwtTemporalClaim::Nbf,
));
}
if temporal_policy.require_iat && iat.is_none() {
return Err(JwtError::MissingRequiredTemporalClaim(
JwtTemporalClaim::Iat,
));
}
validate_expiration(exp, now_unix, temporal_policy.clock_skew_seconds)?;
validate_not_before(nbf, now_unix, temporal_policy.clock_skew_seconds)?;
validate_issued_at(iat, now_unix, temporal_policy.max_future_iat_skew_seconds)?;
Ok(())
}
fn parse_optional_numeric_date(
payload: &JsonValue,
claim: JwtTemporalClaim,
) -> Result<Option<u64>, JwtError> {
let key = match claim {
JwtTemporalClaim::Exp => "exp",
JwtTemporalClaim::Nbf => "nbf",
JwtTemporalClaim::Iat => "iat",
};
let Some(value) = payload.get(key) else {
return Ok(None);
};
value
.as_u64()
.ok_or(JwtError::InvalidTemporalClaimValue(claim))
.map(Some)
}
fn validate_expiration(
exp: Option<u64>,
now_unix: u64,
clock_skew_seconds: u64,
) -> Result<(), JwtError> {
let Some(exp_unix) = exp else {
return Ok(());
};
if exp_unix == 0 {
return Err(JwtError::InvalidTemporalClaimValue(JwtTemporalClaim::Exp));
}
let expiration_floor = checked_skew_floor(now_unix, clock_skew_seconds)?;
if expiration_floor > exp_unix {
return Err(JwtError::Expired);
}
Ok(())
}
fn validate_not_before(
nbf: Option<u64>,
now_unix: u64,
clock_skew_seconds: u64,
) -> Result<(), JwtError> {
let Some(nbf_unix) = nbf else {
return Ok(());
};
if nbf_unix == 0 {
return Err(JwtError::InvalidTemporalClaimValue(JwtTemporalClaim::Nbf));
}
let not_before_ceiling = checked_skew_ceiling(now_unix, clock_skew_seconds)?;
if not_before_ceiling < nbf_unix {
return Err(JwtError::NotYetValid);
}
Ok(())
}
fn validate_issued_at(
iat: Option<u64>,
now_unix: u64,
max_future_iat_skew_seconds: u64,
) -> Result<(), JwtError> {
let Some(iat_unix) = iat else {
return Ok(());
};
if iat_unix == 0 {
return Err(JwtError::InvalidTemporalClaimValue(JwtTemporalClaim::Iat));
}
let issued_at_ceiling = checked_skew_ceiling(now_unix, max_future_iat_skew_seconds)?;
if iat_unix > issued_at_ceiling {
return Err(JwtError::IssuedAtInFuture);
}
Ok(())
}
fn checked_skew_floor(now_unix: u64, clock_skew_seconds: u64) -> Result<u64, JwtError> {
if now_unix < clock_skew_seconds {
return Ok(0);
}
now_unix
.checked_sub(clock_skew_seconds)
.ok_or(JwtError::InvalidTemporalPolicy)
}
fn checked_skew_ceiling(now_unix: u64, skew_seconds: u64) -> Result<u64, JwtError> {
now_unix
.checked_add(skew_seconds)
.ok_or(JwtError::InvalidTemporalPolicy)
}