///|
/// Validation options for JWT verification.
pub(all) struct ValidationOptions {
/// Expected issuer; if set, the token's iss claim must match.
mut expected_issuer : String?
/// Expected audience; if set, the token's aud claim must contain it.
mut expected_audience : String?
/// Expected subject; if set, the token's sub claim must match.
mut expected_subject : String?
/// Current time in seconds since epoch (for exp/nbf checks).
/// Current time in seconds since epoch.
mut now_time : Int
/// Leeway in seconds for time-based checks (clock skew tolerance).
mut leeway : Int
}
///|
/// Default validation options: no issuer/audience check, leeway=0.
pub fn ValidationOptions::new(now : Int) -> ValidationOptions {
{
expected_issuer: None,
expected_audience: None,
expected_subject: None,
now_time: now,
leeway: 0,
}
}
///|
/// Set expected issuer.
pub fn ValidationOptions::with_issuer(
self : ValidationOptions,
iss : String,
) -> ValidationOptions {
self.expected_issuer = Some(iss)
self
}
///|
/// Set expected audience.
pub fn ValidationOptions::with_audience(
self : ValidationOptions,
aud : String,
) -> ValidationOptions {
self.expected_audience = Some(aud)
self
}
///|
/// Set clock skew leeway in seconds.
pub fn ValidationOptions::with_leeway(
self : ValidationOptions,
seconds : Int,
) -> ValidationOptions {
self.leeway = seconds
self
}
///|
/// Extract a string claim from a JSON claims object.
fn get_string_claim(claims : Json, key : String) -> String? {
match claims {
Object(o) =>
match o.get(key) {
Some(String(s)) => Some(s)
_ => None
}
_ => None
}
}
///|
/// Extract a numeric claim (as Int) from a JSON claims object.
fn get_numeric_claim(claims : Json, key : String) -> Int? {
match claims {
Object(o) =>
match o.get(key) {
Some(Number(n, ..)) => Some(n.to_int())
_ => None
}
_ => None
}
}
///|
/// Validate a token's signature AND claims according to [ValidationOptions].
///
/// This is the high-level entry point: it verifies the HMAC signature,
/// then checks exp, nbf, iss, aud, sub claims as configured.
pub fn validate(
token : String,
secret : Bytes,
options : ValidationOptions,
) -> Result[Json, JwtError] {
// Step 1: verify signature
match verify(token, secret) {
Err(e) => return Err(e)
Ok(_) => ()
}
// Step 2: decode claims
let claims = match decode_claims(token) {
Ok(c) => c
Err(e) => return Err(e)
}
// Step 3: check expiration (exp)
match get_numeric_claim(claims, claim_exp) {
Some(exp) => {
if options.now_time > exp + options.leeway {
return Err(TokenExpired)
}
()
}
None => ()
}
// Step 4: check not-before (nbf)
match get_numeric_claim(claims, claim_nbf) {
Some(nbf) => {
if options.now_time + options.leeway < nbf {
return Err(InvalidClaims("token not yet valid (nbf)"))
}
()
}
None => ()
}
// Step 5: check issuer (iss)
match options.expected_issuer {
Some(expected) =>
match get_string_claim(claims, claim_iss) {
Some(actual) =>
if actual != expected {
return Err(
InvalidClaims(
"issuer mismatch: expected " + expected + ", got " + actual,
),
)
}
None => return Err(InvalidClaims("expected issuer but token has none"))
}
None => ()
}
// Step 6: check audience (aud)
match options.expected_audience {
Some(expected) =>
match get_string_claim(claims, claim_aud) {
Some(actual) =>
if actual != expected {
return Err(InvalidClaims("audience mismatch"))
}
None =>
return Err(InvalidClaims("expected audience but token has none"))
}
None => ()
}
// Step 7: check subject (sub)
match options.expected_subject {
Some(expected) =>
match get_string_claim(claims, claim_sub) {
Some(actual) =>
if actual != expected {
return Err(InvalidClaims("subject mismatch"))
}
None => return Err(InvalidClaims("expected subject but token has none"))
}
None => ()
}
Ok(claims)
}
///|
/// Convenience: validate with only signature + expiration check.
pub fn validate_with_expiry(
token : String,
secret : Bytes,
now : Int,
) -> Result[Json, JwtError] {
let opts = ValidationOptions::new(now)
validate(token, secret, opts)
}
///|
/// Check if a token is expired without verifying signature.
pub fn is_expired(token : String, now : Int, leeway : Int) -> Bool {
match decode_claims(token) {
Err(_) => true
Ok(claims) =>
match get_numeric_claim(claims, claim_exp) {
Some(exp) => now > exp + leeway
None => false
}
}
}