use core::fmt;
use crate::error::{Error, Result};
use crate::jwa::SigningAlg;
use crate::jws::VerifyOptions;
use super::clock::{Clock, now_checked};
use super::typ::{ACCESS_TOKEN_REQUIRED_CLAIMS, AT_JWT};
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum TypMode {
#[default]
Default,
Exact(String),
Any,
}
#[derive(Clone)]
pub struct Policy<'a> {
clock: &'a dyn Clock,
jws: VerifyOptions<'a>,
leeway: u32,
issuers: Vec<String>,
audiences: Vec<String>,
subject: Option<String>,
max_age: Option<u32>,
required: Vec<String>,
string_client_id: bool,
require_exp: bool,
ignore_audience: bool,
typ: TypMode,
max_key_candidates: usize,
}
impl<'a> Policy<'a> {
pub const DEFAULT_LEEWAY: u32 = 60;
pub const MAX_LEEWAY: u32 = 600;
pub const DEFAULT_MAX_KEY_CANDIDATES: usize = 8;
pub fn new(clock: &'a dyn Clock, allowed_algs: &'a [SigningAlg]) -> Result<Self> {
Ok(Self {
clock,
jws: VerifyOptions::new(allowed_algs)?,
leeway: Self::DEFAULT_LEEWAY,
issuers: Vec::new(),
audiences: Vec::new(),
subject: None,
max_age: None,
required: Vec::new(),
string_client_id: false,
require_exp: true,
ignore_audience: false,
typ: TypMode::Default,
max_key_candidates: Self::DEFAULT_MAX_KEY_CANDIDATES,
})
}
pub fn access_token(
clock: &'a dyn Clock,
allowed_algs: &'a [SigningAlg],
issuers: &[&str],
audiences: &[&str],
) -> Result<Self> {
let policy = Self::new(clock, allowed_algs)?;
if issuers.is_empty() {
return Err(Error::InvalidClaim("iss"));
}
if audiences.is_empty() {
return Err(Error::InvalidClaim("aud"));
}
let mut policy = ACCESS_TOKEN_REQUIRED_CLAIMS
.iter()
.fold(policy, |policy, claim| policy.require(claim));
policy.string_client_id = true;
Ok(policy
.issuers(issuers)
.audiences(audiences)
.expect_typ(AT_JWT))
}
pub fn issuer(mut self, iss: &str) -> Self {
self.issuers.push(iss.to_owned());
self
}
pub fn issuers(self, iss: &[&str]) -> Self {
iss.iter().fold(self, |policy, iss| policy.issuer(iss))
}
pub fn audience(mut self, aud: &str) -> Self {
self.audiences.push(aud.to_owned());
self
}
pub fn audiences(self, aud: &[&str]) -> Self {
aud.iter().fold(self, |policy, aud| policy.audience(aud))
}
pub fn subject(mut self, sub: &str) -> Self {
self.subject = Some(sub.to_owned());
self
}
pub fn leeway(mut self, secs: u32) -> Result<Self> {
if secs > Self::MAX_LEEWAY {
return Err(Error::InvalidClaim("leeway"));
}
self.leeway = secs;
Ok(self)
}
pub fn max_age(mut self, secs: u32) -> Self {
self.max_age = Some(secs);
self
}
pub fn require(mut self, claim: &str) -> Self {
if !self.required.iter().any(|name| name == claim) {
self.required.push(claim.to_owned());
}
self
}
pub fn allow_missing_exp(mut self) -> Self {
self.require_exp = false;
self
}
pub fn ignore_audience(mut self) -> Self {
self.ignore_audience = true;
self
}
pub fn expect_typ(mut self, typ: &str) -> Self {
self.typ = TypMode::Exact(typ.to_owned());
self
}
pub fn any_typ(mut self) -> Self {
self.typ = TypMode::Any;
self
}
pub fn max_key_candidates(mut self, n: usize) -> Self {
self.max_key_candidates = n;
self
}
pub fn max_input_len(mut self, n: usize) -> Self {
self.jws = self.jws.max_input_len(n);
self
}
}
impl<'a> Policy<'a> {
pub(crate) fn now(&self) -> Result<i64> {
now_checked(self.clock)
}
pub(crate) fn verify_options(&self) -> VerifyOptions<'a> {
self.jws
}
#[cfg(test)]
pub(crate) fn allowed_algs(&self) -> &'a [SigningAlg] {
self.jws.allowed_algs()
}
pub(crate) fn leeway_secs(&self) -> u32 {
self.leeway
}
pub(crate) fn accepted_issuers(&self) -> &[String] {
&self.issuers
}
pub(crate) fn accepted_audiences(&self) -> &[String] {
&self.audiences
}
pub(crate) fn expected_subject(&self) -> Option<&str> {
self.subject.as_deref()
}
pub(crate) fn max_age_secs(&self) -> Option<u32> {
self.max_age
}
pub(crate) fn required_claims(&self) -> &[String] {
&self.required
}
pub(crate) fn requires_string_client_id(&self) -> bool {
self.string_client_id
}
pub(crate) fn requires_exp(&self) -> bool {
self.require_exp
}
pub(crate) fn ignores_audience(&self) -> bool {
self.ignore_audience
}
pub(crate) fn typ_mode(&self) -> &TypMode {
&self.typ
}
pub(crate) fn key_candidate_limit(&self) -> usize {
self.max_key_candidates
}
}
impl fmt::Debug for Policy<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Policy")
.field("clock", &format_args!("dyn Clock"))
.field("jws", &self.jws)
.field("leeway", &self.leeway)
.field("issuers", &self.issuers)
.field("audiences", &self.audiences)
.field("subject", &self.subject)
.field("max_age", &self.max_age)
.field("required", &self.required)
.field("string_client_id", &self.string_client_id)
.field("require_exp", &self.require_exp)
.field("ignore_audience", &self.ignore_audience)
.field("typ", &self.typ)
.field("max_key_candidates", &self.max_key_candidates)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::json::MAX_SAFE_INTEGER;
use crate::jwt::FixedClock;
const CLOCK: FixedClock = FixedClock(1_300_819_380);
const ES256: &[SigningAlg] = &[SigningAlg::ES256];
fn policy() -> Policy<'static> {
Policy::new(&CLOCK, ES256).unwrap()
}
#[test]
fn new_refuses_lists_that_verify_nothing_or_none() {
let unknown = [SigningAlg::Unknown("XS999".into())];
let with_none = [SigningAlg::HS256, SigningAlg::None];
for algs in [&[][..], &[SigningAlg::None], &unknown, &with_none] {
assert_eq!(
Policy::new(&CLOCK, algs).err(),
Some(Error::AlgorithmNotAllowed),
"{algs:?}"
);
}
assert!(Policy::new(&CLOCK, &[SigningAlg::HS256, SigningAlg::EdDSA]).is_ok());
}
#[test]
fn defaults_are_adr_0027() {
let p = policy();
assert!(p.requires_exp());
assert_eq!(p.leeway_secs(), 60);
assert_eq!(p.typ_mode(), &TypMode::Default);
assert!(p.accepted_issuers().is_empty());
assert!(p.accepted_audiences().is_empty());
assert_eq!(p.expected_subject(), None);
assert_eq!(p.max_age_secs(), None);
assert!(p.required_claims().is_empty());
assert!(!p.ignores_audience());
assert_eq!(p.key_candidate_limit(), 8);
assert_eq!(p.allowed_algs(), ES256);
assert_eq!(p.verify_options().allowed_algs(), ES256);
let jws = format!("{:?}", p.verify_options());
assert!(jws.contains("max_input_len: 1048576"), "{jws}");
assert_eq!(p.now(), Ok(1_300_819_380));
}
#[test]
fn leeway_is_capped_at_600_seconds() {
assert_eq!(policy().leeway(600).map(|p| p.leeway_secs()), Ok(600));
assert_eq!(policy().leeway(0).map(|p| p.leeway_secs()), Ok(0));
assert_eq!(
policy().leeway(601).err(),
Some(Error::InvalidClaim("leeway"))
);
assert_eq!(
policy().leeway(u32::MAX).err(),
Some(Error::InvalidClaim("leeway"))
);
}
#[test]
fn builders_set_what_they_name() {
let p = policy()
.issuer("https://a")
.issuers(&["https://b", "https://c"])
.audience("api")
.audiences(&["web"])
.subject("bob")
.subject("alice")
.max_age(300)
.require("client_id")
.require("jti")
.require("client_id")
.allow_missing_exp()
.ignore_audience()
.max_key_candidates(2)
.max_input_len(4096);
assert_eq!(
p.accepted_issuers(),
["https://a", "https://b", "https://c"]
);
assert_eq!(p.accepted_audiences(), ["api", "web"]);
assert_eq!(p.expected_subject(), Some("alice"));
assert_eq!(p.max_age_secs(), Some(300));
assert_eq!(p.required_claims(), ["client_id", "jti"]);
assert!(!p.requires_exp());
assert!(p.ignores_audience());
assert_eq!(p.key_candidate_limit(), 2);
let jws = format!("{:?}", p.verify_options());
assert!(jws.contains("max_input_len: 4096"), "{jws}");
}
#[test]
fn typ_modes() {
assert_eq!(
policy().expect_typ("secevent+jwt").typ_mode(),
&TypMode::Exact("secevent+jwt".into())
);
assert_eq!(policy().any_typ().typ_mode(), &TypMode::Any);
assert_eq!(
policy().any_typ().expect_typ("x").typ_mode(),
&TypMode::Exact("x".into())
);
}
#[test]
fn access_token_sets_the_rfc_9068_requirements() {
let p = Policy::access_token(&CLOCK, ES256, &["https://as"], &["https://rs"]).unwrap();
assert_eq!(p.typ_mode(), &TypMode::Exact("at+jwt".into()));
assert_eq!(
p.required_claims(),
["iss", "exp", "aud", "sub", "client_id", "iat", "jti"]
);
assert_eq!(p.accepted_issuers(), ["https://as"]);
assert_eq!(p.accepted_audiences(), ["https://rs"]);
assert!(p.requires_exp());
assert!(!p.ignores_audience());
assert_eq!(p.leeway_secs(), 60);
assert_eq!(p.allowed_algs(), ES256);
assert!(p.requires_string_client_id());
assert!(!policy().require("client_id").requires_string_client_id());
}
#[test]
fn access_token_needs_algorithms_issuers_and_audiences() {
let none = [SigningAlg::None];
assert_eq!(
Policy::access_token(&CLOCK, &none, &[], &[]).err(),
Some(Error::AlgorithmNotAllowed)
);
assert_eq!(
Policy::access_token(&CLOCK, ES256, &[], &["https://rs"]).err(),
Some(Error::InvalidClaim("iss"))
);
assert_eq!(
Policy::access_token(&CLOCK, ES256, &["https://as"], &[]).err(),
Some(Error::InvalidClaim("aud"))
);
}
#[test]
fn now_rejects_a_clock_out_of_range() {
let late = FixedClock(MAX_SAFE_INTEGER + 1);
let p = Policy::new(&late, ES256).unwrap();
assert_eq!(p.now(), Err(Error::InvalidClaim("clock")));
}
#[test]
fn debug_shows_no_clock_internals() {
let clock = FixedClock(987_654_321);
let p = Policy::new(&clock, ES256).unwrap().issuer("https://a");
let debug = format!("{p:?}");
assert!(!debug.contains("987654321"), "{debug}");
assert!(!debug.contains("FixedClock"), "{debug}");
assert!(debug.starts_with("Policy { clock: dyn Clock, "), "{debug}");
assert!(debug.contains("https://a"), "{debug}");
let alternate = format!("{p:#?}");
assert!(!alternate.contains("987654321"), "{alternate}");
}
#[test]
fn policy_is_send_and_sync() {
fn shared<T: Send + Sync>(_: &T) {}
shared(&policy());
}
}