mas_jose/
claims.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7use std::{collections::HashMap, convert::Infallible, marker::PhantomData, ops::Deref};
8
9use base64ct::{Base64UrlUnpadded, Encoding};
10use mas_iana::jose::JsonWebSignatureAlg;
11use serde::{Deserialize, Serialize, de::DeserializeOwned};
12use sha2::{Digest, Sha256, Sha384, Sha512};
13use thiserror::Error;
14
15#[derive(Debug, Error)]
16pub enum ClaimError {
17    #[error("missing claim {0:?}")]
18    MissingClaim(&'static str),
19
20    #[error("invalid claim {0:?}")]
21    InvalidClaim(&'static str),
22
23    #[error("could not validate claim {claim:?}")]
24    ValidationError {
25        claim: &'static str,
26        #[source]
27        source: Box<dyn std::error::Error + Send + Sync + 'static>,
28    },
29}
30
31pub trait Validator<T> {
32    /// The associated error type returned by this validator.
33    type Error;
34
35    /// Validate a claim value
36    ///
37    /// # Errors
38    ///
39    /// Returns an error if the value is invalid.
40    fn validate(&self, value: &T) -> Result<(), Self::Error>;
41}
42
43impl<T> Validator<T> for () {
44    type Error = Infallible;
45
46    fn validate(&self, _value: &T) -> Result<(), Self::Error> {
47        Ok(())
48    }
49}
50
51pub struct Claim<T, V = ()> {
52    claim: &'static str,
53    t: PhantomData<T>,
54    v: PhantomData<V>,
55}
56
57impl<T, V> Claim<T, V>
58where
59    V: Validator<T>,
60{
61    #[must_use]
62    pub const fn new(claim: &'static str) -> Self {
63        Self {
64            claim,
65            t: PhantomData,
66            v: PhantomData,
67        }
68    }
69
70    /// Insert a claim into the given claims map.
71    ///
72    /// # Errors
73    ///
74    /// Returns an error if the value failed to serialize.
75    pub fn insert<I>(
76        &self,
77        claims: &mut HashMap<String, serde_json::Value>,
78        value: I,
79    ) -> Result<(), ClaimError>
80    where
81        I: Into<T>,
82        T: Serialize,
83    {
84        let value = value.into();
85        let value: serde_json::Value =
86            serde_json::to_value(&value).map_err(|_| ClaimError::InvalidClaim(self.claim))?;
87        claims.insert(self.claim.to_owned(), value);
88
89        Ok(())
90    }
91
92    /// Extract a claim from the given claims map.
93    ///
94    /// # Errors
95    ///
96    /// Returns an error if the value failed to deserialize, if its value is
97    /// invalid or if the claim is missing.
98    pub fn extract_required(
99        &self,
100        claims: &mut HashMap<String, serde_json::Value>,
101    ) -> Result<T, ClaimError>
102    where
103        T: DeserializeOwned,
104        V: Default,
105        V::Error: std::error::Error + Send + Sync + 'static,
106    {
107        let validator = V::default();
108        self.extract_required_with_options(claims, validator)
109    }
110
111    /// Extract a claim from the given claims map, with the given options.
112    ///
113    /// # Errors
114    ///
115    /// Returns an error if the value failed to deserialize, if its value is
116    /// invalid or if the claim is missing.
117    pub fn extract_required_with_options<I>(
118        &self,
119        claims: &mut HashMap<String, serde_json::Value>,
120        validator: I,
121    ) -> Result<T, ClaimError>
122    where
123        T: DeserializeOwned,
124        I: Into<V>,
125        V::Error: std::error::Error + Send + Sync + 'static,
126    {
127        let validator: V = validator.into();
128        let claim = claims
129            .remove(self.claim)
130            .ok_or(ClaimError::MissingClaim(self.claim))?;
131
132        let res =
133            serde_json::from_value(claim).map_err(|_| ClaimError::InvalidClaim(self.claim))?;
134        validator
135            .validate(&res)
136            .map_err(|source| ClaimError::ValidationError {
137                claim: self.claim,
138                source: Box::new(source),
139            })?;
140        Ok(res)
141    }
142
143    /// Extract a claim from the given claims map, if it exists.
144    ///
145    /// # Errors
146    ///
147    /// Returns an error if the value failed to deserialize or if its value is
148    /// invalid.
149    pub fn extract_optional(
150        &self,
151        claims: &mut HashMap<String, serde_json::Value>,
152    ) -> Result<Option<T>, ClaimError>
153    where
154        T: DeserializeOwned,
155        V: Default,
156        V::Error: std::error::Error + Send + Sync + 'static,
157    {
158        let validator = V::default();
159        self.extract_optional_with_options(claims, validator)
160    }
161
162    /// Extract a claim from the given claims map, if it exists, with the given
163    /// options.
164    ///
165    /// # Errors
166    ///
167    /// Returns an error if the value failed to deserialize or if its value is
168    /// invalid.
169    pub fn extract_optional_with_options<I>(
170        &self,
171        claims: &mut HashMap<String, serde_json::Value>,
172        validator: I,
173    ) -> Result<Option<T>, ClaimError>
174    where
175        T: DeserializeOwned,
176        I: Into<V>,
177        V::Error: std::error::Error + Send + Sync + 'static,
178    {
179        match self.extract_required_with_options(claims, validator) {
180            Ok(v) => Ok(Some(v)),
181            Err(ClaimError::MissingClaim(_)) => Ok(None),
182            Err(e) => Err(e),
183        }
184    }
185}
186
187#[derive(Debug, Clone)]
188pub struct TimeOptions {
189    when: chrono::DateTime<chrono::Utc>,
190    leeway: chrono::Duration,
191}
192
193impl TimeOptions {
194    #[must_use]
195    pub fn new(when: chrono::DateTime<chrono::Utc>) -> Self {
196        Self {
197            when,
198            leeway: chrono::Duration::microseconds(5 * 60 * 1000 * 1000),
199        }
200    }
201
202    #[must_use]
203    pub fn leeway(mut self, leeway: chrono::Duration) -> Self {
204        self.leeway = leeway;
205        self
206    }
207}
208
209#[derive(Debug, Clone, Copy, Error)]
210#[error("Current time is too far away")]
211pub struct TimeTooFarError;
212
213#[derive(Debug, Clone)]
214pub struct TimeNotAfter(TimeOptions);
215
216impl Validator<Timestamp> for TimeNotAfter {
217    type Error = TimeTooFarError;
218    fn validate(&self, value: &Timestamp) -> Result<(), Self::Error> {
219        if self.0.when <= value.0 + self.0.leeway {
220            Ok(())
221        } else {
222            Err(TimeTooFarError)
223        }
224    }
225}
226
227impl From<TimeOptions> for TimeNotAfter {
228    fn from(opt: TimeOptions) -> Self {
229        Self(opt)
230    }
231}
232
233impl From<&TimeOptions> for TimeNotAfter {
234    fn from(opt: &TimeOptions) -> Self {
235        opt.clone().into()
236    }
237}
238
239#[derive(Debug, Clone)]
240pub struct TimeNotBefore(TimeOptions);
241
242impl Validator<Timestamp> for TimeNotBefore {
243    type Error = TimeTooFarError;
244
245    fn validate(&self, value: &Timestamp) -> Result<(), Self::Error> {
246        if self.0.when >= value.0 - self.0.leeway {
247            Ok(())
248        } else {
249            Err(TimeTooFarError)
250        }
251    }
252}
253
254impl From<TimeOptions> for TimeNotBefore {
255    fn from(opt: TimeOptions) -> Self {
256        Self(opt)
257    }
258}
259
260impl From<&TimeOptions> for TimeNotBefore {
261    fn from(opt: &TimeOptions) -> Self {
262        opt.clone().into()
263    }
264}
265
266/// Hash the given token with the given algorithm for an ID Token claim.
267///
268/// According to the [OpenID Connect Core 1.0 specification].
269///
270/// # Errors
271///
272/// Returns an error if the algorithm is not supported.
273///
274/// [OpenID Connect Core 1.0 specification]: https://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken
275pub fn hash_token(alg: &JsonWebSignatureAlg, token: &str) -> Result<String, TokenHashError> {
276    let bits = match alg {
277        JsonWebSignatureAlg::Hs256
278        | JsonWebSignatureAlg::Rs256
279        | JsonWebSignatureAlg::Es256
280        | JsonWebSignatureAlg::Ps256
281        | JsonWebSignatureAlg::Es256K => {
282            let mut hasher = Sha256::new();
283            hasher.update(token);
284            let hash: [u8; 32] = hasher.finalize().into();
285            // Left-most half
286            hash[..16].to_owned()
287        }
288        JsonWebSignatureAlg::Hs384
289        | JsonWebSignatureAlg::Rs384
290        | JsonWebSignatureAlg::Es384
291        | JsonWebSignatureAlg::Ps384 => {
292            let mut hasher = Sha384::new();
293            hasher.update(token);
294            let hash: [u8; 48] = hasher.finalize().into();
295            // Left-most half
296            hash[..24].to_owned()
297        }
298        JsonWebSignatureAlg::Hs512
299        | JsonWebSignatureAlg::Rs512
300        | JsonWebSignatureAlg::Es512
301        | JsonWebSignatureAlg::Ps512 => {
302            let mut hasher = Sha512::new();
303            hasher.update(token);
304            let hash: [u8; 64] = hasher.finalize().into();
305            // Left-most half
306            hash[..32].to_owned()
307        }
308        _ => return Err(TokenHashError::UnsupportedAlgorithm),
309    };
310
311    Ok(Base64UrlUnpadded::encode_string(&bits))
312}
313
314#[derive(Debug, Clone, Copy, Error)]
315pub enum TokenHashError {
316    #[error("Hashes don't match")]
317    HashMismatch,
318
319    #[error("Unsupported algorithm for hashing")]
320    UnsupportedAlgorithm,
321}
322
323#[derive(Debug, Clone)]
324pub struct TokenHash<'a> {
325    alg: &'a JsonWebSignatureAlg,
326    token: &'a str,
327}
328
329impl<'a> TokenHash<'a> {
330    /// Creates a new `TokenHash` validator for the given algorithm and token.
331    #[must_use]
332    pub fn new(alg: &'a JsonWebSignatureAlg, token: &'a str) -> Self {
333        Self { alg, token }
334    }
335}
336
337impl Validator<String> for TokenHash<'_> {
338    type Error = TokenHashError;
339    fn validate(&self, value: &String) -> Result<(), Self::Error> {
340        if hash_token(self.alg, self.token)? == *value {
341            Ok(())
342        } else {
343            Err(TokenHashError::HashMismatch)
344        }
345    }
346}
347
348#[derive(Debug, Clone, Copy, Error)]
349#[error("Values don't match")]
350pub struct EqualityError;
351
352#[derive(Debug, Clone)]
353pub struct Equality<'a, T: ?Sized> {
354    value: &'a T,
355}
356
357impl<'a, T: ?Sized> Equality<'a, T> {
358    /// Creates a new `Equality` validator for the given value.
359    #[must_use]
360    pub fn new(value: &'a T) -> Self {
361        Self { value }
362    }
363}
364
365impl<T1, T2> Validator<T1> for Equality<'_, T2>
366where
367    T2: PartialEq<T1> + ?Sized,
368{
369    type Error = EqualityError;
370    fn validate(&self, value: &T1) -> Result<(), Self::Error> {
371        if *self.value == *value {
372            Ok(())
373        } else {
374            Err(EqualityError)
375        }
376    }
377}
378
379impl<'a, T: ?Sized> From<&'a T> for Equality<'a, T> {
380    fn from(value: &'a T) -> Self {
381        Self::new(value)
382    }
383}
384
385#[derive(Debug, Clone)]
386pub struct Contains<'a, T> {
387    value: &'a T,
388}
389
390impl<'a, T> Contains<'a, T> {
391    /// Creates a new `Contains` validator for the given value.
392    #[must_use]
393    pub fn new(value: &'a T) -> Self {
394        Self { value }
395    }
396}
397
398#[derive(Debug, Clone, Copy, Error)]
399#[error("OneOrMany doesn't contain value")]
400pub struct ContainsError;
401
402impl<T> Validator<OneOrMany<T>> for Contains<'_, T>
403where
404    T: PartialEq,
405{
406    type Error = ContainsError;
407    fn validate(&self, value: &OneOrMany<T>) -> Result<(), Self::Error> {
408        if value.contains(self.value) {
409            Ok(())
410        } else {
411            Err(ContainsError)
412        }
413    }
414}
415
416impl<'a, T> From<&'a T> for Contains<'a, T> {
417    fn from(value: &'a T) -> Self {
418        Self::new(value)
419    }
420}
421
422#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
423#[serde(transparent)]
424pub struct Timestamp(#[serde(with = "chrono::serde::ts_seconds")] chrono::DateTime<chrono::Utc>);
425
426impl Deref for Timestamp {
427    type Target = chrono::DateTime<chrono::Utc>;
428
429    fn deref(&self) -> &Self::Target {
430        &self.0
431    }
432}
433
434impl From<chrono::DateTime<chrono::Utc>> for Timestamp {
435    fn from(value: chrono::DateTime<chrono::Utc>) -> Self {
436        Timestamp(value)
437    }
438}
439
440#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
441#[serde(
442    transparent,
443    bound(serialize = "T: Serialize", deserialize = "T: Deserialize<'de>")
444)]
445pub struct OneOrMany<T>(
446    // serde_as seems to not work properly with #[serde(transparent)]
447    // We have use plain old #[serde(with = ...)] with serde_with's utilities, which is a bit
448    // verbose but works
449    #[serde(
450        with = "serde_with::As::<serde_with::OneOrMany<serde_with::Same, serde_with::formats::PreferOne>>"
451    )]
452    Vec<T>,
453);
454
455impl<T> Deref for OneOrMany<T> {
456    type Target = Vec<T>;
457
458    fn deref(&self) -> &Self::Target {
459        &self.0
460    }
461}
462
463impl<T> From<Vec<T>> for OneOrMany<T> {
464    fn from(value: Vec<T>) -> Self {
465        Self(value)
466    }
467}
468
469impl<T> From<T> for OneOrMany<T> {
470    fn from(value: T) -> Self {
471        Self(vec![value])
472    }
473}
474
475/// Claims defined in RFC7519 sec. 4.1
476/// <https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1>
477mod rfc7519 {
478    use super::{Claim, Contains, Equality, OneOrMany, TimeNotAfter, TimeNotBefore, Timestamp};
479
480    pub const ISS: Claim<String, Equality<str>> = Claim::new("iss");
481    pub const SUB: Claim<String> = Claim::new("sub");
482    pub const AUD: Claim<OneOrMany<String>, Contains<String>> = Claim::new("aud");
483    pub const NBF: Claim<Timestamp, TimeNotBefore> = Claim::new("nbf");
484    pub const EXP: Claim<Timestamp, TimeNotAfter> = Claim::new("exp");
485    pub const IAT: Claim<Timestamp, TimeNotBefore> = Claim::new("iat");
486    pub const JTI: Claim<String> = Claim::new("jti");
487}
488
489/// Claims defined in OIDC.Core sec. 2 and sec. 5.1
490/// <https://openid.net/specs/openid-connect-core-1_0.html#IDToken>
491/// <https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims>
492mod oidc_core {
493    use url::Url;
494
495    use super::{Claim, Equality, Timestamp, TokenHash};
496
497    pub const AUTH_TIME: Claim<Timestamp> = Claim::new("auth_time");
498    pub const NONCE: Claim<String, Equality<str>> = Claim::new("nonce");
499    pub const AT_HASH: Claim<String, TokenHash> = Claim::new("at_hash");
500    pub const C_HASH: Claim<String, TokenHash> = Claim::new("c_hash");
501
502    pub const NAME: Claim<String> = Claim::new("name");
503    pub const GIVEN_NAME: Claim<String> = Claim::new("given_name");
504    pub const FAMILY_NAME: Claim<String> = Claim::new("family_name");
505    pub const MIDDLE_NAME: Claim<String> = Claim::new("middle_name");
506    pub const NICKNAME: Claim<String> = Claim::new("nickname");
507    pub const PREFERRED_USERNAME: Claim<String> = Claim::new("preferred_username");
508    pub const PROFILE: Claim<Url> = Claim::new("profile");
509    pub const PICTURE: Claim<Url> = Claim::new("picture");
510    pub const WEBSITE: Claim<Url> = Claim::new("website");
511    // TODO: email type?
512    pub const EMAIL: Claim<String> = Claim::new("email");
513    pub const EMAIL_VERIFIED: Claim<bool> = Claim::new("email_verified");
514    pub const GENDER: Claim<String> = Claim::new("gender");
515    // TODO: date type
516    pub const BIRTHDATE: Claim<String> = Claim::new("birthdate");
517    // TODO: timezone type
518    pub const ZONEINFO: Claim<String> = Claim::new("zoneinfo");
519    // TODO: locale type
520    pub const LOCALE: Claim<String> = Claim::new("locale");
521    // TODO: phone number type
522    pub const PHONE_NUMBER: Claim<String> = Claim::new("phone_number");
523    pub const PHONE_NUMBER_VERIFIED: Claim<bool> = Claim::new("phone_number_verified");
524    // TODO: pub const ADDRESS: Claim<Timestamp> = Claim::new("address");
525    pub const UPDATED_AT: Claim<Timestamp> = Claim::new("updated_at");
526}
527
528pub use self::{oidc_core::*, rfc7519::*};
529
530#[cfg(test)]
531mod tests {
532    use chrono::TimeZone;
533
534    use super::*;
535
536    #[test]
537    fn timestamp_serde() {
538        let datetime = Timestamp(
539            chrono::Utc
540                .with_ymd_and_hms(2018, 1, 18, 1, 30, 22)
541                .unwrap(),
542        );
543        let timestamp = serde_json::Value::Number(1_516_239_022.into());
544
545        assert_eq!(datetime, serde_json::from_value(timestamp.clone()).unwrap());
546        assert_eq!(timestamp, serde_json::to_value(&datetime).unwrap());
547    }
548
549    #[test]
550    fn one_or_many_serde() {
551        let one = OneOrMany(vec!["one".to_owned()]);
552        let many = OneOrMany(vec!["one".to_owned(), "two".to_owned()]);
553
554        assert_eq!(
555            one,
556            serde_json::from_value(serde_json::json!("one")).unwrap()
557        );
558        assert_eq!(
559            one,
560            serde_json::from_value(serde_json::json!(["one"])).unwrap()
561        );
562        assert_eq!(
563            many,
564            serde_json::from_value(serde_json::json!(["one", "two"])).unwrap()
565        );
566        assert_eq!(
567            serde_json::to_value(&one).unwrap(),
568            serde_json::json!("one")
569        );
570        assert_eq!(
571            serde_json::to_value(&many).unwrap(),
572            serde_json::json!(["one", "two"])
573        );
574    }
575
576    #[test]
577    fn extract_claims() {
578        let now = chrono::Utc
579            .with_ymd_and_hms(2018, 1, 18, 1, 30, 22)
580            .unwrap();
581        let expiration = now + chrono::Duration::microseconds(5 * 60 * 1000 * 1000);
582        let time_options = TimeOptions::new(now).leeway(chrono::Duration::zero());
583
584        let claims = serde_json::json!({
585            "iss": "https://foo.com",
586            "sub": "johndoe",
587            "aud": ["abcd-efgh"],
588            "iat": 1_516_239_022,
589            "nbf": 1_516_239_022,
590            "exp": 1_516_239_322,
591            "jti": "1122-3344-5566-7788",
592        });
593        let mut claims = serde_json::from_value(claims).unwrap();
594
595        let iss = ISS
596            .extract_required_with_options(&mut claims, "https://foo.com")
597            .unwrap();
598        let sub = SUB.extract_optional(&mut claims).unwrap();
599        let aud = AUD
600            .extract_optional_with_options(&mut claims, &"abcd-efgh".to_owned())
601            .unwrap();
602        let nbf = NBF
603            .extract_optional_with_options(&mut claims, &time_options)
604            .unwrap();
605        let exp = EXP
606            .extract_optional_with_options(&mut claims, &time_options)
607            .unwrap();
608        let iat = IAT
609            .extract_optional_with_options(&mut claims, &time_options)
610            .unwrap();
611        let jti = JTI.extract_optional(&mut claims).unwrap();
612
613        assert_eq!(iss, "https://foo.com".to_owned());
614        assert_eq!(sub, Some("johndoe".to_owned()));
615        assert_eq!(aud.as_deref(), Some(&vec!["abcd-efgh".to_owned()]));
616        assert_eq!(iat.as_deref(), Some(&now));
617        assert_eq!(nbf.as_deref(), Some(&now));
618        assert_eq!(exp.as_deref(), Some(&expiration));
619        assert_eq!(jti, Some("1122-3344-5566-7788".to_owned()));
620
621        assert!(claims.is_empty());
622    }
623
624    #[test]
625    fn time_validation() {
626        let now = chrono::Utc
627            .with_ymd_and_hms(2018, 1, 18, 1, 30, 22)
628            .unwrap();
629
630        let claims = serde_json::json!({
631            "iat": 1_516_239_022,
632            "nbf": 1_516_239_022,
633            "exp": 1_516_239_322,
634        });
635        let claims: HashMap<String, serde_json::Value> = serde_json::from_value(claims).unwrap();
636
637        // Everything should be fine at this point, the claims iat & nbf == now
638        {
639            let mut claims = claims.clone();
640
641            // so no leeway should be fine as well here
642            let time_options = TimeOptions::new(now).leeway(chrono::Duration::zero());
643            assert!(
644                IAT.extract_required_with_options(&mut claims, &time_options)
645                    .is_ok()
646            );
647            assert!(
648                NBF.extract_required_with_options(&mut claims, &time_options)
649                    .is_ok()
650            );
651            assert!(
652                EXP.extract_required_with_options(&mut claims, &time_options)
653                    .is_ok()
654            );
655        }
656
657        // Let's go back in time a bit
658        let now = now - chrono::Duration::microseconds(60 * 1000 * 1000);
659
660        {
661            // There is now a time variance between the two parties...
662            let mut claims = claims.clone();
663
664            // but no time variance is allowed. "iat" and "nbf" validation will fail
665            let time_options = TimeOptions::new(now).leeway(chrono::Duration::zero());
666            assert!(matches!(
667                IAT.extract_required_with_options(&mut claims, &time_options),
668                Err(ClaimError::ValidationError { claim: "iat", .. }),
669            ));
670            assert!(matches!(
671                NBF.extract_required_with_options(&mut claims, &time_options),
672                Err(ClaimError::ValidationError { claim: "nbf", .. }),
673            ));
674            assert!(
675                EXP.extract_required_with_options(&mut claims, &time_options)
676                    .is_ok()
677            );
678        }
679
680        {
681            // This time, there is a two minute leeway, they all should be fine
682            let mut claims = claims.clone();
683
684            // but no time variance is allowed. "iat" and "nbf" validation will fail
685            let time_options =
686                TimeOptions::new(now).leeway(chrono::Duration::microseconds(2 * 60 * 1000 * 1000));
687            assert!(
688                IAT.extract_required_with_options(&mut claims, &time_options)
689                    .is_ok()
690            );
691            assert!(
692                NBF.extract_required_with_options(&mut claims, &time_options)
693                    .is_ok()
694            );
695            assert!(
696                EXP.extract_required_with_options(&mut claims, &time_options)
697                    .is_ok()
698            );
699        }
700
701        // Let's wait some time so it expires
702        let now = now + chrono::Duration::microseconds((1 + 6) * 60 * 1000 * 1000);
703
704        {
705            // At this point, the claims expired one minute ago
706            let mut claims = claims.clone();
707
708            // but no time variance is allowed. "exp" validation will fail
709            let time_options = TimeOptions::new(now).leeway(chrono::Duration::zero());
710            assert!(
711                IAT.extract_required_with_options(&mut claims, &time_options)
712                    .is_ok()
713            );
714            assert!(
715                NBF.extract_required_with_options(&mut claims, &time_options)
716                    .is_ok()
717            );
718            assert!(matches!(
719                EXP.extract_required_with_options(&mut claims, &time_options),
720                Err(ClaimError::ValidationError { claim: "exp", .. }),
721            ));
722        }
723
724        {
725            let mut claims = claims;
726
727            // Same, but with a 2 minutes leeway should be fine then
728            let time_options =
729                TimeOptions::new(now).leeway(chrono::Duration::try_minutes(2).unwrap());
730            assert!(
731                IAT.extract_required_with_options(&mut claims, &time_options)
732                    .is_ok()
733            );
734            assert!(
735                NBF.extract_required_with_options(&mut claims, &time_options)
736                    .is_ok()
737            );
738            assert!(
739                EXP.extract_required_with_options(&mut claims, &time_options)
740                    .is_ok()
741            );
742        }
743    }
744
745    #[test]
746    fn invalid_claims() {
747        let now = chrono::Utc
748            .with_ymd_and_hms(2018, 1, 18, 1, 30, 22)
749            .unwrap();
750        let time_options = TimeOptions::new(now).leeway(chrono::Duration::zero());
751
752        let claims = serde_json::json!({
753            "iss": 123,
754            "sub": 456,
755            "aud": 789,
756            "iat": "123",
757            "nbf": "456",
758            "exp": "789",
759            "jti": 123,
760        });
761        let mut claims = serde_json::from_value(claims).unwrap();
762
763        assert!(matches!(
764            ISS.extract_required_with_options(&mut claims, "https://foo.com"),
765            Err(ClaimError::InvalidClaim("iss"))
766        ));
767        assert!(matches!(
768            SUB.extract_required(&mut claims),
769            Err(ClaimError::InvalidClaim("sub"))
770        ));
771        assert!(matches!(
772            AUD.extract_required_with_options(&mut claims, &"abcd-efgh".to_owned()),
773            Err(ClaimError::InvalidClaim("aud"))
774        ));
775        assert!(matches!(
776            NBF.extract_required_with_options(&mut claims, &time_options),
777            Err(ClaimError::InvalidClaim("nbf"))
778        ));
779        assert!(matches!(
780            EXP.extract_required_with_options(&mut claims, &time_options),
781            Err(ClaimError::InvalidClaim("exp"))
782        ));
783        assert!(matches!(
784            IAT.extract_required_with_options(&mut claims, &time_options),
785            Err(ClaimError::InvalidClaim("iat"))
786        ));
787        assert!(matches!(
788            JTI.extract_required(&mut claims),
789            Err(ClaimError::InvalidClaim("jti"))
790        ));
791    }
792
793    #[test]
794    fn missing_claims() {
795        // Empty claim set
796        let mut claims = HashMap::new();
797
798        assert!(matches!(
799            ISS.extract_required_with_options(&mut claims, "https://foo.com"),
800            Err(ClaimError::MissingClaim("iss"))
801        ));
802        assert!(matches!(
803            SUB.extract_required(&mut claims),
804            Err(ClaimError::MissingClaim("sub"))
805        ));
806        assert!(matches!(
807            AUD.extract_required_with_options(&mut claims, &"abcd-efgh".to_owned()),
808            Err(ClaimError::MissingClaim("aud"))
809        ));
810
811        assert!(matches!(
812            ISS.extract_optional_with_options(&mut claims, "https://foo.com"),
813            Ok(None)
814        ));
815        assert!(matches!(SUB.extract_optional(&mut claims), Ok(None)));
816        assert!(matches!(
817            AUD.extract_optional_with_options(&mut claims, &"abcd-efgh".to_owned()),
818            Ok(None)
819        ));
820    }
821
822    #[test]
823    fn string_eq_validation() {
824        let claims = serde_json::json!({
825            "iss": "https://foo.com",
826        });
827        let mut claims: HashMap<String, serde_json::Value> =
828            serde_json::from_value(claims).unwrap();
829
830        ISS.extract_required_with_options(&mut claims.clone(), "https://foo.com")
831            .unwrap();
832
833        assert!(matches!(
834            ISS.extract_required_with_options(&mut claims, "https://bar.com"),
835            Err(ClaimError::ValidationError { claim: "iss", .. }),
836        ));
837    }
838
839    #[test]
840    fn contains_validation() {
841        let claims = serde_json::json!({
842            "aud": "abcd-efgh",
843        });
844        let mut claims: HashMap<String, serde_json::Value> =
845            serde_json::from_value(claims).unwrap();
846
847        AUD.extract_required_with_options(&mut claims.clone(), &"abcd-efgh".to_owned())
848            .unwrap();
849
850        assert!(matches!(
851            AUD.extract_required_with_options(&mut claims, &"wxyz".to_owned()),
852            Err(ClaimError::ValidationError { claim: "aud", .. }),
853        ));
854    }
855}