use std::borrow::Cow;
use serde::{Deserialize, Serialize};
use crate::{jwk::PublicJwk, platform::SystemTime};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfirmationClaim {
#[serde(skip_serializing_if = "Option::is_none")]
pub jkt: Option<String>,
#[serde(rename = "x5t#S256", skip_serializing_if = "Option::is_none")]
pub x5t_s256: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub jwe: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub jku: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(bound(deserialize = "ExtraHeaders: serde::de::Deserialize<'de>"))]
pub struct JwtHeader<'a, ExtraHeaders: Clone> {
pub alg: Cow<'a, str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub typ: Option<Cow<'a, str>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub kid: Option<Cow<'a, str>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub crit: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub jwk: Option<PublicJwk>,
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub extra_headers: Option<Cow<'a, ExtraHeaders>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(bound(deserialize = "Claims: serde::de::Deserialize<'de>"))]
pub struct JwtClaims<'a, Claims: Clone> {
#[serde(skip_serializing_if = "Option::is_none")]
pub iss: Option<Cow<'a, str>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sub: Option<Cow<'a, str>>,
#[serde(
default,
skip_serializing_if = "Vec::is_empty",
with = "crate::serde_utils::string::string_or_vec"
)]
pub aud: Vec<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::serde_utils::time::option_unix_secs"
)]
pub iat: Option<SystemTime>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::serde_utils::time::option_unix_secs"
)]
pub exp: Option<SystemTime>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::serde_utils::time::option_unix_secs"
)]
pub nbf: Option<SystemTime>,
#[serde(skip_serializing_if = "Option::is_none")]
pub jti: Option<Cow<'a, str>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cnf: Option<ConfirmationClaim>,
#[serde(flatten)]
pub claims: Cow<'a, Claims>,
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use serde_json::json;
use super::*;
#[test]
fn aud_serialize_empty() {
let claims = JwtClaims::<'static, ()> {
iss: None,
sub: None,
aud: vec![],
iat: None,
exp: None,
nbf: None,
jti: None,
cnf: None,
claims: Cow::Owned(()),
};
let v = serde_json::to_value(&claims).unwrap();
assert!(v.get("aud").is_none());
}
#[test]
fn aud_serialize_single() {
let claims = JwtClaims::<'static, ()> {
iss: None,
sub: None,
aud: vec!["https://example.com".into()],
iat: None,
exp: None,
nbf: None,
jti: None,
cnf: None,
claims: Cow::Owned(()),
};
let v = serde_json::to_value(&claims).unwrap();
assert_eq!(v["aud"], json!("https://example.com"));
}
#[test]
fn aud_serialize_multiple() {
let claims = JwtClaims::<'static, ()> {
iss: None,
sub: None,
aud: vec!["a".into(), "b".into()],
iat: None,
exp: None,
nbf: None,
jti: None,
cnf: None,
claims: Cow::Owned(()),
};
let v = serde_json::to_value(&claims).unwrap();
assert_eq!(v["aud"], json!(["a", "b"]));
}
#[rstest]
#[case::string(r#"{"aud":"x"}"#, vec!["x"])]
#[case::array(r#"{"aud":["a","b"]}"#, vec!["a", "b"])]
#[case::null(r#"{"aud":null}"#, vec![])]
#[case::absent(r"{}", vec![])]
fn aud_deserialize(#[case] json: &str, #[case] expected: Vec<&str>) {
let c: JwtClaims<'_, ()> = serde_json::from_str(json).unwrap();
assert_eq!(c.aud, expected);
}
fn epoch_plus(secs: u64) -> SystemTime {
SystemTime::UNIX_EPOCH + crate::platform::Duration::from_secs(secs)
}
#[test]
fn timestamp_integer() {
let j = r#"{"iat":1234}"#;
let c: JwtClaims<'_, ()> = serde_json::from_str(j).unwrap();
assert_eq!(c.iat, Some(epoch_plus(1234)));
}
#[test]
fn timestamp_float_truncates() {
let j = r#"{"iat":1234.5}"#;
let c: JwtClaims<'_, ()> = serde_json::from_str(j).unwrap();
assert_eq!(c.iat, Some(epoch_plus(1234)));
}
#[test]
fn timestamp_negative_errors() {
let j = r#"{"iat":-1}"#;
let result = serde_json::from_str::<JwtClaims<'_, ()>>(j);
assert!(result.is_err());
}
#[test]
fn timestamp_overflowing_system_time_errors() {
for claim in ["iat", "exp", "nbf"] {
let j = format!(r#"{{"{claim}":18446744073709551615}}"#);
let result = serde_json::from_str::<JwtClaims<'_, ()>>(&j);
assert!(result.is_err(), "{claim} should be rejected");
}
}
#[test]
fn timestamp_null() {
let j = r#"{"iat":null}"#;
let c: JwtClaims<'_, ()> = serde_json::from_str(j).unwrap();
assert_eq!(c.iat, None);
}
#[test]
fn timestamp_absent() {
let j = r"{}";
let c: JwtClaims<'_, ()> = serde_json::from_str(j).unwrap();
assert_eq!(c.iat, None);
assert_eq!(c.exp, None);
assert_eq!(c.nbf, None);
}
#[test]
fn all_timestamps() {
let j = r#"{"iat":100,"exp":200,"nbf":50}"#;
let c: JwtClaims<'_, ()> = serde_json::from_str(j).unwrap();
assert_eq!(c.iat, Some(epoch_plus(100)));
assert_eq!(c.exp, Some(epoch_plus(200)));
assert_eq!(c.nbf, Some(epoch_plus(50)));
}
#[test]
fn confirmation_claim_roundtrip() {
let cnf = ConfirmationClaim {
jkt: Some("thumbprint-value".into()),
x5t_s256: Some("cert-thumbprint".into()),
jwe: None,
jku: None,
};
let json = serde_json::to_string(&cnf).unwrap();
assert!(json.contains("\"jkt\""));
assert!(json.contains("\"x5t#S256\""));
let roundtripped: ConfirmationClaim = serde_json::from_str(&json).unwrap();
assert_eq!(roundtripped.jkt.as_deref(), Some("thumbprint-value"));
assert_eq!(roundtripped.x5t_s256.as_deref(), Some("cert-thumbprint"));
}
#[test]
fn confirmation_claim_skips_none() {
let cnf = ConfirmationClaim {
jkt: None,
x5t_s256: None,
jwe: None,
jku: None,
};
let json = serde_json::to_string(&cnf).unwrap();
assert_eq!(json, "{}");
}
}