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.
87pub(crate) fn numeric_date<'de, D: Deserializer<'de>>(
88 deserializer: D,
89) -> Result<Option<i64>, D::Error> {
90 let Some(number) = Option::<serde_json::Number>::deserialize(deserializer)? else {
91 return Ok(None);
92 };
93 number
94 .as_i64()
95 .or_else(|| {
96 number
97 .as_f64()
98 .filter(|seconds| (i64::MIN as f64..i64::MAX as f64).contains(seconds))
99 .map(|seconds| seconds.floor() as i64)
100 })
101 .map(Some)
102 .ok_or_else(|| D::Error::custom(format!("NumericDate {number} is out of range")))
103}
104
105impl JwtClaims {
106 /// The registered claim names of RFC 7519 §4.1, which have fields of their own.
107 pub const REGISTERED: [&'static str; 7] = ["iss", "sub", "aud", "exp", "nbf", "iat", "jti"];
108
109 /// An empty claims set.
110 pub fn new() -> Self {
111 Self::default()
112 }
113
114 /// This claims set with `iss` set to `issuer`.
115 #[must_use]
116 pub fn with_iss(mut self, issuer: impl Into<String>) -> Self {
117 self.iss = Some(issuer.into());
118 self
119 }
120
121 /// This claims set with `sub` set to `subject`.
122 #[must_use]
123 pub fn with_sub(mut self, subject: impl Into<String>) -> Self {
124 self.sub = Some(subject.into());
125 self
126 }
127
128 /// This claims set with `jti` set to `id`.
129 #[must_use]
130 pub fn with_jti(mut self, id: impl Into<String>) -> Self {
131 self.jti = Some(id.into());
132 self
133 }
134
135 /// The audiences, in order; empty when `aud` is absent.
136 pub fn audiences(&self) -> impl Iterator<Item = &str> {
137 self.aud
138 .iter()
139 .flat_map(OneOrMany::iter)
140 .map(String::as_str)
141 }
142
143 /// This claims set with `aud` set to the one audience `audience`.
144 #[must_use]
145 pub fn with_aud(mut self, audience: impl Into<String>) -> Self {
146 self.aud = Some(OneOrMany::One(audience.into()));
147 self
148 }
149
150 /// This claims set with the claim `name` set to `value`.
151 ///
152 /// `name` should not be one of [`REGISTERED`](Self::REGISTERED), which have fields of their
153 /// own: the member would be serialized twice, and `vc::Jwt::sign` refuses such a claims set.
154 #[must_use]
155 pub fn with_claim(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
156 self.additional.insert(name.into(), value.into());
157 self
158 }
159}