use std::borrow::Cow;
use serde::{Deserialize, Deserializer, Serialize};
use crate::jwk::PublicJwk;
#[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>,
}
fn deserialize_whole_or_fractional<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
where
D: Deserializer<'de>,
{
use serde::de;
use crate::serde_utils::time::SecsVisitor;
struct WholeOrFractionalOrNull;
impl<'de> de::Visitor<'de> for WholeOrFractionalOrNull {
type Value = Option<u64>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a non-negative numeric value, or null")
}
fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
SecsVisitor.visit_u64(v).map(Some)
}
fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
SecsVisitor.visit_i64(v).map(Some)
}
fn visit_f64<E: de::Error>(self, v: f64) -> Result<Self::Value, E> {
SecsVisitor.visit_f64(v).map(Some)
}
fn visit_some<D: Deserializer<'de>>(
self,
deserializer: D,
) -> Result<Self::Value, D::Error> {
deserializer.deserialize_any(self)
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
}
deserializer.deserialize_any(WholeOrFractionalOrNull)
}
#[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",
deserialize_with = "deserialize_whole_or_fractional"
)]
pub iat: Option<u64>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_whole_or_fractional"
)]
pub exp: Option<u64>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_whole_or_fractional"
)]
pub nbf: Option<u64>,
#[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 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"]));
}
#[test]
fn aud_deserialize_string() {
let j = r#"{"aud":"x"}"#;
let c: JwtClaims<'_, ()> = serde_json::from_str(j).unwrap();
assert_eq!(c.aud, vec!["x"]);
}
#[test]
fn aud_deserialize_array() {
let j = r#"{"aud":["a","b"]}"#;
let c: JwtClaims<'_, ()> = serde_json::from_str(j).unwrap();
assert_eq!(c.aud, vec!["a", "b"]);
}
#[test]
fn aud_deserialize_null() {
let j = r#"{"aud":null}"#;
let c: JwtClaims<'_, ()> = serde_json::from_str(j).unwrap();
assert!(c.aud.is_empty());
}
#[test]
fn aud_deserialize_absent() {
let j = r"{}";
let c: JwtClaims<'_, ()> = serde_json::from_str(j).unwrap();
assert!(c.aud.is_empty());
}
#[test]
fn timestamp_integer() {
let j = r#"{"iat":1234}"#;
let c: JwtClaims<'_, ()> = serde_json::from_str(j).unwrap();
assert_eq!(c.iat, Some(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(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_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(100));
assert_eq!(c.exp, Some(200));
assert_eq!(c.nbf, Some(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, "{}");
}
}