use crate::error::{Error, Result};
use crate::header::Header;
use super::{Policy, TypMode};
pub const JWT: &str = "JWT";
pub const AT_JWT: &str = "at+jwt";
pub const ACCESS_TOKEN_REQUIRED_CLAIMS: [&str; 7] =
["iss", "exp", "aud", "sub", "client_id", "iat", "jti"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Layer {
Signed,
Outer,
}
const APPLICATION: &[u8] = b"application/";
pub(crate) fn media_type_eq(a: &str, b: &str) -> bool {
let (a_short, a_type, a_params) = split_media_type(a);
let (b_short, b_type, b_params) = split_media_type(b);
if a_params != b_params {
return false;
}
match (a_short, b_short) {
(true, false) => {
without_application(b_type).is_some_and(|b| b.eq_ignore_ascii_case(a_type))
}
(false, true) => {
without_application(a_type).is_some_and(|a| a.eq_ignore_ascii_case(b_type))
}
_ => a_type.eq_ignore_ascii_case(b_type),
}
}
fn split_media_type(value: &str) -> (bool, &[u8], Option<&[u8]>) {
let short = !value.contains('/');
match value.split_once(';') {
Some((media_type, params)) => (short, media_type.as_bytes(), Some(params.as_bytes())),
None => (short, value.as_bytes(), None),
}
}
fn without_application(media_type: &[u8]) -> Option<&[u8]> {
let (prefix, rest) = media_type.split_at_checked(APPLICATION.len())?;
prefix.eq_ignore_ascii_case(APPLICATION).then_some(rest)
}
pub(crate) fn check_jwt_header(
protected: &Header,
unprotected: Option<&Header>,
p: &Policy<'_>,
layer: Layer,
) -> Result<()> {
if let Some(unprotected) = unprotected {
if unprotected.typ.is_some() {
return Err(Error::InvalidHeader("typ must be in the protected header"));
}
if unprotected.cty.is_some() {
return Err(Error::InvalidHeader("cty must be in the protected header"));
}
}
match layer {
Layer::Signed => {
if protected.b64 == Some(false) {
return Err(Error::InvalidHeader("b64"));
}
if protected
.cty
.as_deref()
.is_some_and(|cty| media_type_eq(cty, JWT))
{
return Err(Error::InvalidHeader("cty"));
}
check_typ(protected.typ.as_deref(), p.typ_mode())
}
Layer::Outer => {
if !protected
.cty
.as_deref()
.is_some_and(|cty| media_type_eq(cty, JWT))
{
return Err(Error::InvalidHeader("cty"));
}
match protected.typ.as_deref() {
Some(typ) => check_typ(Some(typ), p.typ_mode()),
None => Ok(()),
}
}
}
}
fn check_typ(typ: Option<&str>, mode: &TypMode) -> Result<()> {
let accepted = match mode {
TypMode::Default => typ.is_none_or(|t| media_type_eq(t, JWT)),
TypMode::Exact(expected) => typ.is_some_and(|t| media_type_eq(t, expected)),
TypMode::Any => true,
};
if accepted {
Ok(())
} else {
Err(Error::InvalidHeader("typ"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::jwa::SigningAlg;
use crate::jwt::FixedClock;
const CLOCK: FixedClock = FixedClock(1_300_819_380);
const ES256: &[SigningAlg] = &[SigningAlg::ES256];
fn default_policy() -> Policy<'static> {
Policy::new(&CLOCK, ES256).unwrap()
}
fn header(members: &str) -> Header {
let json = if members.is_empty() {
r#"{"alg":"ES256"}"#.to_owned()
} else {
format!(r#"{{"alg":"ES256",{members}}}"#)
};
Header::from_json(json.as_bytes()).unwrap()
}
fn with_typ(typ: Option<&str>) -> Header {
match typ {
Some(t) => header(&format!(r#""typ":"{t}""#)),
None => header(""),
}
}
fn signed(h: &Header, p: &Policy<'_>) -> Result<()> {
check_jwt_header(h, None, p, Layer::Signed)
}
fn is_invalid_header(r: Result<()>) -> bool {
matches!(r, Err(Error::InvalidHeader(_)))
}
#[test]
fn default_mode_accepts_only_absent_or_jwt() {
let p = default_policy();
for ok in [
None,
Some("JWT"),
Some("jwt"),
Some("application/jwt"),
Some("Application/JWT"),
] {
assert_eq!(signed(&with_typ(ok), &p), Ok(()), "{ok:?}");
}
for bad in [
"at+jwt",
"secevent+jwt",
"JOSE",
"JOSE+JSON",
"jwt ",
"",
"text/jwt",
] {
assert!(
is_invalid_header(signed(&with_typ(Some(bad)), &p)),
"{bad:?}"
);
}
}
#[test]
fn explicit_mode_requires_an_equal_typ() {
let p = default_policy().expect_typ(AT_JWT);
for ok in [
"at+jwt",
"application/at+jwt",
"at+JWT",
"Application/AT+JWT",
] {
assert_eq!(signed(&with_typ(Some(ok)), &p), Ok(()), "{ok:?}");
}
assert!(is_invalid_header(signed(&with_typ(None), &p)));
for bad in ["JWT", "jwt", "secevent+jwt", "at+jwt;v=1", "at+jwt "] {
assert!(
is_invalid_header(signed(&with_typ(Some(bad)), &p)),
"{bad:?}"
);
}
let p = default_policy().expect_typ("application/secevent+jwt");
assert_eq!(signed(&with_typ(Some("secevent+jwt")), &p), Ok(()));
}
#[test]
fn any_mode_accepts_every_typ() {
let p = default_policy().any_typ();
for typ in [
None,
Some("JWT"),
Some("at+jwt"),
Some("JOSE"),
Some(""),
Some("x/y;z"),
] {
assert_eq!(signed(&with_typ(typ), &p), Ok(()), "{typ:?}");
}
}
#[test]
fn access_token_policy_expects_at_jwt() {
let p = Policy::access_token(&CLOCK, ES256, &["https://as"], &["https://rs"]).unwrap();
assert_eq!(signed(&with_typ(Some("application/at+jwt")), &p), Ok(()));
assert!(is_invalid_header(signed(&with_typ(Some(JWT)), &p)));
assert!(is_invalid_header(signed(&with_typ(None), &p)));
}
#[test]
fn signed_layer_rejects_unencoded_payloads() {
for p in [default_policy(), default_policy().any_typ()] {
let h = header(r#""b64":false,"crit":["b64"]"#);
assert_eq!(signed(&h, &p), Err(Error::InvalidHeader("b64")));
let h = header(r#""b64":true,"crit":["b64"]"#);
assert_eq!(signed(&h, &p), Ok(()));
}
}
#[test]
fn signed_layer_rejects_jws_in_jws_nesting() {
for p in [default_policy(), default_policy().any_typ()] {
for cty in ["JWT", "jwt", "application/jwt", "APPLICATION/Jwt"] {
let h = header(&format!(r#""cty":"{cty}""#));
assert_eq!(signed(&h, &p), Err(Error::InvalidHeader("cty")), "{cty:?}");
}
for cty in ["text/plain", "JOSE", "jwt;v=1", "at+jwt"] {
let h = header(&format!(r#""cty":"{cty}""#));
assert_eq!(signed(&h, &p), Ok(()), "{cty:?}");
}
}
}
#[track_caller]
fn assert_eq_both_ways(a: &str, b: &str) {
assert!(media_type_eq(a, b), "{a:?} should equal {b:?}");
assert!(media_type_eq(b, a), "{b:?} should equal {a:?}");
}
#[track_caller]
fn assert_ne_both_ways(a: &str, b: &str) {
assert!(!media_type_eq(a, b), "{a:?} should not equal {b:?}");
assert!(!media_type_eq(b, a), "{b:?} should not equal {a:?}");
}
fn outer(h: &Header, p: &Policy<'_>) -> Result<()> {
check_jwt_header(h, None, p, Layer::Outer)
}
#[test]
fn outer_layer_requires_cty_jwt() {
for p in [default_policy(), default_policy().any_typ()] {
assert_eq!(outer(&header(""), &p), Err(Error::InvalidHeader("cty")));
for cty in ["JWT", "jwt", "application/jwt", "Application/JWT"] {
let h = header(&format!(r#""cty":"{cty}""#));
assert_eq!(outer(&h, &p), Ok(()), "{cty:?}");
}
for cty in ["JOSE", "text/plain", "at+jwt", "jwt;v=1", "jwt ", ""] {
let h = header(&format!(r#""cty":"{cty}""#));
assert_eq!(outer(&h, &p), Err(Error::InvalidHeader("cty")), "{cty:?}");
}
}
}
#[test]
fn outer_typ_uses_the_inner_mode_when_present() {
let outer_with = |typ: Option<&str>| match typ {
Some(t) => header(&format!(r#""cty":"JWT","typ":"{t}""#)),
None => header(r#""cty":"JWT""#),
};
let p = default_policy();
for ok in [None, Some("JWT"), Some("application/jwt")] {
assert_eq!(outer(&outer_with(ok), &p), Ok(()), "{ok:?}");
}
for bad in ["at+jwt", "JOSE"] {
let r = outer(&outer_with(Some(bad)), &p);
assert_eq!(r, Err(Error::InvalidHeader("typ")), "{bad:?}");
}
let p = default_policy().expect_typ(AT_JWT);
for ok in [None, Some("at+jwt"), Some("application/AT+JWT")] {
assert_eq!(outer(&outer_with(ok), &p), Ok(()), "{ok:?}");
}
for bad in ["JWT", "secevent+jwt"] {
let r = outer(&outer_with(Some(bad)), &p);
assert_eq!(r, Err(Error::InvalidHeader("typ")), "{bad:?}");
}
let p = default_policy().any_typ();
for typ in [None, Some("JWT"), Some("at+jwt"), Some("JOSE")] {
assert_eq!(outer(&outer_with(typ), &p), Ok(()), "{typ:?}");
}
}
#[test]
fn unprotected_typ_or_cty_is_rejected_on_both_layers() {
let protected = [
(Layer::Signed, header(r#""typ":"JWT""#)),
(Layer::Outer, header(r#""cty":"JWT""#)),
];
let unprotected = [
(Header::from_json(br#"{"typ":"JWT"}"#).unwrap(), "typ"),
(Header::from_json(br#"{"typ":"at+jwt"}"#).unwrap(), "typ"),
(Header::from_json(br#"{"cty":"JWT"}"#).unwrap(), "cty"),
(
Header::from_json(br#"{"cty":"text/plain"}"#).unwrap(),
"cty",
),
(
Header::from_json(br#"{"kid":"k","typ":""}"#).unwrap(),
"typ",
),
];
for p in [
default_policy(),
default_policy().expect_typ(AT_JWT),
default_policy().any_typ(),
] {
for (layer, prot) in &protected {
for (unprot, member) in &unprotected {
let r = check_jwt_header(prot, Some(unprot), &p, *layer);
let detail = if *member == "cty" {
"cty must be in the protected header"
} else {
"typ must be in the protected header"
};
assert_eq!(r, Err(Error::InvalidHeader(detail)), "{layer:?} {unprot:?}");
}
}
}
}
#[test]
fn outer_cty_only_in_the_unprotected_header_is_not_the_nesting_marker() {
let p = default_policy();
let unprot = Header::from_json(br#"{"cty":"JWT"}"#).unwrap();
let r = check_jwt_header(&header(""), Some(&unprot), &p, Layer::Outer);
assert!(is_invalid_header(r));
}
#[test]
fn other_unprotected_members_are_ignored() {
let p = default_policy();
let unprot = Header::from_json(br#"{"kid":"k1","x-custom":true}"#).unwrap();
let signed_h = header(r#""typ":"JWT""#);
let outer_h = header(r#""cty":"JWT""#);
assert_eq!(
check_jwt_header(&signed_h, Some(&unprot), &p, Layer::Signed),
Ok(())
);
assert_eq!(
check_jwt_header(&outer_h, Some(&unprot), &p, Layer::Outer),
Ok(())
);
let empty = Header::new();
assert_eq!(
check_jwt_header(&signed_h, Some(&empty), &p, Layer::Signed),
Ok(())
);
let bad = header(r#""typ":"at+jwt""#);
assert!(is_invalid_header(check_jwt_header(
&bad,
Some(&unprot),
&p,
Layer::Signed
)));
}
#[test]
fn short_and_full_forms_are_equal_ignoring_case() {
let at_jwt = [
"at+jwt",
"application/at+jwt",
"at+JWT",
"Application/AT+JWT",
];
for a in at_jwt {
for b in at_jwt {
assert_eq_both_ways(a, b);
}
}
let jwt = ["JWT", "jwt", "application/jwt", "APPLICATION/JWT"];
for a in jwt {
for b in jwt {
assert_eq_both_ways(a, b);
}
}
}
#[test]
fn a_slash_anywhere_suppresses_the_implicit_prefix() {
assert_ne_both_ways(r#"example;part="1/2""#, r#"application/example;part="1/2""#);
assert_eq_both_ways(r#"example;part="1/2""#, r#"EXAMPLE;part="1/2""#);
assert_eq_both_ways(
r#"application/example;part="1/2""#,
r#"Application/Example;part="1/2""#,
);
}
#[test]
fn parameters_compare_exactly() {
assert_ne_both_ways("a+jwt;v=1", "a+jwt;v=1X");
assert_ne_both_ways("a+jwt;v=x", "a+jwt;v=X");
assert_ne_both_ways("a+jwt;v=1", "a+jwt");
assert_ne_both_ways("a+jwt;", "a+jwt");
assert_eq_both_ways("A+JWT;v=1", "a+jwt;v=1");
assert_eq_both_ways("application/A+JWT;v=1", "a+jwt;v=1");
assert_eq_both_ways("a+jwt;v=1;w=2", "a+JWT;v=1;w=2");
assert_ne_both_ways("a+jwt;v=1;w=2", "a+jwt;w=2;v=1");
}
#[test]
fn different_types_and_untrimmed_values_differ() {
assert_ne_both_ways("at+jwt", "jwt");
assert_ne_both_ways("at+jwt", "at+jwt ");
assert_ne_both_ways("at+jwt", " at+jwt");
assert_ne_both_ways("JWT", "application/jwt ");
assert_ne_both_ways("jwt", "text/jwt");
assert_ne_both_ways("jwt", "applicationjwt");
assert_ne_both_ways("jwt", "application//jwt");
assert_ne_both_ways("JWT", "JOSE");
assert_ne_both_ways("JWT", "");
assert_eq_both_ways("jwt\u{e9}", "JWT\u{e9}");
assert_ne_both_ways("jwt\u{e9}", "JWT\u{c9}");
assert_ne_both_ways("\u{e9}pplication/jwt", "jwt");
}
#[test]
fn constants_are_the_rfc_values() {
assert_eq!(JWT, "JWT");
assert_eq!(AT_JWT, "at+jwt");
assert_eq!(
ACCESS_TOKEN_REQUIRED_CLAIMS,
["iss", "exp", "aud", "sub", "client_id", "iat", "jti"]
);
}
#[test]
fn media_type_eq_edge_inputs() {
assert_eq_both_ways("", "");
assert_eq_both_ways("", "application/");
assert_eq_both_ways("", "APPLICATION/");
assert_ne_both_ways("application/", "jwt");
assert_ne_both_ways("application/", "application//");
assert_ne_both_ways("/jwt", "jwt");
assert_ne_both_ways("/jwt", "application/jwt");
assert_eq_both_ways("/JWT", "/jwt");
assert_ne_both_ways("a;b/c", "application/a;b/c");
assert_eq_both_ways("a;b/c", "A;b/c");
assert_ne_both_ways("a;b/c", "A;B/c");
assert_ne_both_ways("jwt;x=a/b", "application/jwt;x=a/b");
assert_ne_both_ways("jwt;x=a/b", "jwt");
assert_eq_both_ways("JWT;x=a/b", "jwt;x=a/b");
assert_eq_both_ways("jwt;x=ab", "application/JWT;x=ab");
assert_ne_both_ways("jwt;;", "jwt;");
assert_eq_both_ways(";", ";");
assert_ne_both_ways(";", "");
assert_ne_both_ways("applicatio\u{e9}/jwt", "jwt");
assert_ne_both_ways("\u{1F600}\u{1F600}\u{1F600}\u{1F600}", "jwt");
assert_ne_both_ways("jwt;\u{e9}", "jwt;\u{c9}");
assert_eq_both_ways("\u{e9}/\u{e9}", "\u{e9}/\u{e9}");
assert_ne_both_ways("jwt\0", "jwt");
assert_eq_both_ways("JWT\0", "jwt\0");
}
#[test]
fn modes_per_layer_and_check_order() {
let exact = default_policy().expect_typ(AT_JWT);
assert_eq!(outer(&header(r#""cty":"JWT""#), &exact), Ok(()));
assert_eq!(
signed(&header(""), &exact),
Err(Error::InvalidHeader("typ"))
);
let h = header(r#""b64":false,"crit":["b64"],"cty":"JWT","typ":"at+jwt""#);
assert_eq!(
signed(&h, &default_policy()),
Err(Error::InvalidHeader("b64"))
);
let h = header(r#""cty":"JWT","typ":"at+jwt""#);
assert_eq!(
signed(&h, &default_policy()),
Err(Error::InvalidHeader("cty"))
);
let h = header(r#""cty":"JOSE","typ":"at+jwt""#);
assert_eq!(
outer(&h, &default_policy()),
Err(Error::InvalidHeader("cty"))
);
let h = header(r#""b64":false,"crit":["b64"],"cty":"JWT""#);
assert_eq!(outer(&h, &default_policy()), Ok(()));
let unprot = Header::from_json(br#"{"typ":"JWT"}"#).unwrap();
let h = header(r#""b64":false,"crit":["b64"]"#);
assert_eq!(
check_jwt_header(&h, Some(&unprot), &default_policy(), Layer::Signed),
Err(Error::InvalidHeader("typ must be in the protected header"))
);
assert!(is_invalid_header(check_jwt_header(
&header(r#""cty":"JWT""#),
Some(&unprot),
&default_policy().any_typ(),
Layer::Outer
)));
}
mod props {
use super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(1000))]
#[test]
fn media_type_eq_never_panics_and_is_reflexive_and_symmetric(
a in "\\PC*", b in "\\PC*"
) {
prop_assert!(media_type_eq(&a, &a));
prop_assert_eq!(media_type_eq(&a, &b), media_type_eq(&b, &a));
}
#[test]
fn short_form_equals_full_form_when_no_slash(
t in "[a-zA-Z0-9.+-]{0,12}", params in "(;[a-z0-9=]{0,6}){0,2}"
) {
let short = format!("{t}{params}");
let full = format!("application/{t}{params}");
prop_assert!(media_type_eq(&short, &full));
let shouted = format!("{}{params}", t.to_ascii_uppercase());
prop_assert!(media_type_eq(&shouted, &full));
}
#[test]
fn check_jwt_header_never_panics(typ in "\\PC*", cty in "\\PC*") {
let mut h = Header::new();
h.typ = Some(typ);
h.cty = Some(cty);
for p in [default_policy(), default_policy().expect_typ(AT_JWT), default_policy().any_typ()] {
let _ = check_jwt_header(&h, None, &p, Layer::Signed);
let _ = check_jwt_header(&h, Some(&h), &p, Layer::Outer);
}
}
}
}
}