Skip to main content

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}