use core::cmp::Ordering;
use crate::error::{Error, Result};
use crate::json::Value;
use super::claims::REGISTERED_CLAIMS;
use super::typ::ACCESS_TOKEN_REQUIRED_CLAIMS;
use super::{Claims, NumericDate, Policy};
const OTHER_REQUIRED_CLAIM: &str = "required";
const CLIENT_ID: &str = "client_id";
pub(crate) fn validate_claims(c: &Claims, p: &Policy<'_>) -> Result<()> {
check_presence(c, p)?;
let mut now = Now::new(p);
check_exp(c, p, &mut now)?;
check_nbf(c, p, &mut now)?;
check_iat(c, p, &mut now)?;
check_iss(c, p)?;
check_sub(c, p)?;
check_aud(c, p)
}
fn check_iss(c: &Claims, p: &Policy<'_>) -> Result<()> {
let accepted = p.accepted_issuers();
if accepted.is_empty() {
return Ok(());
}
match &c.iss {
Some(iss) if accepted.contains(iss) => Ok(()),
_ => Err(Error::InvalidClaim("iss")),
}
}
fn check_sub(c: &Claims, p: &Policy<'_>) -> Result<()> {
match p.expected_subject() {
Some(expected) if c.sub.as_deref() != Some(expected) => Err(Error::InvalidClaim("sub")),
_ => Ok(()),
}
}
fn check_aud(c: &Claims, p: &Policy<'_>) -> Result<()> {
let accepted = p.accepted_audiences();
if accepted.is_empty() {
return aud_verdict(c.aud.is_none() || p.ignores_audience());
}
aud_verdict(
c.aud
.as_ref()
.is_some_and(|aud| aud.values().iter().any(|value| accepted.contains(value))),
)
}
fn aud_verdict(ok: bool) -> Result<()> {
if ok {
Ok(())
} else {
Err(Error::InvalidClaim("aud"))
}
}
struct Now<'p, 'a> {
policy: &'p Policy<'a>,
secs: Option<i64>,
}
impl<'p, 'a> Now<'p, 'a> {
fn new(policy: &'p Policy<'a>) -> Self {
Self { policy, secs: None }
}
fn get(&mut self) -> Result<i64> {
match self.secs {
Some(secs) => Ok(secs),
None => {
let secs = self.policy.now()?;
self.secs = Some(secs);
Ok(secs)
}
}
}
}
fn check_exp(c: &Claims, p: &Policy<'_>, now: &mut Now<'_, '_>) -> Result<()> {
match &c.exp {
Some(exp) if exp.is_expired(now.get()?, p.leeway_secs()) => Err(Error::TokenExpired),
_ => Ok(()),
}
}
fn check_nbf(c: &Claims, p: &Policy<'_>, now: &mut Now<'_, '_>) -> Result<()> {
match &c.nbf {
Some(nbf) if nbf.is_not_yet_valid(now.get()?, p.leeway_secs()) => {
Err(Error::TokenNotYetValid)
}
_ => Ok(()),
}
}
fn check_iat(c: &Claims, p: &Policy<'_>, now: &mut Now<'_, '_>) -> Result<()> {
let Some(max_age) = p.max_age_secs() else {
return Ok(());
};
let iat = c.iat.as_ref().ok_or(Error::InvalidClaim("iat"))?;
if iat_outside_window(iat, now.get()?, max_age, p.leeway_secs()) {
return Err(Error::InvalidClaim("iat"));
}
Ok(())
}
fn iat_outside_window(iat: &NumericDate, now: i64, max_age: u32, leeway: u32) -> bool {
let leeway = i64::from(leeway);
let latest = now.saturating_add(leeway);
let earliest = now
.saturating_sub(i64::from(max_age))
.saturating_sub(leeway);
iat.cmp_secs(latest) == Ordering::Greater || iat.cmp_secs(earliest) == Ordering::Less
}
fn check_presence(c: &Claims, p: &Policy<'_>) -> Result<()> {
let required = p.required_claims();
let present = [
c.iss.is_some(),
c.sub.is_some(),
c.aud.is_some(),
c.exp.is_some(),
c.nbf.is_some(),
c.iat.is_some(),
c.jti.is_some(),
];
let implied = [
!p.accepted_issuers().is_empty(),
p.expected_subject().is_some(),
!p.accepted_audiences().is_empty(),
p.requires_exp(),
false,
p.max_age_secs().is_some(),
false,
];
for ((name, present), implied) in REGISTERED_CLAIMS.into_iter().zip(present).zip(implied) {
if !present && (implied || required.iter().any(|r| r == name)) {
return Err(Error::InvalidClaim(name));
}
}
for name in required {
if !REGISTERED_CLAIMS.contains(&name.as_str()) && !satisfies(c.get(name), name, p) {
return Err(Error::InvalidClaim(detail(name)));
}
}
Ok(())
}
fn satisfies(value: Option<&Value>, name: &str, p: &Policy<'_>) -> bool {
match value {
None => false,
Some(Value::String(_)) => true,
Some(_) => !(name == CLIENT_ID && p.requires_string_client_id()),
}
}
fn detail(name: &str) -> &'static str {
ACCESS_TOKEN_REQUIRED_CLAIMS
.into_iter()
.find(|known| *known == name)
.unwrap_or(OTHER_REQUIRED_CLAIM)
}
#[cfg(test)]
mod tests {
use super::*;
use core::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
use crate::json::{MAX_SAFE_INTEGER, Number};
use crate::json::{Object, Value};
use crate::jwa::SigningAlg;
use crate::jwt::{Audience, Clock, FixedClock, NumericDate};
const ES256: &[SigningAlg] = &[SigningAlg::ES256];
struct PanicClock;
impl Clock for PanicClock {
fn now(&self) -> Result<i64> {
panic!("the clock was read although no time check runs");
}
}
fn untimed() -> Policy<'static> {
Policy::new(&PanicClock, ES256).unwrap()
}
fn date(secs: i64) -> Option<NumericDate> {
NumericDate::from_secs(secs)
}
#[test]
fn missing_exp_is_rejected_by_default_without_reading_the_clock() {
assert_eq!(
validate_claims(&Claims::new(), &untimed()),
Err(Error::InvalidClaim("exp"))
);
}
#[test]
fn allow_missing_exp_accepts_a_token_without_exp_or_a_clock_read() {
let p = untimed().allow_missing_exp();
assert_eq!(validate_claims(&Claims::new(), &p), Ok(()));
let mut c = Claims::new();
c.iat = date(i64::from(u32::MAX));
c.jti = Some("id".into());
assert_eq!(validate_claims(&c, &p), Ok(()));
}
#[test]
fn configured_issuer_subject_audience_and_max_age_imply_presence() {
let p = untimed()
.issuer("https://a")
.subject("alice")
.audience("api")
.max_age(300);
let mut c = Claims::new();
assert_eq!(validate_claims(&c, &p), Err(Error::InvalidClaim("iss")));
c.iss = Some("https://a".into());
assert_eq!(validate_claims(&c, &p), Err(Error::InvalidClaim("sub")));
c.sub = Some("alice".into());
assert_eq!(validate_claims(&c, &p), Err(Error::InvalidClaim("aud")));
c.aud = Some(Audience::One("api".into()));
assert_eq!(validate_claims(&c, &p), Err(Error::InvalidClaim("exp")));
let p = p.allow_missing_exp();
assert_eq!(validate_claims(&c, &p), Err(Error::InvalidClaim("iat")));
}
#[test]
fn required_claim_may_hold_any_json_type() {
let p = untimed().allow_missing_exp().require("client_id");
let mut c = Claims::new();
assert_eq!(
validate_claims(&c, &p),
Err(Error::InvalidClaim("client_id"))
);
c.insert("client", "s6BhdRkqt3").unwrap();
assert_eq!(
validate_claims(&c, &p),
Err(Error::InvalidClaim("client_id"))
);
let mut object = Object::new();
object.insert("id", "s6BhdRkqt3");
for value in [
Value::from("s6BhdRkqt3"),
Value::Number(Number::from_i64(42).unwrap()),
Value::from(object),
Value::Null,
Value::Bool(false),
Value::from(Vec::new()),
] {
c.insert("client_id", value.clone()).unwrap();
assert_eq!(validate_claims(&c, &p), Ok(()), "{value:?}");
}
}
#[test]
fn required_names_follow_the_registered_order_then_the_callers() {
let clock = FixedClock(0);
let first_tenant = Policy::new(&clock, ES256)
.unwrap()
.allow_missing_exp()
.require("x-tenant")
.require("jti")
.require("client_id")
.require("nbf");
let first_client = Policy::new(&clock, ES256)
.unwrap()
.allow_missing_exp()
.require("client_id")
.require("nbf")
.require("x-tenant")
.require("jti");
let mut c = Claims::new();
for p in [&first_tenant, &first_client] {
assert_eq!(validate_claims(&c, p), Err(Error::InvalidClaim("nbf")));
}
c.nbf = date(-100);
for p in [&first_tenant, &first_client] {
assert_eq!(validate_claims(&c, p), Err(Error::InvalidClaim("jti")));
}
c.jti = Some("id".into());
assert_eq!(
validate_claims(&c, &first_tenant),
Err(Error::InvalidClaim("required"))
);
assert_eq!(
validate_claims(&c, &first_client),
Err(Error::InvalidClaim("client_id"))
);
c.insert("x-tenant", "t1").unwrap();
for p in [&first_tenant, &first_client] {
assert_eq!(
validate_claims(&c, p),
Err(Error::InvalidClaim("client_id"))
);
}
c.insert("client_id", Value::Null).unwrap();
for p in [&first_tenant, &first_client] {
assert_eq!(validate_claims(&c, p), Ok(()));
}
}
#[test]
fn require_exp_still_applies_after_allow_missing_exp() {
let p = untimed().allow_missing_exp().require("exp");
assert_eq!(
validate_claims(&Claims::new(), &p),
Err(Error::InvalidClaim("exp"))
);
}
fn timed(clock: &FixedClock, leeway: u32) -> Policy<'_> {
Policy::new(clock, ES256).unwrap().leeway(leeway).unwrap()
}
struct CountingClock(i64, AtomicUsize);
impl Clock for CountingClock {
fn now(&self) -> Result<i64> {
self.1.fetch_add(1, AtomicOrdering::Relaxed);
Ok(self.0)
}
}
#[test]
fn exp_boundaries_of_adr_0027_section_3() {
for (exp, leeway, now, expected) in [
(1000.0, 60, 1059, Ok(())),
(1000.0, 60, 1060, Err(Error::TokenExpired)),
(1000.0, 0, 999, Ok(())),
(1000.0, 0, 1000, Err(Error::TokenExpired)),
(1000.5, 0, 1000, Ok(())),
(1000.5, 0, 1001, Err(Error::TokenExpired)),
(-1.0, 0, 0, Err(Error::TokenExpired)),
(-1.0, 60, 0, Ok(())),
(-61.0, 60, 0, Err(Error::TokenExpired)),
] {
let clock = FixedClock(now);
let mut c = Claims::new();
c.exp = NumericDate::from_f64(exp);
assert_eq!(
validate_claims(&c, &timed(&clock, leeway)),
expected,
"exp {exp}, leeway {leeway}, now {now}"
);
}
}
#[test]
fn nbf_boundaries_of_adr_0027_section_3() {
for (nbf, leeway, now, expected) in [
(Some(1000.0), 60, 940, Ok(())),
(Some(1000.0), 60, 939, Err(Error::TokenNotYetValid)),
(Some(1000.0), 0, 1000, Ok(())),
(Some(1000.0), 0, 999, Err(Error::TokenNotYetValid)),
(Some(1000.5), 0, 1000, Err(Error::TokenNotYetValid)),
(Some(1000.5), 0, 1001, Ok(())),
(Some(-100.0), 60, 0, Ok(())),
(Some(-100.0), 0, -100, Ok(())),
(None, 0, 0, Ok(())),
] {
let clock = FixedClock(now);
let mut c = Claims::new();
c.nbf = nbf.and_then(NumericDate::from_f64);
let p = timed(&clock, leeway).allow_missing_exp();
assert_eq!(
validate_claims(&c, &p),
expected,
"nbf {nbf:?}, leeway {leeway}, now {now}"
);
}
}
#[test]
fn future_iat_is_accepted_without_max_age() {
let now = 1_300_819_380;
let clock = FixedClock(now);
let mut c = Claims::new();
c.exp = date(now + 7200);
c.iat = date(now + 3600);
assert_eq!(validate_claims(&c, &timed(&clock, 60)), Ok(()));
assert_eq!(validate_claims(&c, &timed(&clock, 0)), Ok(()));
}
#[test]
fn iat_window_with_max_age() {
let now = 1_300_819_380;
let clock = FixedClock(now);
let p = timed(&clock, 60).max_age(300);
let mut c = Claims::new();
c.exp = date(now + 7200);
assert_eq!(validate_claims(&c, &p), Err(Error::InvalidClaim("iat")));
for (offset, expected) in [
(61.0, Err(Error::InvalidClaim("iat"))),
(60.5, Err(Error::InvalidClaim("iat"))),
(60.0, Ok(())),
(0.0, Ok(())),
(-360.0, Ok(())),
(-360.5, Err(Error::InvalidClaim("iat"))),
(-361.0, Err(Error::InvalidClaim("iat"))),
] {
c.iat = NumericDate::from_f64(now as f64 + offset);
assert_eq!(validate_claims(&c, &p), expected, "iat now{offset:+}");
}
}
#[test]
fn iat_window_is_exact_for_any_now_and_bound() {
let max = NumericDate::from_secs(MAX_SAFE_INTEGER).unwrap();
let min = NumericDate::from_secs(-MAX_SAFE_INTEGER).unwrap();
let half = NumericDate::from_f64(0.5).unwrap();
for (iat, now, max_age, leeway, outside) in [
(max, i64::MAX, u32::MAX, u32::MAX, true),
(min, i64::MAX, u32::MAX, u32::MAX, true),
(max, i64::MAX, 0, u32::MAX, true),
(max, i64::MIN, u32::MAX, u32::MAX, true),
(min, i64::MIN, u32::MAX, u32::MAX, true),
(min, i64::MIN + 1, 0, 0, true),
(max, MAX_SAFE_INTEGER, 0, 0, false),
(max, MAX_SAFE_INTEGER - 1, 0, 0, true),
(max, MAX_SAFE_INTEGER + 1, 0, 0, true),
(max, MAX_SAFE_INTEGER + 1, 1, 0, false),
(max, MAX_SAFE_INTEGER + 600, 0, 600, false),
(max, MAX_SAFE_INTEGER + 601, 0, 600, true),
(min, -MAX_SAFE_INTEGER, 0, 0, false),
(min, -MAX_SAFE_INTEGER + 1, 0, 0, true),
(min, -MAX_SAFE_INTEGER - 1, 0, 0, true),
(min, -MAX_SAFE_INTEGER - 1, 0, 1, false),
(half, 0, 0, 0, true),
(half, 1, 0, 0, true),
(half, 1, 1, 0, false),
(half, 0, 0, 1, false),
] {
assert_eq!(
iat_outside_window(&iat, now, max_age, leeway),
outside,
"iat {}, now {now}, max_age {max_age}, leeway {leeway}",
iat.as_f64()
);
}
}
#[test]
fn the_clock_is_read_once_and_only_for_a_time_check() {
let clock = CountingClock(1000, AtomicUsize::new(0));
let p = Policy::new(&clock, ES256).unwrap().max_age(300);
let mut c = Claims::new();
c.exp = date(2000);
c.nbf = date(900);
c.iat = date(950);
assert_eq!(validate_claims(&c, &p), Ok(()));
assert_eq!(clock.1.load(AtomicOrdering::Relaxed), 1);
for claim in ["exp", "nbf", "iat"] {
let clock = CountingClock(1000, AtomicUsize::new(0));
let p = Policy::new(&clock, ES256).unwrap().allow_missing_exp();
let mut c = Claims::new();
let p = match claim {
"exp" => {
c.exp = date(2000);
p
}
"nbf" => {
c.nbf = date(900);
p
}
_ => {
c.iat = date(950);
p.max_age(300)
}
};
assert_eq!(validate_claims(&c, &p), Ok(()), "{claim}");
assert_eq!(clock.1.load(AtomicOrdering::Relaxed), 1, "{claim}");
}
}
#[test]
fn a_clock_out_of_range_fails_the_first_time_check() {
let clock = FixedClock(MAX_SAFE_INTEGER + 1);
let p = Policy::new(&clock, ES256).unwrap();
let mut c = Claims::new();
c.exp = date(1000);
assert_eq!(validate_claims(&c, &p), Err(Error::InvalidClaim("clock")));
let p = Policy::new(&clock, ES256).unwrap().allow_missing_exp();
let mut c = Claims::new();
c.nbf = date(1000);
assert_eq!(validate_claims(&c, &p), Err(Error::InvalidClaim("clock")));
let p = p.max_age(300);
let mut c = Claims::new();
c.iat = date(1000);
assert_eq!(validate_claims(&c, &p), Err(Error::InvalidClaim("clock")));
let p = Policy::new(&clock, ES256).unwrap();
assert_eq!(
validate_claims(&Claims::new(), &p),
Err(Error::InvalidClaim("exp"))
);
let p = p.allow_missing_exp();
assert_eq!(validate_claims(&Claims::new(), &p), Ok(()));
}
fn many(values: &[&str]) -> Option<Audience> {
Some(Audience::Many(
values.iter().map(|v| (*v).to_owned()).collect(),
))
}
fn one(value: &str) -> Option<Audience> {
Some(Audience::One(value.to_owned()))
}
#[test]
fn iss_must_equal_an_accepted_issuer_exactly() {
let p = untimed()
.allow_missing_exp()
.issuers(&["https://a", "https://b"]);
for (iss, expected) in [
(Some("https://b"), Ok(())),
(Some("https://a"), Ok(())),
(Some("https://B"), Err(Error::InvalidClaim("iss"))),
(Some("https://b/"), Err(Error::InvalidClaim("iss"))),
(Some("https://c"), Err(Error::InvalidClaim("iss"))),
(Some(""), Err(Error::InvalidClaim("iss"))),
(None, Err(Error::InvalidClaim("iss"))),
] {
let mut c = Claims::new();
c.iss = iss.map(str::to_owned);
assert_eq!(validate_claims(&c, &p), expected, "{iss:?}");
}
let p = untimed().allow_missing_exp();
for iss in [Some("https://anyone"), None] {
let mut c = Claims::new();
c.iss = iss.map(str::to_owned);
assert_eq!(validate_claims(&c, &p), Ok(()), "{iss:?}");
}
}
#[test]
fn sub_must_equal_the_expected_subject_exactly() {
let p = untimed().allow_missing_exp().subject("alice");
for (sub, expected) in [
(Some("alice"), Ok(())),
(Some("Alice"), Err(Error::InvalidClaim("sub"))),
(Some("alice "), Err(Error::InvalidClaim("sub"))),
(None, Err(Error::InvalidClaim("sub"))),
] {
let mut c = Claims::new();
c.sub = sub.map(str::to_owned);
assert_eq!(validate_claims(&c, &p), expected, "{sub:?}");
}
let mut c = Claims::new();
c.sub = Some("bob".into());
assert_eq!(validate_claims(&c, &untimed().allow_missing_exp()), Ok(()));
}
#[test]
fn aud_must_contain_an_accepted_audience() {
const BAD: Result<()> = Err(Error::InvalidClaim("aud"));
let api = untimed().allow_missing_exp().audience("api");
let api_web = untimed().allow_missing_exp().audiences(&["api", "web"]);
for (p, aud, expected) in [
(&api, one("api"), Ok(())),
(&api, many(&["x", "api"]), Ok(())),
(&api, many(&["api"]), Ok(())),
(&api, many(&["x"]), BAD),
(&api, many(&[]), BAD),
(&api, None, BAD),
(&api, one("API"), BAD),
(&api, one("x"), BAD),
(&api, many(&["API", "ap"]), BAD),
(&api_web, many(&["web"]), Ok(())),
(&api_web, one("web"), Ok(())),
(&api_web, many(&["x", "y"]), BAD),
] {
let mut c = Claims::new();
c.aud = aud.clone();
assert_eq!(validate_claims(&c, p), expected, "{aud:?}");
}
}
#[test]
fn aud_without_a_configured_audience() {
let strict = untimed().allow_missing_exp();
let ignoring = untimed().allow_missing_exp().ignore_audience();
for (aud, strict_expected) in [
(None, Ok(())),
(one("api"), Err(Error::InvalidClaim("aud"))),
(many(&[]), Err(Error::InvalidClaim("aud"))),
(many(&["api", "web"]), Err(Error::InvalidClaim("aud"))),
] {
let mut c = Claims::new();
c.aud = aud.clone();
assert_eq!(validate_claims(&c, &strict), strict_expected, "{aud:?}");
assert_eq!(validate_claims(&c, &ignoring), Ok(()), "{aud:?}");
}
let configured = untimed()
.allow_missing_exp()
.ignore_audience()
.audience("api");
for aud in [one("x"), many(&[]), None] {
let mut c = Claims::new();
c.aud = aud.clone();
assert_eq!(
validate_claims(&c, &configured),
Err(Error::InvalidClaim("aud")),
"{aud:?}"
);
}
}
#[test]
fn access_token_policy_requires_the_rfc_9068_claims() {
let now = 1_300_819_380;
let clock = FixedClock(now);
let p = Policy::access_token(&clock, ES256, &["https://as"], &["https://rs"]).unwrap();
let mut c = Claims::new();
c.iss = Some("https://as".into());
c.exp = date(now + 300);
c.aud = one("https://rs");
c.sub = Some("5ba552d67".into());
c.iat = date(now - 10);
c.insert("client_id", "s6BhdRkqt3").unwrap();
assert_eq!(validate_claims(&c, &p), Err(Error::InvalidClaim("jti")));
c.jti = Some("dbe39bf3a3ba4238a513f51d6e1691c4".into());
assert_eq!(validate_claims(&c, &p), Ok(()));
for name in ["iss", "exp", "aud", "sub", "iat", "jti"] {
let mut missing = c.clone();
match name {
"iss" => missing.iss = None,
"exp" => missing.exp = None,
"aud" => missing.aud = None,
"sub" => missing.sub = None,
"iat" => missing.iat = None,
_ => missing.jti = None,
}
assert_eq!(
validate_claims(&missing, &p),
Err(Error::InvalidClaim(name)),
"{name}"
);
}
let mut without_client = Claims::new();
without_client.iss = c.iss.clone();
without_client.exp = c.exp;
without_client.aud = c.aud.clone();
without_client.sub = c.sub.clone();
without_client.iat = c.iat;
without_client.jti = c.jti.clone();
assert_eq!(
validate_claims(&without_client, &p),
Err(Error::InvalidClaim("client_id"))
);
let mut wrong = c.clone();
wrong.iss = Some("https://other".into());
assert_eq!(validate_claims(&wrong, &p), Err(Error::InvalidClaim("iss")));
let mut wrong = c;
wrong.aud = one("https://other");
assert_eq!(validate_claims(&wrong, &p), Err(Error::InvalidClaim("aud")));
}
fn access_token_claims(now: i64, client_id: Value) -> Claims {
let mut c = Claims::new();
c.iss = Some("https://as".into());
c.exp = date(now + 300);
c.aud = one("https://rs");
c.sub = Some("5ba552d67".into());
c.iat = date(now - 10);
c.jti = Some("dbe39bf3a3ba4238a513f51d6e1691c4".into());
c.insert("client_id", client_id).unwrap();
c
}
fn non_strings() -> [Value; 5] {
let mut object = Object::new();
object.insert("id", "s6BhdRkqt3");
[
Value::Null,
Value::Number(Number::from_i64(5).unwrap()),
Value::Bool(true),
Value::from(vec![Value::from("a")]),
Value::from(object),
]
}
#[test]
fn access_token_policy_requires_client_id_to_be_a_string() {
let now = 1_300_819_380;
let clock = FixedClock(now);
let p = Policy::access_token(&clock, ES256, &["https://as"], &["https://rs"]).unwrap();
for id in ["s6BhdRkqt3", ""] {
assert_eq!(
validate_claims(&access_token_claims(now, Value::from(id)), &p),
Ok(()),
"{id:?}"
);
}
for value in non_strings() {
assert_eq!(
validate_claims(&access_token_claims(now, value.clone()), &p),
Err(Error::InvalidClaim("client_id")),
"{value:?}"
);
}
}
#[test]
fn non_string_client_id_fails_at_its_j4_position() {
let now = ORDER_NOW;
let p = Policy::access_token(&PanicClock, ES256, &["https://as"], &["https://rs"]).unwrap();
for value in non_strings() {
let mut c = access_token_claims(now, value.clone());
c.jti = None;
assert_eq!(
validate_claims(&c, &p),
Err(Error::InvalidClaim("jti")),
"{value:?}"
);
let mut c = access_token_claims(now, value.clone());
c.exp = date(now - 3600);
c.nbf = date(now + 3600);
c.iss = Some("https://evil".into());
c.aud = one("https://other");
assert_eq!(
validate_claims(&c, &p),
Err(Error::InvalidClaim("client_id")),
"{value:?}"
);
}
let p = p.require("x-tenant");
let c = access_token_claims(now, Value::Null);
assert_eq!(
validate_claims(&c, &p),
Err(Error::InvalidClaim("client_id"))
);
}
#[test]
fn builder_calls_after_the_profile_keep_the_string_rule() {
let now = 1_300_819_380;
let clock = FixedClock(now);
let p = Policy::access_token(&clock, ES256, &["https://as"], &["https://rs"])
.unwrap()
.require("client_id")
.any_typ()
.leeway(0)
.unwrap();
assert_eq!(
validate_claims(&access_token_claims(now, Value::Null), &p),
Err(Error::InvalidClaim("client_id"))
);
}
#[test]
fn plain_require_client_id_keeps_any_json_type() {
let now = 1_300_819_380;
let clock = FixedClock(now);
let p = Policy::new(&clock, ES256)
.unwrap()
.issuer("https://as")
.audience("https://rs")
.expect_typ("at+jwt");
let p = ACCESS_TOKEN_REQUIRED_CLAIMS
.iter()
.fold(p, |p, claim| p.require(claim));
for value in non_strings() {
assert_eq!(
validate_claims(&access_token_claims(now, value.clone()), &p),
Ok(()),
"{value:?}"
);
}
}
const ORDER_NOW: i64 = 1_300_819_380;
fn order_policy(clock: &dyn Clock) -> Policy<'_> {
Policy::new(clock, ES256)
.unwrap()
.issuer("https://as")
.subject("alice")
.audience("api")
.max_age(300)
.require("client_id")
}
const STEPS: [(&str, Error); 7] = [
("J4", Error::InvalidClaim("client_id")),
("J5", Error::TokenExpired),
("J6", Error::TokenNotYetValid),
("J7", Error::InvalidClaim("iat")),
("J8", Error::InvalidClaim("iss")),
("J9", Error::InvalidClaim("sub")),
("J10", Error::InvalidClaim("aud")),
];
fn order_claims(broken: u8) -> Claims {
let fails = |step: usize| broken & (1 << step) != 0;
let now = ORDER_NOW;
let mut c = Claims::new();
if !fails(0) {
c.insert("client_id", "s6BhdRkqt3").unwrap();
}
c.exp = date(if fails(1) { now - 120 } else { now + 300 });
c.nbf = date(if fails(2) { now + 120 } else { now - 10 });
c.iat = date(if fails(3) { now - 1000 } else { now - 10 });
c.iss = Some(
if fails(4) {
"https://evil"
} else {
"https://as"
}
.into(),
);
c.sub = Some(if fails(5) { "mallory" } else { "alice" }.into());
c.aud = one(if fails(6) { "other" } else { "api" });
c
}
#[test]
fn the_first_failing_step_decides_for_every_combination() {
let clock = FixedClock(ORDER_NOW);
let p = order_policy(&clock);
let mut pairs = 0;
for broken in 0..(1_u8 << STEPS.len()) {
let expected = STEPS
.iter()
.enumerate()
.find(|(i, _)| broken & (1 << i) != 0)
.map(|(_, (_, error))| error.clone());
let names: Vec<&str> = STEPS
.iter()
.enumerate()
.filter(|(i, _)| broken & (1 << i) != 0)
.map(|(_, (name, _))| *name)
.collect();
assert_eq!(
validate_claims(&order_claims(broken), &p).err(),
expected,
"failing steps {names:?}"
);
if names.len() == 2 {
pairs += 1;
}
if broken & 1 != 0 {
assert_eq!(
validate_claims(&order_claims(broken), &order_policy(&PanicClock)).err(),
expected,
"failing steps {names:?}, unread clock"
);
}
}
assert_eq!(pairs, 21);
}
#[test]
fn named_ordering_cases_of_adr_0027_section_9() {
let clock = FixedClock(ORDER_NOW);
let p = Policy::new(&clock, ES256)
.unwrap()
.issuer("https://as")
.audience("api");
let mut c = Claims::new();
c.exp = date(ORDER_NOW - 3600);
c.iss = Some("https://evil".into());
c.aud = one("other");
assert_eq!(validate_claims(&c, &p), Err(Error::TokenExpired));
let mut c = Claims::new();
c.exp = date(ORDER_NOW + 3600);
c.nbf = date(ORDER_NOW + 600);
c.iss = Some("https://as".into());
c.aud = many(&["other"]);
assert_eq!(validate_claims(&c, &p), Err(Error::TokenNotYetValid));
let mut c = Claims::new();
c.nbf = date(ORDER_NOW + 600);
c.iss = Some("https://as".into());
c.aud = one("api");
assert_eq!(validate_claims(&c, &p), Err(Error::InvalidClaim("exp")));
let untimed = untimed().issuer("https://as").audience("api");
assert_eq!(
validate_claims(&c, &untimed),
Err(Error::InvalidClaim("exp"))
);
let p = Policy::new(&clock, ES256).unwrap();
let mut c = Claims::new();
c.exp = date(ORDER_NOW - 3600);
c.aud = one("api");
assert_eq!(validate_claims(&c, &p), Err(Error::TokenExpired));
c.exp = date(ORDER_NOW + 3600);
assert_eq!(validate_claims(&c, &p), Err(Error::InvalidClaim("aud")));
}
#[test]
fn extreme_clock_and_settings_never_overflow_or_panic() {
for now in [MAX_SAFE_INTEGER, -MAX_SAFE_INTEGER] {
let clock = FixedClock(now);
let p = timed(&clock, Policy::MAX_LEEWAY).max_age(u32::MAX);
let mut c = Claims::new();
c.exp = date(MAX_SAFE_INTEGER);
c.nbf = date(-MAX_SAFE_INTEGER);
for (iat, expected) in [
(now, Ok(())),
(now + 600, Ok(())),
(now - i64::from(u32::MAX) - 600, Ok(())),
(now + 601, Err(Error::InvalidClaim("iat"))),
(
now - i64::from(u32::MAX) - 601,
Err(Error::InvalidClaim("iat")),
),
] {
c.iat = date(iat);
let expected = match (iat.abs() > MAX_SAFE_INTEGER, expected) {
(true, _) => Err(Error::InvalidClaim("iat")),
(false, e) => e,
};
let got = validate_claims(&c, &p);
assert_eq!(got, expected, "now {now}, iat {iat}");
}
}
let clock = FixedClock(1_000_000);
let p = timed(&clock, 60).max_age(u32::MAX);
let mut c = Claims::new();
c.exp = date(2_000_000);
c.iat = NumericDate::from_f64(1_000_060.5);
assert_eq!(validate_claims(&c, &p), Err(Error::InvalidClaim("iat")));
c.iat = NumericDate::from_f64(1_000_060.0);
assert_eq!(validate_claims(&c, &p), Ok(()));
let clock = FixedClock(MAX_SAFE_INTEGER);
let p = timed(&clock, 0);
let mut c = Claims::new();
c.exp = date(MAX_SAFE_INTEGER);
assert_eq!(validate_claims(&c, &p), Err(Error::TokenExpired));
let clock = FixedClock(-MAX_SAFE_INTEGER);
let p = timed(&clock, 0).allow_missing_exp();
let mut c = Claims::new();
c.nbf = date(-MAX_SAFE_INTEGER);
assert_eq!(validate_claims(&c, &p), Ok(()));
c.nbf = date(-MAX_SAFE_INTEGER + 1);
assert_eq!(validate_claims(&c, &p), Err(Error::TokenNotYetValid));
let clock = FixedClock(-MAX_SAFE_INTEGER - 1);
let p = timed(&clock, 0).allow_missing_exp();
assert_eq!(validate_claims(&c, &p), Err(Error::InvalidClaim("clock")));
}
#[test]
fn duplicate_audiences_change_nothing() {
let policy_twice = untimed()
.allow_missing_exp()
.audience("api")
.audience("api");
let policy_once = untimed().allow_missing_exp().audience("api");
for (aud, expected) in [
(many(&["api", "api"]), Ok(())),
(many(&["x", "x"]), Err(Error::InvalidClaim("aud"))),
(one("api"), Ok(())),
(one("x"), Err(Error::InvalidClaim("aud"))),
] {
let mut c = Claims::new();
c.aud = aud.clone();
assert_eq!(validate_claims(&c, &policy_twice), expected, "{aud:?}");
assert_eq!(validate_claims(&c, &policy_once), expected, "{aud:?}");
}
}
#[test]
fn error_details_never_echo_claim_values_or_caller_names() {
let clock = FixedClock(ORDER_NOW);
let secret = "SECRET-VALUE-9f3a";
let mut c = Claims::new();
c.iss = Some(secret.into());
c.sub = Some(secret.into());
c.aud = one(secret);
c.exp = date(ORDER_NOW + 100);
c.insert("x-custom", secret).unwrap();
let policies = [
timed(&clock, 0).issuer("https://as"),
timed(&clock, 0).subject("alice"),
timed(&clock, 0).audience("api"),
timed(&clock, 0),
timed(&clock, 0).require("SECRET-NAME-77"),
timed(&clock, 0).require("jti"),
];
let mut failed = 0;
for p in &policies {
if let Err(e) = validate_claims(&c, p) {
failed += 1;
for text in [format!("{e}"), format!("{e:?}")] {
assert!(!text.contains("SECRET"), "{text}");
}
}
}
assert_eq!(failed, 6);
c.exp = date(ORDER_NOW - 100);
let e = validate_claims(&c, &timed(&clock, 0)).unwrap_err();
assert_eq!(e, Error::TokenExpired);
}
#[test]
fn required_exp_wins_over_allow_missing_exp_in_either_order() {
let a = untimed().require("exp").allow_missing_exp();
let b = untimed().allow_missing_exp().require("exp");
for p in [&a, &b] {
assert_eq!(
validate_claims(&Claims::new(), p),
Err(Error::InvalidClaim("exp"))
);
}
}
}