Skip to main content

identity_kit/vc/
jwt.rs

1//! `Jwt`, a compact JWS carrying a JSON claims set, and `JwtClaims`, the registered claims of
2//! RFC 7519.
3
4use core::fmt;
5use core::str::FromStr;
6
7use chrono::{DateTime, Utc};
8use indexmap::IndexMap;
9use serde::de::Error as _;
10use serde::{Deserialize, Deserializer, Serialize};
11use serde_json::Value;
12
13use crate::did::OneOrMany;
14use crate::jose::{CompactJws, JwsError, JwsHeader, Signer, encode_compact};
15
16/// A JSON Web Token (RFC 7519) in compact serialization, kept as the string it is.
17///
18/// Construction checks that the string is a well-formed compact JWS with a supported `alg`;
19/// nothing is verified. The type serializes as a plain string, which is how a presentation
20/// carries its credentials.
21///
22/// # Examples
23///
24/// ```
25/// use identity_kit::vc::Jwt;
26///
27/// // RFC 8037 Appendix A.4.
28/// let jwt: Jwt = "eyJhbGciOiJFZERTQSJ9.RXhhbXBsZSBvZiBFZDI1NTE5IHNpZ25pbmc.\
29///     hgyY0il_MGCjP0JzlnLWG1PPOt7-09PGcvMg3AIbQR6dWbhijcNR4ki4iylGjg5BhVsPt9g7sVvpAr_MuM0KAg"
30///     .parse()?;
31/// assert_eq!(jwt.decode()?.payload(), b"Example of Ed25519 signing");
32/// assert!("not.a.jwt".parse::<Jwt>().is_err());
33/// # Ok::<(), identity_kit::jose::JwsError>(())
34/// ```
35#[derive(Clone, PartialEq, Eq, Hash, Serialize)]
36#[serde(transparent)]
37pub struct Jwt(String);
38
39impl Jwt {
40    /// Wraps `token` after checking it is a compact JWS this crate can decode.
41    ///
42    /// # Errors
43    ///
44    /// The [`JwsError`] of [`CompactJws::decode`].
45    pub fn new(token: impl Into<String>) -> Result<Self, JwsError> {
46        let token = token.into();
47        CompactJws::decode(&token)?;
48        Ok(Self(token))
49    }
50
51    /// Signs `claims` under `header` with `signer`.
52    ///
53    /// The claims are serialized as compact JSON in their field order, `additional` last.
54    ///
55    /// # Errors
56    ///
57    /// [`JwsError::InvalidPayload`] when `additional` holds a registered claim, which would put
58    /// the member in the payload twice; otherwise as [`encode_compact`].
59    pub async fn sign(
60        header: &JwsHeader,
61        claims: &JwtClaims,
62        signer: &dyn Signer,
63    ) -> Result<Self, JwsError> {
64        if let Some(name) = claims.registered_in_additional() {
65            return Err(JwsError::InvalidPayload(format!(
66                "registered claim `{name}` is set under `additional`; set its field instead"
67            )));
68        }
69        let payload = serde_json::to_vec(claims).expect("a claims set serializes");
70        encode_compact(header, &payload, signer).await.map(Self)
71    }
72
73    /// The compact serialization.
74    pub fn as_str(&self) -> &str {
75        &self.0
76    }
77
78    /// The compact serialization.
79    pub fn into_string(self) -> String {
80        self.0
81    }
82
83    /// The decoded parts, unverified.
84    ///
85    /// # Errors
86    ///
87    /// Never, in practice: construction decoded the same string.
88    pub fn decode(&self) -> Result<CompactJws, JwsError> {
89        CompactJws::decode(&self.0)
90    }
91}
92
93impl fmt::Debug for Jwt {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        f.debug_tuple("Jwt").field(&self.0).finish()
96    }
97}
98
99impl fmt::Display for Jwt {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        f.write_str(&self.0)
102    }
103}
104
105impl AsRef<str> for Jwt {
106    fn as_ref(&self) -> &str {
107        &self.0
108    }
109}
110
111impl FromStr for Jwt {
112    type Err = JwsError;
113
114    fn from_str(s: &str) -> Result<Self, Self::Err> {
115        Self::new(s)
116    }
117}
118
119impl TryFrom<String> for Jwt {
120    type Error = JwsError;
121
122    fn try_from(token: String) -> Result<Self, Self::Error> {
123        Self::new(token)
124    }
125}
126
127impl<'de> Deserialize<'de> for Jwt {
128    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
129        Self::new(String::deserialize(deserializer)?).map_err(D::Error::custom)
130    }
131}
132
133/// A JWT claims set: the registered claims of RFC 7519 §4.1 as fields, every other claim
134/// (`vc`, `vp`, `nonce`, `cnf`, …) in `additional`.
135///
136/// Times are `NumericDate` values, seconds since the Unix epoch; [`JwtClaims::numeric_date`]
137/// and [`JwtClaims::timestamp`] convert.
138///
139/// # Examples
140///
141/// ```
142/// use identity_kit::vc::JwtClaims;
143///
144/// let claims: JwtClaims = serde_json::from_str(
145///     r#"{"iss":"did:example:123","exp":1573029723,"nonce":"660!6345FSer"}"#,
146/// )?;
147/// assert_eq!(claims.iss.as_deref(), Some("did:example:123"));
148/// assert_eq!(
149///     claims.exp.and_then(JwtClaims::timestamp).map(|t| t.to_rfc3339()),
150///     Some("2019-11-06T08:42:03+00:00".to_owned())
151/// );
152/// assert_eq!(claims.additional["nonce"], "660!6345FSer");
153/// # Ok::<(), serde_json::Error>(())
154/// ```
155#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
156#[non_exhaustive]
157pub struct JwtClaims {
158    /// `iss`: the issuer of the token.
159    #[serde(default, skip_serializing_if = "Option::is_none")]
160    pub iss: Option<String>,
161    /// `sub`: the subject of the token.
162    #[serde(default, skip_serializing_if = "Option::is_none")]
163    pub sub: Option<String>,
164    /// `aud`: the intended recipients.
165    #[serde(default, skip_serializing_if = "Option::is_none")]
166    pub aud: Option<OneOrMany<String>>,
167    /// `exp`: when the token expires.
168    #[serde(
169        default,
170        skip_serializing_if = "Option::is_none",
171        deserialize_with = "numeric_date"
172    )]
173    pub exp: Option<i64>,
174    /// `nbf`: when the token becomes valid.
175    #[serde(
176        default,
177        skip_serializing_if = "Option::is_none",
178        deserialize_with = "numeric_date"
179    )]
180    pub nbf: Option<i64>,
181    /// `iat`: when the token was issued.
182    #[serde(
183        default,
184        skip_serializing_if = "Option::is_none",
185        deserialize_with = "numeric_date"
186    )]
187    pub iat: Option<i64>,
188    /// `jti`: the identifier of the token.
189    #[serde(default, skip_serializing_if = "Option::is_none")]
190    pub jti: Option<String>,
191    /// Every other claim, in insertion order.
192    #[serde(flatten)]
193    pub additional: IndexMap<String, Value>,
194}
195
196/// Deserializes a `NumericDate` (RFC 7519 §2): a JSON number, possibly with a fraction, floored
197/// to whole seconds.
198pub(crate) fn numeric_date<'de, D: Deserializer<'de>>(
199    deserializer: D,
200) -> Result<Option<i64>, D::Error> {
201    let Some(number) = Option::<serde_json::Number>::deserialize(deserializer)? else {
202        return Ok(None);
203    };
204    number
205        .as_i64()
206        .or_else(|| {
207            number
208                .as_f64()
209                .filter(|seconds| (i64::MIN as f64..i64::MAX as f64).contains(seconds))
210                .map(|seconds| seconds.floor() as i64)
211        })
212        .map(Some)
213        .ok_or_else(|| D::Error::custom(format!("NumericDate {number} is out of range")))
214}
215
216/// As [`numeric_date`], for a claim that must be present.
217#[cfg(feature = "sd-jwt")]
218pub(crate) fn required_numeric_date<'de, D: Deserializer<'de>>(
219    deserializer: D,
220) -> Result<i64, D::Error> {
221    numeric_date(deserializer)?.ok_or_else(|| serde::de::Error::custom("a NumericDate is required"))
222}
223
224impl JwtClaims {
225    /// The registered claim names of RFC 7519 §4.1, which have fields of their own.
226    pub const REGISTERED: [&'static str; 7] = ["iss", "sub", "aud", "exp", "nbf", "iat", "jti"];
227
228    /// An empty claims set.
229    pub fn new() -> Self {
230        Self::default()
231    }
232
233    /// The first registered claim name found under `additional`, if any.
234    fn registered_in_additional(&self) -> Option<&str> {
235        Self::REGISTERED
236            .into_iter()
237            .find(|name| self.additional.contains_key(*name))
238    }
239
240    /// This claims set with `iss` set to `issuer`.
241    #[must_use]
242    pub fn with_iss(mut self, issuer: impl Into<String>) -> Self {
243        self.iss = Some(issuer.into());
244        self
245    }
246
247    /// This claims set with `sub` set to `subject`.
248    #[must_use]
249    pub fn with_sub(mut self, subject: impl Into<String>) -> Self {
250        self.sub = Some(subject.into());
251        self
252    }
253
254    /// This claims set with `jti` set to `id`.
255    #[must_use]
256    pub fn with_jti(mut self, id: impl Into<String>) -> Self {
257        self.jti = Some(id.into());
258        self
259    }
260
261    /// `time` as a `NumericDate`: whole seconds since the Unix epoch, fractions dropped.
262    pub fn numeric_date(time: DateTime<Utc>) -> i64 {
263        time.timestamp()
264    }
265
266    /// The time a `NumericDate` names, if it is in the range `chrono` represents.
267    pub fn timestamp(numeric_date: i64) -> Option<DateTime<Utc>> {
268        DateTime::from_timestamp(numeric_date, 0)
269    }
270
271    /// The audiences, in order; empty when `aud` is absent.
272    pub fn audiences(&self) -> impl Iterator<Item = &str> {
273        self.aud
274            .iter()
275            .flat_map(OneOrMany::iter)
276            .map(String::as_str)
277    }
278
279    /// This claims set with `exp` set to `time`.
280    #[must_use]
281    pub fn with_exp(mut self, time: DateTime<Utc>) -> Self {
282        self.exp = Some(Self::numeric_date(time));
283        self
284    }
285
286    /// This claims set with `nbf` set to `time`.
287    #[must_use]
288    pub fn with_nbf(mut self, time: DateTime<Utc>) -> Self {
289        self.nbf = Some(Self::numeric_date(time));
290        self
291    }
292
293    /// This claims set with `iat` set to `time`.
294    #[must_use]
295    pub fn with_iat(mut self, time: DateTime<Utc>) -> Self {
296        self.iat = Some(Self::numeric_date(time));
297        self
298    }
299
300    /// This claims set with `aud` set to the one audience `audience`.
301    #[must_use]
302    pub fn with_aud(mut self, audience: impl Into<String>) -> Self {
303        self.aud = Some(OneOrMany::One(audience.into()));
304        self
305    }
306
307    /// This claims set with the claim `name` set to `value`.
308    ///
309    /// `name` should not be one of [`REGISTERED`](Self::REGISTERED), which have setters of their
310    /// own: [`Jwt::sign`] refuses a claims set holding a registered claim under `additional`.
311    #[must_use]
312    pub fn with_claim(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
313        self.additional.insert(name.into(), value.into());
314        self
315    }
316}