Skip to main content

jwt_simple_legacy/
claims.rs

1use std::collections::HashSet;
2use std::convert::TryInto;
3
4use coarsetime::{Clock, Duration, UnixTimeStamp};
5use ct_codecs::{Base64UrlSafeNoPadding, Encoder};
6use rand::RngCore;
7use serde::{de::DeserializeOwned, Deserialize, Serialize};
8
9use crate::common::VerificationOptions;
10use crate::error::*;
11use crate::serde_additions;
12
13pub const DEFAULT_TIME_TOLERANCE_SECS: u64 = 900;
14
15/// Type representing the fact that no application-defined claims is necessary.
16#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize)]
17pub struct NoCustomClaims {}
18
19/// Depending on applications, the `audiences` property may be either a set or a
20/// string. We support both.
21#[derive(Debug, Clone, Eq, PartialEq)]
22pub enum Audiences {
23    AsSet(HashSet<String>),
24    AsString(String),
25}
26
27impl Audiences {
28    /// Return `true` if the audiences are represented as a set.
29    pub fn is_set(&self) -> bool {
30        matches!(self, Audiences::AsSet(_))
31    }
32
33    /// Return `true` if the audiences are represented as a string.
34    pub fn is_string(&self) -> bool {
35        matches!(self, Audiences::AsString(_))
36    }
37
38    /// Return `true` if the audiences include any of the `allowed_audiences`
39    /// entries
40    pub fn contains(&self, allowed_audiences: &HashSet<String>) -> bool {
41        match self {
42            Audiences::AsString(audience) => allowed_audiences.contains(audience),
43            Audiences::AsSet(audiences) => {
44                audiences.intersection(allowed_audiences).next().is_some()
45            }
46        }
47    }
48
49    /// Get the audiences as a set
50    pub fn into_set(self) -> HashSet<String> {
51        match self {
52            Audiences::AsSet(audiences_set) => audiences_set,
53            Audiences::AsString(audiences) => {
54                let mut audiences_set = HashSet::new();
55                if !audiences.is_empty() {
56                    audiences_set.insert(audiences);
57                }
58                audiences_set
59            }
60        }
61    }
62
63    /// Get the audiences as a string.
64    /// If it was originally serialized as a set, it can be only converted to a
65    /// string if it contains at most one element.
66    pub fn into_string(self) -> Result<String, Error> {
67        match self {
68            Audiences::AsString(audiences_str) => Ok(audiences_str),
69            Audiences::AsSet(audiences) => {
70                if audiences.len() > 1 {
71                    bail!(JWTError::TooManyAudiences);
72                }
73                Ok(audiences
74                    .iter()
75                    .next()
76                    .map(|x| x.to_string())
77                    .unwrap_or_default())
78            }
79        }
80    }
81}
82
83impl TryInto<String> for Audiences {
84    type Error = Error;
85
86    fn try_into(self) -> Result<String, Error> {
87        self.into_string()
88    }
89}
90
91impl From<Audiences> for HashSet<String> {
92    fn from(audiences: Audiences) -> HashSet<String> {
93        audiences.into_set()
94    }
95}
96
97impl<T: ToString> From<T> for Audiences {
98    fn from(audience: T) -> Self {
99        Audiences::AsString(audience.to_string())
100    }
101}
102
103/// A set of JWT claims.
104///
105/// The `CustomClaims` parameter can be set to `NoCustomClaims` if only standard
106/// claims are used, or to a user-defined type that must be `serde`-serializable
107/// if custom claims are required.
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct JWTClaims<CustomClaims> {
110    /// Time the claims were created at
111    #[serde(
112        rename = "iat",
113        default,
114        skip_serializing_if = "Option::is_none",
115        with = "self::serde_additions::unix_timestamp"
116    )]
117    pub issued_at: Option<UnixTimeStamp>,
118
119    /// Time the claims expire at
120    #[serde(
121        rename = "exp",
122        default,
123        skip_serializing_if = "Option::is_none",
124        with = "self::serde_additions::unix_timestamp"
125    )]
126    pub expires_at: Option<UnixTimeStamp>,
127
128    /// Time the claims will be invalid until
129    #[serde(
130        rename = "nbf",
131        default,
132        skip_serializing_if = "Option::is_none",
133        with = "self::serde_additions::unix_timestamp"
134    )]
135    pub invalid_before: Option<UnixTimeStamp>,
136
137    /// Issuer - This can be set to anything application-specific
138    #[serde(rename = "iss", default, skip_serializing_if = "Option::is_none")]
139    pub issuer: Option<String>,
140
141    /// Subject - This can be set to anything application-specific
142    #[serde(rename = "sub", default, skip_serializing_if = "Option::is_none")]
143    pub subject: Option<String>,
144
145    /// Audience
146    #[serde(
147        rename = "aud",
148        default,
149        skip_serializing_if = "Option::is_none",
150        with = "self::serde_additions::audiences"
151    )]
152    pub audiences: Option<Audiences>,
153
154    /// JWT identifier
155    ///
156    /// That property was originally designed to avoid replay attacks, but
157    /// keeping all previously sent JWT token IDs is unrealistic.
158    ///
159    /// Replay attacks are better addressed by keeping only the timestamp of the
160    /// last valid token for a user, and rejecting anything older in future
161    /// tokens.
162    #[serde(rename = "jti", default, skip_serializing_if = "Option::is_none")]
163    pub jwt_id: Option<String>,
164
165    /// Nonce
166    #[serde(rename = "nonce", default, skip_serializing_if = "Option::is_none")]
167    pub nonce: Option<String>,
168
169    /// Custom (application-defined) claims
170    #[serde(flatten)]
171    pub custom: CustomClaims,
172}
173
174impl<CustomClaims> JWTClaims<CustomClaims> {
175    pub(crate) fn validate(&self, options: &VerificationOptions) -> Result<(), Error> {
176        let now = Clock::now_since_epoch();
177        let time_tolerance = options.time_tolerance.unwrap_or_default();
178
179        if let Some(reject_before) = options.reject_before {
180            ensure!(now <= reject_before, JWTError::OldTokenReused);
181        }
182        if let Some(time_issued) = self.issued_at {
183            ensure!(time_issued <= now + time_tolerance, JWTError::ClockDrift);
184            if let Some(max_validity) = options.max_validity {
185                ensure!(
186                    now <= time_issued || now - time_issued <= max_validity,
187                    JWTError::TokenIsTooOld
188                );
189            }
190        }
191        if !options.accept_future {
192            if let Some(invalid_before) = self.invalid_before {
193                ensure!(
194                    now + time_tolerance >= invalid_before,
195                    JWTError::TokenNotValidYet
196                );
197            }
198        }
199        if let Some(expires_at) = self.expires_at {
200            ensure!(
201                now - time_tolerance <= expires_at,
202                JWTError::TokenHasExpired
203            );
204        }
205        if let Some(allowed_issuers) = &options.allowed_issuers {
206            if let Some(issuer) = &self.issuer {
207                ensure!(
208                    allowed_issuers.contains(issuer),
209                    JWTError::RequiredIssuerMismatch
210                );
211            } else {
212                bail!(JWTError::RequiredIssuerMissing);
213            }
214        }
215        if let Some(required_subject) = &options.required_subject {
216            if let Some(subject) = &self.subject {
217                ensure!(
218                    subject == required_subject,
219                    JWTError::RequiredSubjectMismatch
220                );
221            } else {
222                bail!(JWTError::RequiredSubjectMissing);
223            }
224        }
225        if let Some(required_nonce) = &options.required_nonce {
226            if let Some(nonce) = &self.nonce {
227                ensure!(nonce == required_nonce, JWTError::RequiredNonceMismatch);
228            } else {
229                bail!(JWTError::RequiredNonceMissing);
230            }
231        }
232        if let Some(allowed_audiences) = &options.allowed_audiences {
233            if let Some(audiences) = &self.audiences {
234                ensure!(
235                    audiences.contains(allowed_audiences),
236                    JWTError::RequiredAudienceMismatch
237                );
238            } else {
239                bail!(JWTError::RequiredAudienceMissing);
240            }
241        }
242        Ok(())
243    }
244
245    /// Set the token as not being valid until `unix_timestamp`
246    pub fn invalid_before(mut self, unix_timestamp: UnixTimeStamp) -> Self {
247        self.invalid_before = Some(unix_timestamp);
248        self
249    }
250
251    /// Set the issuer
252    pub fn with_issuer(mut self, issuer: impl ToString) -> Self {
253        self.issuer = Some(issuer.to_string());
254        self
255    }
256
257    /// Set the subject
258    pub fn with_subject(mut self, subject: impl ToString) -> Self {
259        self.subject = Some(subject.to_string());
260        self
261    }
262
263    /// Register one or more audiences (optional recipient identifiers), as a
264    /// set
265    pub fn with_audiences(mut self, audiences: HashSet<impl ToString>) -> Self {
266        self.audiences = Some(Audiences::AsSet(
267            audiences.iter().map(|x| x.to_string()).collect(),
268        ));
269        self
270    }
271
272    /// Set a unique audience (an optional recipient identifier), as a string
273    pub fn with_audience(mut self, audience: impl ToString) -> Self {
274        self.audiences = Some(Audiences::AsString(audience.to_string()));
275        self
276    }
277
278    /// Set the JWT identifier
279    pub fn with_jwt_id(mut self, jwt_id: impl ToString) -> Self {
280        self.jwt_id = Some(jwt_id.to_string());
281        self
282    }
283
284    /// Set the nonce
285    pub fn with_nonce(mut self, nonce: impl ToString) -> Self {
286        self.nonce = Some(nonce.to_string());
287        self
288    }
289
290    /// Create a nonce, attach it and return it
291    pub fn create_nonce(&mut self) -> String {
292        let mut raw_nonce = [0u8; 24];
293        let mut rng = rand::thread_rng();
294        rng.fill_bytes(&mut raw_nonce);
295        let nonce = Base64UrlSafeNoPadding::encode_to_string(raw_nonce).unwrap();
296        self.nonce = Some(nonce);
297        self.nonce.as_deref().unwrap().to_string()
298    }
299}
300
301pub struct Claims;
302
303impl Claims {
304    /// Create a new set of claims, without custom data, expiring in
305    /// `valid_for`.
306    pub fn create(valid_for: Duration) -> JWTClaims<NoCustomClaims> {
307        let now = Some(Clock::now_since_epoch());
308        JWTClaims {
309            issued_at: now,
310            expires_at: Some(now.unwrap() + valid_for),
311            invalid_before: now,
312            audiences: None,
313            issuer: None,
314            jwt_id: None,
315            subject: None,
316            nonce: None,
317            custom: NoCustomClaims {},
318        }
319    }
320
321    /// Create a new set of claims, with custom data, expiring in `valid_for`.
322    pub fn with_custom_claims<CustomClaims: Serialize + DeserializeOwned>(
323        custom_claims: CustomClaims,
324        valid_for: Duration,
325    ) -> JWTClaims<CustomClaims> {
326        let now = Some(Clock::now_since_epoch());
327        JWTClaims {
328            issued_at: now,
329            expires_at: Some(now.unwrap() + valid_for),
330            invalid_before: now,
331            audiences: None,
332            issuer: None,
333            jwt_id: None,
334            subject: None,
335            nonce: None,
336            custom: custom_claims,
337        }
338    }
339}
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344
345    #[test]
346    fn should_set_standard_claims() {
347        let exp = Duration::from_mins(10);
348        let mut audiences = HashSet::new();
349        audiences.insert("audience1".to_string());
350        audiences.insert("audience2".to_string());
351        let claims = Claims::create(exp)
352            .with_audiences(audiences.clone())
353            .with_issuer("issuer")
354            .with_jwt_id("jwt_id")
355            .with_nonce("nonce")
356            .with_subject("subject");
357
358        assert_eq!(claims.audiences, Some(Audiences::AsSet(audiences)));
359        assert_eq!(claims.issuer, Some("issuer".to_owned()));
360        assert_eq!(claims.jwt_id, Some("jwt_id".to_owned()));
361        assert_eq!(claims.nonce, Some("nonce".to_owned()));
362        assert_eq!(claims.subject, Some("subject".to_owned()));
363    }
364
365    #[test]
366    fn parse_floating_point_unix_time() {
367        let claims: JWTClaims<()> = serde_json::from_str(r#"{"exp":1617757825.8}"#).unwrap();
368        assert_eq!(
369            claims.expires_at,
370            Some(UnixTimeStamp::from_secs(1617757825))
371        );
372    }
373}