Skip to main content

jwt_compact_preview/
claims.rs

1use chrono::{DateTime, Duration, Utc};
2use serde::{Deserialize, Serialize};
3
4use crate::ValidationError;
5
6/// Time-related validation options.
7#[derive(Debug, Clone, Copy)]
8pub struct TimeOptions {
9    /// Leeway to use during validation.
10    pub leeway: Duration,
11    /// Current time to check against. If not set, the current time will be set to `Utc::now()`.
12    pub current_time: Option<DateTime<Utc>>,
13}
14
15impl Default for TimeOptions {
16    fn default() -> Self {
17        Self {
18            leeway: Duration::seconds(60),
19            current_time: None,
20        }
21    }
22}
23
24/// A structure with no fields that can be used as a type parameter to `Claims`.
25#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
26pub struct Empty {}
27
28/// Claims encoded in a token.
29///
30/// Claims are comprised of a "standard" part (`exp`, `nbf` and `iat` claims as per [JWT spec]),
31/// and custom fields. `iss`, `sub` and `aud` claims are not in the standard part
32/// due to a variety of data types they can be reasonably represented by.
33///
34/// [JWT spec]: https://tools.ietf.org/html/rfc7519#section-4.1
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36pub struct Claims<T> {
37    /// Expiration date of the token.
38    #[serde(
39        rename = "exp",
40        default,
41        skip_serializing_if = "Option::is_none",
42        with = "self::serde_timestamp"
43    )]
44    pub expiration_date: Option<DateTime<Utc>>,
45
46    /// Minimum date at which token is valid.
47    #[serde(
48        rename = "nbf",
49        default,
50        skip_serializing_if = "Option::is_none",
51        with = "self::serde_timestamp"
52    )]
53    pub not_before: Option<DateTime<Utc>>,
54
55    /// Date of token issuance.
56    #[serde(
57        rename = "iat",
58        default,
59        skip_serializing_if = "Option::is_none",
60        with = "self::serde_timestamp"
61    )]
62    pub issued_at: Option<DateTime<Utc>>,
63
64    /// Custom claims.
65    #[serde(flatten)]
66    pub custom: T,
67}
68
69impl Claims<Empty> {
70    /// Creates an empty claims instance.
71    pub fn empty() -> Self {
72        Self {
73            expiration_date: None,
74            not_before: None,
75            issued_at: None,
76            custom: Empty {},
77        }
78    }
79}
80
81impl<T> Claims<T> {
82    /// Creates a new instance with the provided custom claims.
83    pub fn new(custom_claims: T) -> Self {
84        Self {
85            expiration_date: None,
86            not_before: None,
87            issued_at: None,
88            custom: custom_claims,
89        }
90    }
91
92    /// Sets `expiration_date` claim so that the token has the specified `duration`.
93    pub fn set_duration(self, duration: Duration) -> Self {
94        Self {
95            expiration_date: Some(Utc::now() + duration),
96            ..self
97        }
98    }
99
100    /// Atomically sets `issued_at` and `expiration_date` claims: first to the current time,
101    /// and the second to match the specified `duration` of the token.
102    pub fn set_duration_and_issuance(self, duration: Duration) -> Self {
103        let issued_at = Utc::now();
104        Self {
105            expiration_date: Some(issued_at + duration),
106            issued_at: Some(issued_at),
107            ..self
108        }
109    }
110
111    /// Sets the `nbf` claim.
112    pub fn set_not_before(self, moment: DateTime<Utc>) -> Self {
113        Self {
114            not_before: Some(moment),
115            ..self
116        }
117    }
118
119    /// Validates the expiration claim.
120    ///
121    /// This method will return an error if the claims do not feature an expiration date,
122    /// or if it is in the past (subject to the provided `options`).
123    pub fn validate_expiration(&self, options: TimeOptions) -> Result<&Self, ValidationError> {
124        if let Some(expiration) = self.expiration_date {
125            let current_time = options.current_time.unwrap_or_else(Utc::now);
126            if current_time > expiration + options.leeway {
127                Err(ValidationError::Expired)
128            } else {
129                Ok(self)
130            }
131        } else {
132            Err(ValidationError::NoClaim)
133        }
134    }
135
136    /// Validates the maturity date (`nbf` claim).
137    ///
138    /// This method will return an error if the claims do not feature a maturity date,
139    /// or if it is in the future (subject to the provided `options`).
140    pub fn validate_maturity(&self, options: TimeOptions) -> Result<&Self, ValidationError> {
141        if let Some(not_before) = self.not_before {
142            let current_time = options.current_time.unwrap_or_else(Utc::now);
143            if current_time < not_before - options.leeway {
144                Err(ValidationError::NotMature)
145            } else {
146                Ok(self)
147            }
148        } else {
149            Err(ValidationError::NoClaim)
150        }
151    }
152}
153
154mod serde_timestamp {
155    use chrono::{offset::TimeZone, DateTime, Utc};
156    use serde::{
157        de::{Error as DeError, Visitor},
158        Deserializer, Serializer,
159    };
160
161    use std::{convert::TryFrom, fmt};
162
163    struct TimestampVisitor;
164
165    impl<'de> Visitor<'de> for TimestampVisitor {
166        type Value = DateTime<Utc>;
167
168        fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
169            formatter.write_str("UTC timestamp")
170        }
171
172        fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
173        where
174            E: DeError,
175        {
176            Ok(Utc.timestamp(value, 0))
177        }
178
179        fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
180        where
181            E: DeError,
182        {
183            let value = i64::try_from(value).map_err(DeError::custom)?;
184            Ok(Utc.timestamp(value, 0))
185        }
186    }
187
188    pub fn serialize<S: Serializer>(
189        time: &Option<DateTime<Utc>>,
190        serializer: S,
191    ) -> Result<S::Ok, S::Error> {
192        // `unwrap` is safe due to `skip_serializing_if` option
193        serializer.serialize_i64(time.unwrap().timestamp())
194    }
195
196    pub fn deserialize<'de, D: Deserializer<'de>>(
197        deserializer: D,
198    ) -> Result<Option<DateTime<Utc>>, D::Error> {
199        deserializer.deserialize_i64(TimestampVisitor).map(Some)
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use assert_matches::assert_matches;
207
208    #[test]
209    fn empty_claims_can_be_serialized() {
210        let mut claims = Claims::empty();
211        assert!(serde_json::to_string(&claims).is_ok());
212        assert!(serde_cbor::to_vec(&claims).is_ok());
213        claims.expiration_date = Some(Utc::now());
214        assert!(serde_json::to_string(&claims).is_ok());
215        assert!(serde_cbor::to_vec(&claims).is_ok());
216        claims.not_before = Some(Utc::now());
217        assert!(serde_json::to_string(&claims).is_ok());
218        assert!(serde_cbor::to_vec(&claims).is_ok());
219    }
220
221    #[test]
222    fn expired_claim() {
223        let mut claims = Claims::empty();
224        assert_matches!(
225            claims
226                .validate_expiration(TimeOptions::default())
227                .unwrap_err(),
228            ValidationError::NoClaim
229        );
230
231        claims.expiration_date = Some(Utc::now() - Duration::hours(1));
232        assert_matches!(
233            claims
234                .validate_expiration(TimeOptions::default())
235                .unwrap_err(),
236            ValidationError::Expired
237        );
238
239        claims.expiration_date = Some(Utc::now() - Duration::seconds(10));
240        // With the default leeway, this claim is still valid.
241        assert!(claims.validate_expiration(TimeOptions::default()).is_ok());
242        // If we set leeway lower, then the claim will be considered expired.
243        assert_matches!(
244            claims
245                .validate_expiration(TimeOptions {
246                    leeway: Duration::seconds(5),
247                    ..Default::default()
248                })
249                .unwrap_err(),
250            ValidationError::Expired
251        );
252    }
253
254    #[test]
255    fn immature_claim() {
256        let mut claims = Claims::empty();
257        assert_matches!(
258            claims
259                .validate_maturity(TimeOptions::default())
260                .unwrap_err(),
261            ValidationError::NoClaim
262        );
263
264        claims.not_before = Some(Utc::now() + Duration::hours(1));
265        assert_matches!(
266            claims
267                .validate_maturity(TimeOptions::default())
268                .unwrap_err(),
269            ValidationError::NotMature
270        );
271
272        claims.not_before = Some(Utc::now() + Duration::seconds(10));
273        // With the default leeway, this claim is still valid.
274        assert!(claims.validate_maturity(TimeOptions::default()).is_ok());
275        // If we set leeway lower, then the claim will be considered expired.
276        assert_matches!(
277            claims
278                .validate_maturity(TimeOptions {
279                    leeway: Duration::seconds(5),
280                    ..Default::default()
281                })
282                .unwrap_err(),
283            ValidationError::NotMature
284        );
285    }
286}