Skip to main content

identity_kit/vc/
jwt.rs

1//! `Jwt`, a compact JWS carrying a JSON claims set, and the `chrono` conversions of
2//! [`JwtClaims`].
3
4use core::fmt;
5use core::str::FromStr;
6
7use chrono::{DateTime, Utc};
8use serde::de::Error as _;
9use serde::{Deserialize, Deserializer, Serialize};
10
11#[cfg(feature = "status-list")]
12use crate::jose::numeric_date;
13use crate::jose::{CompactJws, JwsError, JwsHeader, JwtClaims, Signer, encode_compact};
14
15/// A JSON Web Token (RFC 7519) in compact serialization, kept as the string it is.
16///
17/// Construction checks that the string is a well-formed compact JWS with a supported `alg`;
18/// nothing is verified. The type serializes as a plain string, which is how a presentation
19/// carries its credentials.
20///
21/// # Examples
22///
23/// ```
24/// use identity_kit::vc::Jwt;
25///
26/// // RFC 8037 Appendix A.4.
27/// let jwt: Jwt = "eyJhbGciOiJFZERTQSJ9.RXhhbXBsZSBvZiBFZDI1NTE5IHNpZ25pbmc.\
28///     hgyY0il_MGCjP0JzlnLWG1PPOt7-09PGcvMg3AIbQR6dWbhijcNR4ki4iylGjg5BhVsPt9g7sVvpAr_MuM0KAg"
29///     .parse()?;
30/// assert_eq!(jwt.decode()?.payload(), b"Example of Ed25519 signing");
31/// assert!("not.a.jwt".parse::<Jwt>().is_err());
32/// # Ok::<(), identity_kit::jose::JwsError>(())
33/// ```
34#[derive(Clone, PartialEq, Eq, Hash, Serialize)]
35#[serde(transparent)]
36pub struct Jwt(String);
37
38impl Jwt {
39    /// Wraps `token` after checking it is a compact JWS this crate can decode.
40    ///
41    /// # Errors
42    ///
43    /// The [`JwsError`] of [`CompactJws::decode`].
44    pub fn new(token: impl Into<String>) -> Result<Self, JwsError> {
45        let token = token.into();
46        CompactJws::decode(&token)?;
47        Ok(Self(token))
48    }
49
50    /// Signs `claims` under `header` with `signer`.
51    ///
52    /// The claims are serialized as compact JSON in their field order, `additional` last.
53    ///
54    /// # Errors
55    ///
56    /// [`JwsError::InvalidPayload`] when `additional` holds a registered claim, which would put
57    /// the member in the payload twice; otherwise as [`encode_compact`].
58    ///
59    /// # Panics
60    ///
61    /// Never: a claims set always serializes to JSON.
62    pub async fn sign(
63        header: &JwsHeader,
64        claims: &JwtClaims,
65        signer: &dyn Signer,
66    ) -> Result<Self, JwsError> {
67        if let Some(name) = registered_in_additional(claims) {
68            return Err(JwsError::InvalidPayload {
69                reason: format!(
70                    "registered claim `{name}` is set under `additional`; set its field instead"
71                ),
72                source: None,
73            });
74        }
75        let payload = serde_json::to_vec(claims).expect("a claims set serializes");
76        encode_compact(header, &payload, signer).await.map(Self)
77    }
78
79    /// The compact serialization.
80    pub fn as_str(&self) -> &str {
81        &self.0
82    }
83
84    /// The compact serialization.
85    pub fn into_string(self) -> String {
86        self.0
87    }
88
89    /// The decoded parts, unverified.
90    ///
91    /// # Errors
92    ///
93    /// Never, in practice: construction decoded the same string.
94    pub fn decode(&self) -> Result<CompactJws, JwsError> {
95        CompactJws::decode(&self.0)
96    }
97}
98
99impl fmt::Debug for Jwt {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        f.debug_tuple("Jwt").field(&self.0).finish()
102    }
103}
104
105impl fmt::Display for Jwt {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        f.write_str(&self.0)
108    }
109}
110
111impl AsRef<str> for Jwt {
112    fn as_ref(&self) -> &str {
113        &self.0
114    }
115}
116
117impl FromStr for Jwt {
118    type Err = JwsError;
119
120    fn from_str(s: &str) -> Result<Self, Self::Err> {
121        Self::new(s)
122    }
123}
124
125impl TryFrom<String> for Jwt {
126    type Error = JwsError;
127
128    fn try_from(token: String) -> Result<Self, Self::Error> {
129        Self::new(token)
130    }
131}
132
133impl<'de> Deserialize<'de> for Jwt {
134    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
135        Self::new(String::deserialize(deserializer)?).map_err(D::Error::custom)
136    }
137}
138
139/// The first registered claim name found under `additional` of `claims`, if any.
140fn registered_in_additional(claims: &JwtClaims) -> Option<&'static str> {
141    JwtClaims::REGISTERED
142        .into_iter()
143        .find(|name| claims.additional.contains_key(*name))
144}
145
146/// As [`numeric_date`], for a claim that must be present.
147#[cfg(feature = "status-list")]
148pub(crate) fn required_numeric_date<'de, D: Deserializer<'de>>(
149    deserializer: D,
150) -> Result<i64, D::Error> {
151    numeric_date(deserializer)?.ok_or_else(|| serde::de::Error::custom("a NumericDate is required"))
152}
153
154/// The conversions between `NumericDate` values and `chrono` times, which come with the `vc`
155/// feature.
156#[cfg_attr(docsrs, doc(cfg(feature = "vc")))]
157impl JwtClaims {
158    /// `time` as a `NumericDate`: whole seconds since the Unix epoch, fractions dropped.
159    pub fn numeric_date(time: DateTime<Utc>) -> i64 {
160        time.timestamp()
161    }
162
163    /// The time a `NumericDate` names, if it is in the range `chrono` represents.
164    pub fn timestamp(numeric_date: i64) -> Option<DateTime<Utc>> {
165        DateTime::from_timestamp(numeric_date, 0)
166    }
167
168    /// This claims set with `exp` set to `time`.
169    #[must_use]
170    pub fn with_exp(mut self, time: DateTime<Utc>) -> Self {
171        self.exp = Some(Self::numeric_date(time));
172        self
173    }
174
175    /// This claims set with `nbf` set to `time`.
176    #[must_use]
177    pub fn with_nbf(mut self, time: DateTime<Utc>) -> Self {
178        self.nbf = Some(Self::numeric_date(time));
179        self
180    }
181
182    /// This claims set with `iat` set to `time`.
183    #[must_use]
184    pub fn with_iat(mut self, time: DateTime<Utc>) -> Self {
185        self.iat = Some(Self::numeric_date(time));
186        self
187    }
188}