identity_kit/jose/claims.rs
1//! `JwtClaims`, the registered claims of RFC 7519 and every other claim of a JWT.
2
3use indexmap::IndexMap;
4use serde::de::Error as _;
5use serde::{Deserialize, Deserializer, Serialize};
6use serde_json::Value;
7
8use super::OneOrMany;
9
10/// A JWT claims set: the registered claims of RFC 7519 §4.1 as fields, every other claim
11/// (`vc`, `vp`, `nonce`, `cnf`, …) in `additional`.
12///
13/// Times are `NumericDate` values (RFC 7519 §2), whole seconds since the Unix epoch; a fraction
14/// is dropped on deserialization. With the `vc` feature, `JwtClaims::numeric_date`,
15/// `JwtClaims::timestamp` and the `with_exp`, `with_nbf` and `with_iat` setters convert from
16/// and to `chrono` times.
17///
18/// Decoding a token's payload with [`CompactJws::claims`](super::CompactJws::claims) checks
19/// nothing; validating the registered claims is the validators' job.
20///
21/// # Examples
22///
23/// ```
24/// use identity_kit::jose::JwtClaims;
25///
26/// let claims: JwtClaims = serde_json::from_str(
27/// r#"{"iss":"did:example:123","aud":["a","b"],"exp":1573029723,"nonce":"660!6345FSer"}"#,
28/// )?;
29/// assert_eq!(claims.iss.as_deref(), Some("did:example:123"));
30/// assert_eq!(claims.audiences().collect::<Vec<_>>(), ["a", "b"]);
31/// assert_eq!(claims.exp, Some(1573029723));
32/// assert_eq!(claims.additional["nonce"], "660!6345FSer");
33///
34/// let built = JwtClaims::new()
35/// .with_iss("did:example:123")
36/// .with_aud("did:example:verifier")
37/// .with_claim("nonce", "n-0S6_WzA2Mj");
38/// assert_eq!(
39/// serde_json::to_string(&built)?,
40/// r#"{"iss":"did:example:123","aud":"did:example:verifier","nonce":"n-0S6_WzA2Mj"}"#
41/// );
42/// # Ok::<(), serde_json::Error>(())
43/// ```
44#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
45#[non_exhaustive]
46pub struct JwtClaims {
47 /// `iss`: the issuer of the token.
48 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub iss: Option<String>,
50 /// `sub`: the subject of the token.
51 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub sub: Option<String>,
53 /// `aud`: the intended recipients.
54 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub aud: Option<OneOrMany<String>>,
56 /// `exp`: when the token expires.
57 #[serde(
58 default,
59 skip_serializing_if = "Option::is_none",
60 deserialize_with = "numeric_date"
61 )]
62 pub exp: Option<i64>,
63 /// `nbf`: when the token becomes valid.
64 #[serde(
65 default,
66 skip_serializing_if = "Option::is_none",
67 deserialize_with = "numeric_date"
68 )]
69 pub nbf: Option<i64>,
70 /// `iat`: when the token was issued.
71 #[serde(
72 default,
73 skip_serializing_if = "Option::is_none",
74 deserialize_with = "numeric_date"
75 )]
76 pub iat: Option<i64>,
77 /// `jti`: the identifier of the token.
78 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub jti: Option<String>,
80 /// Every other claim, in insertion order.
81 #[serde(flatten)]
82 pub additional: IndexMap<String, Value>,
83}
84
85/// Deserializes a `NumericDate` (RFC 7519 §2): a JSON number, possibly with a fraction, floored
86/// to whole seconds.
87#[expect(
88 clippy::cast_precision_loss,
89 clippy::cast_possible_truncation,
90 reason = "a fraction is floored to an `i64` only within the range `i64` covers"
91)]
92pub(crate) fn numeric_date<'de, D: Deserializer<'de>>(
93 deserializer: D,
94) -> Result<Option<i64>, D::Error> {
95 let Some(number) = Option::<serde_json::Number>::deserialize(deserializer)? else {
96 return Ok(None);
97 };
98 number
99 .as_i64()
100 .or_else(|| {
101 number
102 .as_f64()
103 .filter(|seconds| (i64::MIN as f64..i64::MAX as f64).contains(seconds))
104 .map(|seconds| seconds.floor() as i64)
105 })
106 .map(Some)
107 .ok_or_else(|| D::Error::custom(format!("NumericDate {number} is out of range")))
108}
109
110impl JwtClaims {
111 /// The registered claim names of RFC 7519 §4.1, which have fields of their own.
112 pub const REGISTERED: [&'static str; 7] = ["iss", "sub", "aud", "exp", "nbf", "iat", "jti"];
113
114 /// An empty claims set.
115 pub fn new() -> Self {
116 Self::default()
117 }
118
119 /// This claims set with `iss` set to `issuer`.
120 #[must_use]
121 pub fn with_iss(mut self, issuer: impl Into<String>) -> Self {
122 self.iss = Some(issuer.into());
123 self
124 }
125
126 /// This claims set with `sub` set to `subject`.
127 #[must_use]
128 pub fn with_sub(mut self, subject: impl Into<String>) -> Self {
129 self.sub = Some(subject.into());
130 self
131 }
132
133 /// This claims set with `jti` set to `id`.
134 #[must_use]
135 pub fn with_jti(mut self, id: impl Into<String>) -> Self {
136 self.jti = Some(id.into());
137 self
138 }
139
140 /// The audiences, in order; empty when `aud` is absent.
141 pub fn audiences(&self) -> impl Iterator<Item = &str> {
142 self.aud
143 .iter()
144 .flat_map(OneOrMany::iter)
145 .map(String::as_str)
146 }
147
148 /// This claims set with `aud` set to the one audience `audience`.
149 #[must_use]
150 pub fn with_aud(mut self, audience: impl Into<String>) -> Self {
151 self.aud = Some(OneOrMany::One(audience.into()));
152 self
153 }
154
155 /// This claims set with the claim `name` set to `value`.
156 ///
157 /// `name` should not be one of [`REGISTERED`](Self::REGISTERED), which have fields of their
158 /// own: the member would be serialized twice, and `vc::Jwt::sign` refuses such a claims set.
159 #[must_use]
160 pub fn with_claim(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
161 self.additional.insert(name.into(), value.into());
162 self
163 }
164}