jwt_simple/claims.rs
1use std::collections::HashSet;
2use std::convert::TryInto;
3
4use coarsetime::{Clock, Duration, UnixTimeStamp};
5use ct_codecs::{Base64UrlSafeNoPadding, Encoder, Hex};
6use rand::RngCore;
7use serde::{de::DeserializeOwned, Deserialize, Serialize};
8
9use crate::common::VerificationOptions;
10use crate::error::*;
11use crate::serde_additions;
12
13/// Default time tolerance value in seconds (15 minutes) for token verification.
14///
15/// This value is used to account for clock skew between systems. When verifying token
16/// expiration and validity period, this tolerance is applied to allow for slight
17/// differences in system clocks.
18///
19/// The default value is set to 15 minutes (900 seconds), which is a common practice
20/// for JWT implementations. This means:
21///
22/// - Tokens that expired less than 15 minutes ago are still considered valid
23/// - Tokens that will become valid within the next 15 minutes are already considered valid
24///
25/// You can override this value by setting the `time_tolerance` field in the `VerificationOptions`.
26pub const DEFAULT_TIME_TOLERANCE_SECS: u64 = 900;
27
28/// Empty struct representing that no application-defined claims are necessary.
29///
30/// Use this as the generic type parameter for `JWTClaims<T>` when you only need
31/// standard JWT claims and no custom application-specific data.
32///
33/// # Example
34///
35/// ```
36/// use jwt_simple::prelude::*;
37///
38/// // Create claims with no custom data
39/// let claims = Claims::create(Duration::from_hours(2));
40///
41/// // When verifying, specify NoCustomClaims as the type parameter
42/// # let key = HS256Key::generate();
43/// # let token_str = key.authenticate(claims).unwrap();
44/// let verified_claims = key.verify_token::<NoCustomClaims>(&token_str, None).unwrap();
45/// ```
46#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize)]
47pub struct NoCustomClaims {}
48
49/// Representation of the JWT audience claim, which can be either a single string or a set of strings.
50///
51/// The JWT specification allows the `aud` (audience) claim to be represented either as a single
52/// string value or an array of strings. This enum provides a unified way to handle both formats.
53///
54/// # Variants
55///
56/// * `AsSet(HashSet<String>)` - Represents multiple audience values as a set of strings
57/// * `AsString(String)` - Represents a single audience value as a string
58///
59/// # Example
60///
61/// ```
62/// use jwt_simple::prelude::*;
63/// use std::collections::HashSet;
64///
65/// // Using a single audience
66/// let claims = Claims::create(Duration::from_hours(2))
67/// .with_audience("https://api.example.com");
68///
69/// // Using multiple audiences
70/// let mut audiences = HashSet::new();
71/// audiences.insert("https://api.example.com".to_string());
72/// audiences.insert("https://admin.example.com".to_string());
73/// let claims = Claims::create(Duration::from_hours(2))
74/// .with_audiences(audiences);
75/// ```
76#[derive(Debug, Clone, Eq, PartialEq)]
77pub enum Audiences {
78 /// Multiple audience values stored as a set of strings
79 AsSet(HashSet<String>),
80 /// Single audience value stored as a string
81 AsString(String),
82}
83
84impl Audiences {
85 /// Returns `true` if the audiences are represented as a set.
86 ///
87 /// This method checks whether the audience value is stored as a set of strings
88 /// rather than a single string value.
89 ///
90 /// # Example
91 ///
92 /// ```
93 /// use jwt_simple::prelude::*;
94 /// use std::collections::HashSet;
95 ///
96 /// let mut audiences = HashSet::new();
97 /// audiences.insert("audience1".to_string());
98 /// audiences.insert("audience2".to_string());
99 ///
100 /// let audience_set = Audiences::AsSet(audiences);
101 /// assert!(audience_set.is_set());
102 /// assert!(!audience_set.is_string());
103 /// ```
104 pub fn is_set(&self) -> bool {
105 matches!(self, Audiences::AsSet(_))
106 }
107
108 /// Returns `true` if the audiences are represented as a single string.
109 ///
110 /// This method checks whether the audience value is stored as a single string
111 /// rather than a set of strings.
112 ///
113 /// # Example
114 ///
115 /// ```
116 /// use jwt_simple::prelude::*;
117 ///
118 /// let audience_string = Audiences::AsString("audience1".to_string());
119 /// assert!(audience_string.is_string());
120 /// assert!(!audience_string.is_set());
121 /// ```
122 pub fn is_string(&self) -> bool {
123 matches!(self, Audiences::AsString(_))
124 }
125
126 /// Returns `true` if the audiences include any of the `allowed_audiences` entries.
127 ///
128 /// This method is used for audience verification during token validation to check
129 /// if any of the allowed audiences matches the token's audience claim.
130 ///
131 /// - For a string audience, it checks if the string exists in the `allowed_audiences` set
132 /// - For a set of audiences, it checks if there's any overlap with the `allowed_audiences` set
133 ///
134 /// # Arguments
135 ///
136 /// * `allowed_audiences` - A set of allowed audience values to check against
137 ///
138 /// # Example
139 ///
140 /// ```
141 /// use jwt_simple::prelude::*;
142 /// use std::collections::HashSet;
143 ///
144 /// let mut allowed = HashSet::new();
145 /// allowed.insert("audience1".to_string());
146 /// allowed.insert("audience2".to_string());
147 ///
148 /// // String audience
149 /// let audience_string = Audiences::AsString("audience1".to_string());
150 /// assert!(audience_string.contains(&allowed));
151 ///
152 /// // Set of audiences with overlap
153 /// let mut audiences = HashSet::new();
154 /// audiences.insert("audience2".to_string());
155 /// audiences.insert("audience3".to_string());
156 /// let audience_set = Audiences::AsSet(audiences);
157 /// assert!(audience_set.contains(&allowed));
158 /// ```
159 pub fn contains(&self, allowed_audiences: &HashSet<String>) -> bool {
160 match self {
161 Audiences::AsString(audience) => allowed_audiences.contains(audience),
162 Audiences::AsSet(audiences) => {
163 audiences.intersection(allowed_audiences).next().is_some()
164 }
165 }
166 }
167
168 /// Converts the audiences to a set of strings.
169 ///
170 /// This method consumes the `Audiences` enum and returns a `HashSet<String>`:
171 /// - If it's already a set, returns the set directly
172 /// - If it's a string, wraps it in a singleton set (unless it's empty)
173 ///
174 /// # Example
175 ///
176 /// ```
177 /// use jwt_simple::prelude::*;
178 /// use std::collections::HashSet;
179 ///
180 /// // From a string
181 /// let audience_string = Audiences::AsString("audience1".to_string());
182 /// let set = audience_string.into_set();
183 /// assert_eq!(set.len(), 1);
184 /// assert!(set.contains("audience1"));
185 ///
186 /// // From a set
187 /// let mut original_set = HashSet::new();
188 /// original_set.insert("audience1".to_string());
189 /// original_set.insert("audience2".to_string());
190 /// let audience_set = Audiences::AsSet(original_set.clone());
191 /// let result_set = audience_set.into_set();
192 /// assert_eq!(result_set, original_set);
193 /// ```
194 pub fn into_set(self) -> HashSet<String> {
195 match self {
196 Audiences::AsSet(audiences_set) => audiences_set,
197 Audiences::AsString(audiences) => {
198 let mut audiences_set = HashSet::new();
199 if !audiences.is_empty() {
200 audiences_set.insert(audiences);
201 }
202 audiences_set
203 }
204 }
205 }
206
207 /// Converts the audiences to a single string value.
208 ///
209 /// This method consumes the `Audiences` enum and attempts to return a single `String`:
210 /// - If it's already a string, returns the string directly
211 /// - If it's a set with 0 or 1 elements, returns the single element or an empty string
212 /// - If it's a set with more than 1 element, returns an error
213 ///
214 /// # Errors
215 ///
216 /// Returns `JWTError::TooManyAudiences` if the audiences are stored as a set
217 /// with more than one element, since it cannot be unambiguously converted to a single string.
218 ///
219 /// # Example
220 ///
221 /// ```
222 /// use jwt_simple::prelude::*;
223 /// use std::collections::HashSet;
224 ///
225 /// // From a string - succeeds
226 /// let audience_string = Audiences::AsString("audience1".to_string());
227 /// let result = audience_string.into_string().unwrap();
228 /// assert_eq!(result, "audience1");
229 ///
230 /// // From an empty set - succeeds with empty string
231 /// let audience_set = Audiences::AsSet(HashSet::new());
232 /// let result = audience_set.into_string().unwrap();
233 /// assert_eq!(result, "");
234 ///
235 /// // From a set with one element - succeeds
236 /// let mut single_set = HashSet::new();
237 /// single_set.insert("audience1".to_string());
238 /// let audience_set = Audiences::AsSet(single_set);
239 /// let result = audience_set.into_string().unwrap();
240 /// assert_eq!(result, "audience1");
241 ///
242 /// // From a set with multiple elements - fails
243 /// let mut multi_set = HashSet::new();
244 /// multi_set.insert("audience1".to_string());
245 /// multi_set.insert("audience2".to_string());
246 /// let audience_set = Audiences::AsSet(multi_set);
247 /// assert!(audience_set.into_string().is_err());
248 /// ```
249 pub fn into_string(self) -> Result<String, Error> {
250 match self {
251 Audiences::AsString(audiences_str) => Ok(audiences_str),
252 Audiences::AsSet(audiences) => {
253 if audiences.len() > 1 {
254 bail!(JWTError::TooManyAudiences);
255 }
256 Ok(audiences
257 .iter()
258 .next()
259 .map(|x| x.to_string())
260 .unwrap_or_default())
261 }
262 }
263 }
264}
265
266/// Implementation of `TryInto<String>` for `Audiences`, allowing conversion to a string
267/// using the standard `try_into()` method.
268///
269/// This delegates to the `into_string()` method, which will return an error if there
270/// are multiple audience values that cannot be unambiguously converted to a single string.
271impl TryInto<String> for Audiences {
272 type Error = Error;
273
274 fn try_into(self) -> Result<String, Error> {
275 self.into_string()
276 }
277}
278
279/// Implementation of `From<Audiences>` for `HashSet<String>`, allowing conversion to a set
280/// using the standard `into()` method.
281///
282/// This delegates to the `into_set()` method, which always succeeds.
283impl From<Audiences> for HashSet<String> {
284 fn from(audiences: Audiences) -> HashSet<String> {
285 audiences.into_set()
286 }
287}
288
289/// Convenient conversion from any string-like type to `Audiences`.
290///
291/// This converts any type that implements `ToString` into an `Audiences::AsString` variant,
292/// making it simpler to create single-audience tokens.
293///
294/// # Example
295///
296/// ```
297/// use jwt_simple::prelude::*;
298///
299/// // String conversion
300/// let audiences: Audiences = "https://api.example.com".into();
301/// assert!(audiences.is_string());
302///
303/// // &str conversion
304/// let audiences: Audiences = "https://api.example.com".into();
305/// assert!(audiences.is_string());
306/// ```
307impl<T: ToString> From<T> for Audiences {
308 fn from(audience: T) -> Self {
309 Audiences::AsString(audience.to_string())
310 }
311}
312
313/// A set of JWT claims that can include both standard JWT claims and custom application-specific data.
314///
315/// This struct represents the payload of a JWT token, containing standard registered claims
316/// defined in the JWT specification (RFC 7519) as well as optional custom claims.
317///
318/// The `CustomClaims` generic parameter allows for including application-specific data:
319/// - Use `NoCustomClaims` if you only need the standard JWT claims
320/// - Use your own type that implements `Serialize` and `Deserialize` for custom claims
321///
322/// # Standard Claims
323///
324/// - `iss` (Issuer): Identifies the principal that issued the JWT
325/// - `sub` (Subject): Identifies the principal that is the subject of the JWT
326/// - `aud` (Audience): Identifies the recipients the JWT is intended for
327/// - `exp` (Expiration Time): Identifies the time after which the JWT expires
328/// - `nbf` (Not Before): Identifies the time before which the JWT must not be accepted
329/// - `iat` (Issued At): Identifies the time at which the JWT was issued
330/// - `jti` (JWT ID): Provides a unique identifier for the JWT
331///
332/// Plus additional non-standard but commonly used claims:
333/// - `kid` (Key ID): Identifier for the key used to sign the token
334/// - `nonce`: Random value that can be used to prevent replay attacks
335///
336/// # Example
337///
338/// ```
339/// use jwt_simple::prelude::*;
340/// use serde::{Serialize, Deserialize};
341///
342/// // Using only standard claims
343/// let std_claims = Claims::create(Duration::from_hours(1))
344/// .with_issuer("auth.example.com")
345/// .with_subject("user123");
346///
347/// // Using custom claims
348/// #[derive(Serialize, Deserialize)]
349/// struct UserClaims {
350/// user_id: u64,
351/// is_admin: bool,
352/// }
353///
354/// let custom_claims = Claims::with_custom_claims(
355/// UserClaims { user_id: 42, is_admin: false },
356/// Duration::from_hours(1)
357/// ).with_issuer("auth.example.com");
358/// ```
359#[derive(Clone, Serialize, Deserialize)]
360pub struct JWTClaims<CustomClaims> {
361 /// The "Issued At" (`iat`) claim - identifies the time at which the JWT was issued.
362 ///
363 /// This claim can be used to determine the age of the token. It is represented as
364 /// the number of seconds from 1970-01-01T00:00:00Z UTC (the UNIX epoch).
365 ///
366 /// This field is automatically set when using `Claims::create()` or
367 /// `Claims::with_custom_claims()` to the current time.
368 #[serde(
369 rename = "iat",
370 default,
371 skip_serializing_if = "Option::is_none",
372 with = "self::serde_additions::unix_timestamp"
373 )]
374 pub issued_at: Option<UnixTimeStamp>,
375
376 /// The "Expiration Time" (`exp`) claim - identifies the expiration time of the token.
377 ///
378 /// This claim specifies the time after which the JWT must not be accepted for processing.
379 /// It is represented as the number of seconds from 1970-01-01T00:00:00Z UTC (the UNIX epoch).
380 ///
381 /// This field is automatically set when using `Claims::create()` or
382 /// `Claims::with_custom_claims()` to the current time plus the duration passed
383 /// as the `valid_for` parameter.
384 #[serde(
385 rename = "exp",
386 default,
387 skip_serializing_if = "Option::is_none",
388 with = "self::serde_additions::unix_timestamp"
389 )]
390 pub expires_at: Option<UnixTimeStamp>,
391
392 /// The "Not Before" (`nbf`) claim - identifies the time before which the JWT must not be accepted.
393 ///
394 /// This claim specifies the time before which the JWT must not be accepted for processing.
395 /// It is represented as the number of seconds from 1970-01-01T00:00:00Z UTC (the UNIX epoch).
396 ///
397 /// This field is automatically set when using `Claims::create()` or
398 /// `Claims::with_custom_claims()` to the current time, meaning the token is valid immediately.
399 /// It can be modified using the `invalid_before()` method.
400 #[serde(
401 rename = "nbf",
402 default,
403 skip_serializing_if = "Option::is_none",
404 with = "self::serde_additions::unix_timestamp"
405 )]
406 pub invalid_before: Option<UnixTimeStamp>,
407
408 /// The "Issuer" (`iss`) claim - identifies the principal that issued the JWT.
409 ///
410 /// This claim is a case-sensitive string and is typically a URI or an identifier for the
411 /// issuing system. It can be used to validate tokens from specific trusted issuers.
412 ///
413 /// This field is optional and can be set using the `with_issuer()` method.
414 #[serde(rename = "iss", default, skip_serializing_if = "Option::is_none")]
415 pub issuer: Option<String>,
416
417 /// The "Subject" (`sub`) claim - identifies the principal that is the subject of the JWT.
418 ///
419 /// This claim is a case-sensitive string and typically contains an identifier for the user
420 /// or entity on behalf of which the token was issued.
421 ///
422 /// This field is optional and can be set using the `with_subject()` method.
423 #[serde(rename = "sub", default, skip_serializing_if = "Option::is_none")]
424 pub subject: Option<String>,
425
426 /// The "Audience" (`aud`) claim - identifies the recipients that the JWT is intended for.
427 ///
428 /// This claim can be either a string value or an array of strings, each of which
429 /// typically identifies an intended recipient. Recipients must verify that they are
430 /// among the intended audience values.
431 ///
432 /// This field is optional and can be set using the `with_audience()` or `with_audiences()` methods.
433 #[serde(
434 rename = "aud",
435 default,
436 skip_serializing_if = "Option::is_none",
437 with = "self::serde_additions::audiences"
438 )]
439 pub audiences: Option<Audiences>,
440
441 /// The "JWT ID" (`jti`) claim - provides a unique identifier for the JWT.
442 ///
443 /// This claim creates a unique identifier for the token, which can be used to
444 /// prevent the JWT from being replayed (i.e., using the same token multiple times).
445 ///
446 /// While traditionally used for preventing replay attacks by storing all issued IDs,
447 /// this is challenging to scale. A more practical approach is to use timestamps.
448 ///
449 /// This field supports binary data through the custom Debug implementation that will
450 /// display non-UTF8 data as hex-encoded strings.
451 ///
452 /// This field is optional and can be set using the `with_jwt_id()` method.
453 #[serde(rename = "jti", default, skip_serializing_if = "Option::is_none")]
454 pub jwt_id: Option<String>,
455
456 /// The "Nonce" claim - provides a random value to prevent replay attacks.
457 ///
458 /// A nonce is a random value generated for use exactly once, which can be used to
459 /// prevent replay attacks. When a new JWT is issued, the nonce can be stored
460 /// temporarily and then checked when validating subsequent tokens.
461 ///
462 /// This field supports binary data through the custom Debug implementation that will
463 /// display non-UTF8 data as hex-encoded strings.
464 ///
465 /// This field is optional and can be set using the `with_nonce()` or `create_nonce()` methods.
466 #[serde(rename = "nonce", default, skip_serializing_if = "Option::is_none")]
467 pub nonce: Option<String>,
468
469 /// Custom application-defined claims.
470 ///
471 /// This field allows for including custom, application-specific claims in the JWT.
472 /// It must be a type that implements `Serialize` and `Deserialize`.
473 ///
474 /// Use `NoCustomClaims` if you don't need any custom claims, or your own type
475 /// to include custom data.
476 #[serde(flatten)]
477 pub custom: CustomClaims,
478}
479
480/// Custom Debug implementation for JWTClaims to handle binary data fields.
481///
482/// This implementation ensures that the `jwt_id` and `nonce` fields are displayed correctly,
483/// even if they contain non-UTF8 data:
484/// - For valid UTF-8 strings, displays them normally as strings
485/// - For strings containing invalid UTF-8 sequences, displays them as hex-encoded values
486///
487/// This is necessary because JWT tokens can contain binary data in these fields when used
488/// with CBOR Web Tokens (CWT) or when binary data is base64-encoded into JWT claims.
489impl<CustomClaims: std::fmt::Debug> std::fmt::Debug for JWTClaims<CustomClaims> {
490 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
491 // Helper function to format potentially non-UTF8 strings
492 let format_binary_string = |s: &[u8]| -> String {
493 if std::str::from_utf8(s).is_ok() {
494 // If the string contains only valid UTF-8, display it as a normal string
495 format!("Some(\"{}\")", String::from_utf8_lossy(s))
496 } else {
497 // If the string contains invalid UTF-8, display it as hex
498 let hex_encoded = Hex::encode_to_string(s).unwrap_or_default();
499 format!("Some(hex: \"{}\")", hex_encoded)
500 }
501 };
502
503 // Format jwt_id, handling binary data
504 let jwt_id_display = match &self.jwt_id {
505 Some(id) => format_binary_string(id.as_bytes()),
506 None => "None".to_string(),
507 };
508
509 // Format nonce, handling binary data
510 let nonce_display = match &self.nonce {
511 Some(nonce) => format_binary_string(nonce.as_bytes()),
512 None => "None".to_string(),
513 };
514
515 // Build debug representation with properly formatted fields
516 f.debug_struct("JWTClaims")
517 .field("issued_at", &self.issued_at)
518 .field("expires_at", &self.expires_at)
519 .field("invalid_before", &self.invalid_before)
520 .field("issuer", &self.issuer)
521 .field("subject", &self.subject)
522 .field("audiences", &self.audiences)
523 .field("jwt_id", &format_args!("{}", jwt_id_display))
524 .field("nonce", &format_args!("{}", nonce_display))
525 .field("custom", &self.custom)
526 .finish()
527 }
528}
529
530impl<CustomClaims> JWTClaims<CustomClaims> {
531 /// Create a new empty JWTClaims instance with default values
532 pub fn new() -> Self
533 where
534 CustomClaims: Default,
535 {
536 JWTClaims {
537 issued_at: None,
538 expires_at: None,
539 invalid_before: None,
540 audiences: None,
541 issuer: None,
542 subject: None,
543 jwt_id: None,
544 nonce: None,
545 custom: CustomClaims::default(),
546 }
547 }
548
549 /// Validates the claims against the provided verification options.
550 ///
551 /// This method performs a thorough validation of all standard JWT claims according to
552 /// the JWT specification (RFC 7519) and the provided options. It checks:
553 ///
554 /// - Time-based claims (`exp`, `nbf`, `iat`) against the current time, with configurable tolerance
555 /// - Issuer claim (`iss`) against allowed issuers
556 /// - Subject claim (`sub`) against required subject
557 /// - Audience claim (`aud`) against allowed audiences
558 /// - JWT ID claim (`jti`) against replay protection
559 /// - Nonce claim against required nonce
560 /// - Key ID claim (`kid`) against required key ID
561 ///
562 /// # Arguments
563 ///
564 /// * `options` - The verification options to use for validating the claims
565 ///
566 /// # Returns
567 ///
568 /// * `Ok(())` if all claims are valid according to the options
569 /// * `Err(Error)` if any validation fails, with a descriptive error message
570 ///
571 /// # Example
572 ///
573 /// ```
574 /// use jwt_simple::prelude::*;
575 /// use std::collections::HashSet;
576 ///
577 /// // Create verification options
578 /// let mut options = VerificationOptions::default();
579 ///
580 /// // Configure time tolerance
581 /// options.time_tolerance = Some(Duration::from_mins(15));
582 ///
583 /// // Require specific issuer
584 /// let mut allowed_issuers = HashSet::new();
585 /// allowed_issuers.insert("auth.example.com".to_string());
586 /// options.allowed_issuers = Some(allowed_issuers);
587 ///
588 /// // Verify the claims using these options
589 /// # let key = HS256Key::generate();
590 /// # let claims = Claims::create(Duration::from_hours(1)).with_issuer("auth.example.com");
591 /// # let token_str = key.authenticate(claims).unwrap();
592 /// let verified_claims = key.verify_token::<NoCustomClaims>(&token_str, Some(options)).unwrap();
593 /// ```
594 pub(crate) fn validate(&self, options: &VerificationOptions) -> Result<(), Error> {
595 let now = options
596 .artificial_time
597 .unwrap_or_else(Clock::now_since_epoch);
598 let time_tolerance = options.time_tolerance.unwrap_or_default();
599
600 if let Some(reject_before) = options.reject_before {
601 ensure!(now >= reject_before, JWTError::OldTokenReused);
602 }
603 if let Some(time_issued) = self.issued_at {
604 ensure!(time_issued <= now + time_tolerance, JWTError::ClockDrift);
605 if let Some(max_validity) = options.max_validity {
606 ensure!(
607 now <= time_issued || now - time_issued <= max_validity,
608 JWTError::TokenIsTooOld
609 );
610 }
611 }
612 if !options.accept_future {
613 if let Some(invalid_before) = self.invalid_before {
614 ensure!(
615 now + time_tolerance >= invalid_before,
616 JWTError::TokenNotValidYet
617 );
618 }
619 }
620 if let Some(expires_at) = self.expires_at {
621 ensure!(
622 now >= time_tolerance && now - time_tolerance <= expires_at,
623 JWTError::TokenHasExpired
624 );
625 }
626 if let Some(allowed_issuers) = &options.allowed_issuers {
627 if let Some(issuer) = &self.issuer {
628 ensure!(
629 allowed_issuers.contains(issuer),
630 JWTError::RequiredIssuerMismatch
631 );
632 } else {
633 bail!(JWTError::RequiredIssuerMissing);
634 }
635 }
636 if let Some(required_subject) = &options.required_subject {
637 if let Some(subject) = &self.subject {
638 ensure!(
639 subject == required_subject,
640 JWTError::RequiredSubjectMismatch
641 );
642 } else {
643 bail!(JWTError::RequiredSubjectMissing);
644 }
645 }
646 if let Some(required_nonce) = &options.required_nonce {
647 if let Some(nonce) = &self.nonce {
648 ensure!(nonce == required_nonce, JWTError::RequiredNonceMismatch);
649 } else {
650 bail!(JWTError::RequiredNonceMissing);
651 }
652 }
653 if let Some(allowed_audiences) = &options.allowed_audiences {
654 if let Some(audiences) = &self.audiences {
655 ensure!(
656 audiences.contains(allowed_audiences),
657 JWTError::RequiredAudienceMismatch
658 );
659 } else {
660 bail!(JWTError::RequiredAudienceMissing);
661 }
662 }
663 Ok(())
664 }
665
666 /// Sets the token as not being valid until the specified timestamp.
667 ///
668 /// This sets the `nbf` (Not Before) claim, which specifies the time before which the token
669 /// must not be accepted for processing.
670 ///
671 /// # Arguments
672 ///
673 /// * `unix_timestamp` - The UNIX timestamp (in seconds) before which the token should be rejected
674 ///
675 /// # Returns
676 ///
677 /// * The modified claims object for method chaining
678 ///
679 /// # Example
680 ///
681 /// ```
682 /// use jwt_simple::prelude::*;
683 ///
684 /// // Token will not be valid until 1 hour from now
685 /// let future_time = Clock::now_since_epoch() + Duration::from_hours(1);
686 /// let claims = Claims::create(Duration::from_hours(2))
687 /// .invalid_before(future_time);
688 /// ```
689 pub fn invalid_before(mut self, unix_timestamp: UnixTimeStamp) -> Self {
690 self.invalid_before = Some(unix_timestamp);
691 self
692 }
693
694 /// Sets the issuer claim (`iss`) for the token.
695 ///
696 /// The issuer claim identifies the principal that issued the JWT.
697 /// This can be used during token verification to ensure the token comes
698 /// from a trusted issuer.
699 ///
700 /// # Arguments
701 ///
702 /// * `issuer` - Any type that can be converted to a string, identifying the issuer
703 ///
704 /// # Returns
705 ///
706 /// * The modified claims object for method chaining
707 ///
708 /// # Example
709 ///
710 /// ```
711 /// use jwt_simple::prelude::*;
712 ///
713 /// let claims = Claims::create(Duration::from_hours(2))
714 /// .with_issuer("auth.example.com");
715 /// ```
716 pub fn with_issuer(mut self, issuer: impl ToString) -> Self {
717 self.issuer = Some(issuer.to_string());
718 self
719 }
720
721 /// Sets the subject claim (`sub`) for the token.
722 ///
723 /// The subject claim identifies the principal that is the subject of the JWT.
724 /// This is typically the user ID or another identifier for the token's subject.
725 ///
726 /// # Arguments
727 ///
728 /// * `subject` - Any type that can be converted to a string, identifying the subject
729 ///
730 /// # Returns
731 ///
732 /// * The modified claims object for method chaining
733 ///
734 /// # Example
735 ///
736 /// ```
737 /// use jwt_simple::prelude::*;
738 ///
739 /// let claims = Claims::create(Duration::from_hours(2))
740 /// .with_subject("user123@example.com");
741 /// ```
742 pub fn with_subject(mut self, subject: impl ToString) -> Self {
743 self.subject = Some(subject.to_string());
744 self
745 }
746
747 /// Sets multiple audience values (`aud`) for the token as a set.
748 ///
749 /// The audience claim identifies the recipients that the JWT is intended for.
750 /// This method allows specifying multiple audience values as a set.
751 ///
752 /// # Arguments
753 ///
754 /// * `audiences` - A HashSet of audience values that can be converted to strings
755 ///
756 /// # Returns
757 ///
758 /// * The modified claims object for method chaining
759 ///
760 /// # Example
761 ///
762 /// ```
763 /// use jwt_simple::prelude::*;
764 /// use std::collections::HashSet;
765 ///
766 /// let mut audiences = HashSet::new();
767 /// audiences.insert("https://api.example.com");
768 /// audiences.insert("https://admin.example.com");
769 ///
770 /// let claims = Claims::create(Duration::from_hours(2))
771 /// .with_audiences(audiences);
772 /// ```
773 pub fn with_audiences(mut self, audiences: HashSet<impl ToString>) -> Self {
774 self.audiences = Some(Audiences::AsSet(
775 audiences.iter().map(|x| x.to_string()).collect(),
776 ));
777 self
778 }
779
780 /// Sets a single audience value (`aud`) for the token as a string.
781 ///
782 /// The audience claim identifies the recipient that the JWT is intended for.
783 /// This method is convenient when you only need to specify a single audience.
784 ///
785 /// # Arguments
786 ///
787 /// * `audience` - Any type that can be converted to a string, identifying the audience
788 ///
789 /// # Returns
790 ///
791 /// * The modified claims object for method chaining
792 ///
793 /// # Example
794 ///
795 /// ```
796 /// use jwt_simple::prelude::*;
797 ///
798 /// let claims = Claims::create(Duration::from_hours(2))
799 /// .with_audience("https://api.example.com");
800 /// ```
801 pub fn with_audience(mut self, audience: impl ToString) -> Self {
802 self.audiences = Some(Audiences::AsString(audience.to_string()));
803 self
804 }
805
806 /// Sets the JWT ID claim (`jti`) for the token.
807 ///
808 /// The JWT ID claim provides a unique identifier for the JWT, which can be used
809 /// to prevent the token from being replayed. This is useful when a one-time token
810 /// is needed.
811 ///
812 /// # Arguments
813 ///
814 /// * `jwt_id` - Any type that can be converted to a string, providing a unique ID
815 ///
816 /// # Returns
817 ///
818 /// * The modified claims object for method chaining
819 ///
820 /// # Example
821 ///
822 /// ```
823 /// use jwt_simple::prelude::*;
824 ///
825 /// let claims = Claims::create(Duration::from_hours(2))
826 /// .with_jwt_id("token-123456");
827 /// ```
828 pub fn with_jwt_id(mut self, jwt_id: impl ToString) -> Self {
829 self.jwt_id = Some(jwt_id.to_string());
830 self
831 }
832
833 /// Sets the nonce claim for the token.
834 ///
835 /// A nonce is a random value that can be used to prevent replay attacks.
836 /// When a new JWT is created, a nonce can be included and stored. When a JWT
837 /// is received for verification, the previously stored nonce can be validated.
838 ///
839 /// # Arguments
840 ///
841 /// * `nonce` - Any type that can be converted to a string, representing the nonce
842 ///
843 /// # Returns
844 ///
845 /// * The modified claims object for method chaining
846 ///
847 /// # Example
848 ///
849 /// ```
850 /// use jwt_simple::prelude::*;
851 ///
852 /// let claims = Claims::create(Duration::from_hours(2))
853 /// .with_nonce("random-nonce-value");
854 /// ```
855 pub fn with_nonce(mut self, nonce: impl ToString) -> Self {
856 self.nonce = Some(nonce.to_string());
857 self
858 }
859
860 /// Creates a cryptographically secure random nonce, attaches it to the claims, and returns it.
861 ///
862 /// This method generates a 24-byte random nonce, encodes it using Base64UrlSafeNoPadding,
863 /// attaches it to the claims, and returns the generated nonce. This is useful for creating
864 /// tokens with built-in protection against replay attacks.
865 ///
866 /// # Returns
867 ///
868 /// * A string containing the Base64UrlSafeNoPadding-encoded nonce
869 ///
870 /// # Example
871 ///
872 /// ```
873 /// use jwt_simple::prelude::*;
874 ///
875 /// let mut claims = Claims::create(Duration::from_hours(2));
876 /// let nonce = claims.create_nonce();
877 /// // Store nonce for later verification
878 /// ```
879 pub fn create_nonce(&mut self) -> String {
880 let mut raw_nonce = [0u8; 24];
881 let mut rng = rand::thread_rng();
882 rng.fill_bytes(&mut raw_nonce);
883 let nonce = Base64UrlSafeNoPadding::encode_to_string(raw_nonce).unwrap();
884 self.nonce = Some(nonce);
885 self.nonce.as_deref().unwrap().to_string()
886 }
887}
888
889/// Factory for creating JWT claim sets with standard and custom claims.
890///
891/// This struct provides static methods for creating JWT claims with or without
892/// custom application-specific data.
893pub struct Claims;
894
895impl Claims {
896 /// Creates a new set of claims with standard JWT fields but no custom data.
897 ///
898 /// This method initializes a new claims object with:
899 /// - `iat` (Issued At) set to the current time
900 /// - `exp` (Expiration Time) set to the current time plus the specified duration
901 /// - `nbf` (Not Before) set to the current time
902 /// - All other standard claims initialized to None
903 /// - No custom claims (using `NoCustomClaims`)
904 ///
905 /// # Arguments
906 ///
907 /// * `valid_for` - The duration for which the token should be valid
908 ///
909 /// # Returns
910 ///
911 /// * A new `JWTClaims<NoCustomClaims>` object that can be further customized with the builder pattern
912 ///
913 /// # Example
914 ///
915 /// ```
916 /// use jwt_simple::prelude::*;
917 ///
918 /// // Create a token valid for 1 hour with standard fields
919 /// let claims = Claims::create(Duration::from_hours(1))
920 /// .with_issuer("auth.example.com")
921 /// .with_subject("user123");
922 ///
923 /// // Token can be created with any supported algorithm
924 /// let key = HS256Key::generate();
925 /// let token = key.authenticate(claims).unwrap();
926 /// ```
927 pub fn create(valid_for: Duration) -> JWTClaims<NoCustomClaims> {
928 let now = Clock::now_since_epoch();
929 JWTClaims {
930 issued_at: Some(now),
931 expires_at: Some(now + valid_for),
932 invalid_before: Some(now),
933 audiences: None,
934 issuer: None,
935 jwt_id: None,
936 subject: None,
937 nonce: None,
938 custom: NoCustomClaims {},
939 }
940 }
941
942 /// Creates a new set of claims with both standard JWT fields and custom application data.
943 ///
944 /// This method initializes a new claims object with:
945 /// - `iat` (Issued At) set to the current time
946 /// - `exp` (Expiration Time) set to the current time plus the specified duration
947 /// - `nbf` (Not Before) set to the current time
948 /// - All other standard claims initialized to None
949 /// - The provided custom claims
950 ///
951 /// # Type Parameters
952 ///
953 /// * `CustomClaims` - A type that implements `Serialize` and `DeserializeOwned` for custom application data
954 ///
955 /// # Arguments
956 ///
957 /// * `custom_claims` - The application-specific data to include in the token
958 /// * `valid_for` - The duration for which the token should be valid
959 ///
960 /// # Returns
961 ///
962 /// * A new `JWTClaims<CustomClaims>` object that can be further customized with the builder pattern
963 ///
964 /// # Example
965 ///
966 /// ```
967 /// use jwt_simple::prelude::*;
968 /// use serde::{Serialize, Deserialize};
969 ///
970 /// #[derive(Serialize, Deserialize)]
971 /// struct UserClaims {
972 /// user_id: u64,
973 /// roles: Vec<String>,
974 /// email: String,
975 /// }
976 ///
977 /// // Create custom claims
978 /// let user_data = UserClaims {
979 /// user_id: 1234,
980 /// roles: vec!["user".to_string(), "admin".to_string()],
981 /// email: "user@example.com".to_string(),
982 /// };
983 ///
984 /// // Create a token valid for 1 hour with custom data
985 /// let claims = Claims::with_custom_claims(user_data, Duration::from_hours(1))
986 /// .with_issuer("auth.example.com");
987 ///
988 /// // Token can be created with any supported algorithm
989 /// let key_pair = ES256KeyPair::generate();
990 /// let token = key_pair.sign(claims).unwrap();
991 /// ```
992 pub fn with_custom_claims<CustomClaims: Serialize + DeserializeOwned>(
993 custom_claims: CustomClaims,
994 valid_for: Duration,
995 ) -> JWTClaims<CustomClaims> {
996 let now = Clock::now_since_epoch();
997 JWTClaims {
998 issued_at: Some(now),
999 expires_at: Some(now + valid_for),
1000 invalid_before: Some(now),
1001 audiences: None,
1002 issuer: None,
1003 jwt_id: None,
1004 subject: None,
1005 nonce: None,
1006 custom: custom_claims,
1007 }
1008 }
1009}
1010
1011impl Default for JWTClaims<NoCustomClaims> {
1012 fn default() -> Self {
1013 JWTClaims {
1014 issued_at: None,
1015 expires_at: None,
1016 invalid_before: None,
1017 audiences: None,
1018 issuer: None,
1019 jwt_id: None,
1020 subject: None,
1021 nonce: None,
1022 custom: NoCustomClaims::default(),
1023 }
1024 }
1025}
1026
1027#[cfg(test)]
1028mod tests {
1029 use super::*;
1030
1031 #[test]
1032 fn should_set_standard_claims() {
1033 let exp = Duration::from_mins(10);
1034 let mut audiences = HashSet::new();
1035 audiences.insert("audience1".to_string());
1036 audiences.insert("audience2".to_string());
1037 let claims = Claims::create(exp)
1038 .with_audiences(audiences.clone())
1039 .with_issuer("issuer")
1040 .with_jwt_id("jwt_id")
1041 .with_nonce("nonce")
1042 .with_subject("subject");
1043
1044 assert_eq!(claims.audiences, Some(Audiences::AsSet(audiences)));
1045 assert_eq!(claims.issuer, Some("issuer".to_owned()));
1046 assert_eq!(claims.jwt_id, Some("jwt_id".to_owned()));
1047 assert_eq!(claims.nonce, Some("nonce".to_owned()));
1048 assert_eq!(claims.subject, Some("subject".to_owned()));
1049 }
1050
1051 #[test]
1052 fn parse_floating_point_unix_time() {
1053 let claims: JWTClaims<()> = serde_json::from_str(r#"{"exp":1617757825.8}"#).unwrap();
1054 assert_eq!(
1055 claims.expires_at,
1056 Some(UnixTimeStamp::from_secs(1617757825))
1057 );
1058 }
1059
1060 #[test]
1061 fn should_tolerate_clock_drift() {
1062 let exp = Duration::from_mins(1);
1063 let claims = Claims::create(exp);
1064 let mut options = VerificationOptions::default();
1065
1066 // Verifier clock is 2 minutes ahead of the token clock.
1067 // The token is valid for 1 minute, with an extra tolerance of 1 minute.
1068 // Verification should pass.
1069 let drift = Duration::from_mins(2);
1070 options.artificial_time = Some(claims.issued_at.unwrap() + drift);
1071 options.time_tolerance = Some(Duration::from_mins(1));
1072 claims.validate(&options).unwrap();
1073
1074 // Verifier clock is 2 minutes ahead of the token clock.
1075 // The token is valid for 1 minute, with an extra tolerance of 1 minute.
1076 // Verification must not pass.
1077 let drift = Duration::from_mins(3);
1078 options.artificial_time = Some(claims.issued_at.unwrap() + drift);
1079 options.time_tolerance = Some(Duration::from_mins(1));
1080 assert!(claims.validate(&options).is_err());
1081
1082 // Verifier clock is 2 minutes ahead of the token clock.
1083 // The token is valid for 30 seconds, with an extra tolerance of 1 minute.
1084 // Verification must not pass.
1085 let drift = Duration::from_secs(30);
1086 options.artificial_time = Some(claims.issued_at.unwrap() + drift);
1087 options.time_tolerance = Some(Duration::from_mins(1));
1088 claims.validate(&options).unwrap();
1089
1090 // Verifier clock is 2 minutes behind the token clock.
1091 // The token is valid for 1 minute, so it is already expired.
1092 // We have a tolerance of 1 minute.
1093 // Verification must not pass.
1094 let drift = Duration::from_mins(2);
1095 options.artificial_time = Some(claims.issued_at.unwrap() - drift);
1096 options.time_tolerance = Some(Duration::from_mins(1));
1097 assert!(claims.validate(&options).is_err());
1098
1099 // Verifier clock is 2 minutes behind the token clock.
1100 // The token is valid for 1 minute, so it is already expired.
1101 // We have a tolerance of 2 minute.
1102 // Verification should pass.
1103 let drift = Duration::from_mins(2);
1104 options.artificial_time = Some(claims.issued_at.unwrap() - drift);
1105 options.time_tolerance = Some(Duration::from_mins(2));
1106 claims.validate(&options).unwrap();
1107 }
1108
1109 #[test]
1110 fn debug_displays_jwt_id_correctly() {
1111 let exp = Duration::from_mins(10);
1112
1113 // Test valid UTF-8
1114 let claims1 = Claims::create(exp).with_jwt_id("valid-utf8-jwt-id");
1115 let debug_str1 = format!("{:?}", claims1);
1116 assert!(debug_str1.contains("jwt_id: Some(\"valid-utf8-jwt-id\")"));
1117
1118 // Create a binary JWT ID containing bytes that cannot be represented as valid UTF-8
1119 // We'll use a base64-encoded string with deliberately non-UTF8 bytes
1120 let binary_jwt_id =
1121 Base64UrlSafeNoPadding::encode_to_string(vec![0xff, 0x00, 0xfe, 0x7f]).unwrap();
1122
1123 // Create claims with the JWT ID containing binary data
1124 let claims2 = Claims::create(exp).with_jwt_id(binary_jwt_id);
1125
1126 // We need to modify the test assertion. Since we're representing the binary data
1127 // with a valid base64-encoded string (which is valid UTF-8), it will be displayed
1128 // as a regular string, not as hex. However, we still want to check that the Debug
1129 // implementation works correctly.
1130 let debug_str2 = format!("{:?}", claims2);
1131
1132 // The JWT ID will be displayed normally, so we'll just check the basic formatting
1133 assert!(debug_str2.contains("jwt_id: Some("));
1134 }
1135}