Skip to main content

identity_kit/resolver/
jwt.rs

1//! `JwtValidator`: validation of a JWT that is not a credential or a presentation.
2
3use core::fmt;
4use core::time::Duration;
5use std::collections::BTreeSet;
6use std::time::{SystemTime, UNIX_EPOCH};
7
8use serde::de::DeserializeOwned;
9
10use super::{KeyReference, KeyReferenceError, KeyResolutionError, KeyResolver, KeyRole};
11use crate::jose::{
12    CompactJws, DefaultVerifier, Jwk, JwsAlgorithm, JwsError, JwsHeader, JwsVerifier, JwtClaims,
13    OneOrMany, VerificationError,
14};
15
16/// Validates a compact JWT that is neither a credential nor a presentation: an OAuth 2.0 access
17/// token (RFC 9068), an ID token, a request object.
18///
19/// The checks follow RFC 7519 §7.2 and the best current practices of RFC 8725, in this order:
20///
21/// 1. The token is a compact JWS ([`CompactJws::decode`]), whose `alg` is one the options allow
22///    (RFC 8725 §3.1) and whose header `typ` is the one the options expect, if they expect one
23///    (RFC 8725 §3.11). `typ` is compared as RFC 7515 §4.1.9 says: ignoring case and an
24///    `application/` prefix.
25/// 2. The payload is a JSON claims set.
26/// 3. The key the header names ([`KeyReference::from_jws`], with the `iss` of the claims) is
27///    resolved for the [`KeyRole`] of the options and verifies the signature.
28/// 4. Under [`IssuerBinding::KeyOfIssuer`], the default, the key belongs to `iss`
29///    ([`KeyReference::binds_to`]): `iss` is the DID of a `kid` DID URL, the HTTPS issuer of a
30///    key set, or the `did:jwk` or thumbprint of a key by value.
31/// 5. `iss` is the expected issuer, and `aud` includes the expected audience, when the options
32///    name them.
33/// 6. `exp`, `nbf` and `iat` hold at the validation time, within the leeway.
34/// 7. Every claim the options require is present.
35///
36/// What else a token must say (a `scope`, a `client_id`, a `nonce`) is the application's to
37/// check, on the claims it gets back. Explicit typing is what keeps one kind of JWT from being
38/// accepted as another (RFC 8725 §3.11); name the `typ` whenever the token has one.
39///
40/// # Examples
41///
42/// An authorization server signs an access token with the Ed25519 key of RFC 8037 Appendix
43/// A.1, naming it by a DID URL while its `iss` is its HTTPS URL. The resource server pins that
44/// one key: its resolver knows no other, so the key need not belong to `iss`. A resolver that
45/// returns any key a token carries, such as [`JwkResolver`](super::JwkResolver), must never be
46/// combined with [`IssuerBinding::Any`]: anyone could sign with their own key.
47///
48/// ```
49/// use identity_kit::jose::{DefaultVerifier, Jwk, JwsAlgorithm, JwsHeader, JwtClaims, SignError, Signer, encode_compact};
50/// use identity_kit::resolver::{
51///     IssuerBinding, JwtValidationError, JwtValidationOptions, JwtValidator, KeyReference,
52///     KeyResolutionError, KeyResolver, KeyRole,
53/// };
54///
55/// struct Server(ed25519_dalek::SigningKey);
56///
57/// #[async_trait::async_trait]
58/// impl Signer for Server {
59///     fn alg(&self) -> JwsAlgorithm { JwsAlgorithm::EdDSA }
60///     fn kid(&self) -> Option<&str> { None }
61///     async fn sign(&self, input: &[u8]) -> Result<Vec<u8>, SignError> {
62///         use ed25519_dalek::Signer as _;
63///         Ok(self.0.sign(input).to_bytes().to_vec())
64///     }
65/// }
66///
67/// /// The authorization server's one key, under the one DID URL it signs with.
68/// struct Pinned(Jwk);
69///
70/// #[async_trait::async_trait]
71/// impl KeyResolver for Pinned {
72///     async fn resolve_key(&self, key: &KeyReference, _: KeyRole) -> Result<Jwk, KeyResolutionError> {
73///         match key {
74///             KeyReference::DidUrl(url) if url.as_str() == "did:example:as#key-1" => Ok(self.0.clone()),
75///             other => Err(KeyResolutionError::unsupported(other)),
76///         }
77///     }
78/// }
79///
80/// # tokio::runtime::Builder::new_current_thread().build()?.block_on(async {
81/// let secret = [
82///     0x9d, 0x61, 0xb1, 0x9d, 0xef, 0xfd, 0x5a, 0x60, 0xba, 0x84, 0x4a, 0xf4, 0x92, 0xec, 0x2c, 0xc4,
83///     0x44, 0x49, 0xc5, 0x69, 0x7b, 0x32, 0x69, 0x19, 0x70, 0x3b, 0xac, 0x03, 0x1c, 0xae, 0x7f, 0x60,
84/// ];
85/// let server = Server(ed25519_dalek::SigningKey::from_bytes(&secret));
86/// let key = Jwk::from_ed25519_public(server.0.verifying_key().as_bytes())?;
87///
88/// let header = JwsHeader::new(JwsAlgorithm::EdDSA)
89///     .with_typ("at+jwt")
90///     .with_kid("did:example:as#key-1");
91/// let mut claims = JwtClaims::new()
92///     .with_iss("https://as.example.com")
93///     .with_sub("5ba552d67")
94///     .with_aud("https://rs.example.com")
95///     .with_jti("dbe39bf3a3ba4238a513f51d6e1691c4")
96///     .with_claim("client_id", "s6BhdRkqt3")
97///     .with_claim("scope", "openid profile");
98/// claims.iat = Some(1_700_000_000);
99/// claims.exp = Some(1_700_003_600);
100/// let token = encode_compact(&header, &serde_json::to_vec(&claims)?, &server).await?;
101///
102/// // The key is the pinned one, not the DID of `iss`, so the binding is waived.
103/// let validator = JwtValidator::new(Pinned(key.clone()), DefaultVerifier);
104/// let options = JwtValidationOptions::new()
105///     .with_typ("at+jwt")
106///     .with_algorithm(JwsAlgorithm::EdDSA)
107///     .with_issuer("https://as.example.com")
108///     .with_audience("https://rs.example.com")
109///     .with_issuer_binding(IssuerBinding::Any)
110///     .with_required_claims(["exp", "iat", "jti", "sub", "client_id"])
111///     .at(1_700_000_100);
112/// let validated = validator.validate(&token, &options).await?;
113/// assert_eq!(validated.claims.sub.as_deref(), Some("5ba552d67"));
114/// assert_eq!(validated.key, key);
115///
116/// // A token naming any other key is refused.
117/// let forged = encode_compact(
118///     &JwsHeader::new(JwsAlgorithm::EdDSA).with_typ("at+jwt").with_kid("did:example:attacker#key-1"),
119///     &serde_json::to_vec(&claims)?,
120///     &server,
121/// )
122/// .await?;
123/// let error = validator.validate(&forged, &options).await.unwrap_err();
124/// assert!(matches!(error, JwtValidationError::KeyResolution { .. }));
125///
126/// // An hour and a minute later it has expired.
127/// let error = validator.validate(&token, &options.at(1_700_003_660)).await.unwrap_err();
128/// assert!(matches!(error, JwtValidationError::Expired { expired_at: 1_700_003_600, .. }));
129/// # Ok::<(), Box<dyn std::error::Error>>(())
130/// # })?;
131/// # Ok::<(), Box<dyn std::error::Error>>(())
132/// ```
133pub struct JwtValidator<R, V = DefaultVerifier> {
134    resolver: R,
135    verifier: V,
136}
137
138impl<R: KeyResolver, V: JwsVerifier> JwtValidator<R, V> {
139    /// A validator resolving keys with `resolver` and verifying signatures with `verifier`.
140    pub fn new(resolver: R, verifier: V) -> Self {
141        Self { resolver, verifier }
142    }
143
144    /// The key resolver.
145    pub fn resolver(&self) -> &R {
146        &self.resolver
147    }
148
149    /// The signature verifier.
150    pub fn verifier(&self) -> &V {
151        &self.verifier
152    }
153
154    /// Validates `token`, returning its claims as a [`JwtClaims`] set.
155    ///
156    /// # Errors
157    ///
158    /// The first [`JwtValidationError`] found, in the order [`JwtValidator`] lists the checks.
159    pub async fn validate(
160        &self,
161        token: &str,
162        options: &JwtValidationOptions,
163    ) -> Result<ValidatedJwt, JwtValidationError> {
164        self.validate_as(token, options).await
165    }
166
167    /// Validates `token`, returning its claims read into `T`, once the registered claims have
168    /// been checked.
169    ///
170    /// # Errors
171    ///
172    /// As [`validate`](Self::validate), and [`JwtValidationError::Claims`] when the payload
173    /// does not fit `T`.
174    pub async fn validate_as<T: DeserializeOwned>(
175        &self,
176        token: &str,
177        options: &JwtValidationOptions,
178    ) -> Result<ValidatedJwt<T>, JwtValidationError> {
179        let jws = CompactJws::decode(token)?;
180        let header = jws.header();
181        if !options.allows(header.alg) {
182            return Err(JwtValidationError::DisallowedAlgorithm(header.alg));
183        }
184        if let Some(expected) = &options.typ
185            && !header
186                .typ
187                .as_deref()
188                .is_some_and(|typ| same_typ(typ, expected))
189        {
190            return Err(JwtValidationError::UnexpectedTyp {
191                expected: expected.clone(),
192                found: header.typ.clone(),
193            });
194        }
195        let claims: JwtClaims =
196            serde_json::from_slice(jws.payload()).map_err(JwtValidationError::Payload)?;
197
198        let reference = KeyReference::from_jws(&jws, claims.iss.as_deref())?;
199        let key = self
200            .resolver
201            .resolve_key(&reference, options.role)
202            .await
203            .map_err(|source| JwtValidationError::KeyResolution {
204                key: Box::new(reference.clone()),
205                source,
206            })?;
207        jws.verify(&self.verifier, &key)?;
208        if options.issuer_binding == IssuerBinding::KeyOfIssuer
209            && !reference.binds_to(claims.iss.as_deref(), &key)
210        {
211            return Err(JwtValidationError::KeyNotIssuers {
212                issuer: claims.iss.clone(),
213                key: Box::new(reference),
214            });
215        }
216
217        if let Some(expected) = &options.issuer
218            && claims.iss.as_ref() != Some(expected)
219        {
220            return Err(JwtValidationError::IssuerMismatch {
221                expected: expected.clone(),
222                found: claims.iss.clone(),
223            });
224        }
225        if let Some(expected) = &options.audience
226            && !claims.audiences().any(|audience| audience == expected)
227        {
228            return Err(JwtValidationError::AudienceMismatch {
229                expected: expected.clone(),
230                found: claims.aud.clone(),
231            });
232        }
233        options.check_times(&claims)?;
234        for claim in &options.required_claims {
235            if !has_claim(&claims, claim) {
236                return Err(JwtValidationError::MissingClaim(claim.clone()));
237            }
238        }
239
240        let typed = serde_json::from_slice(jws.payload()).map_err(JwtValidationError::Claims)?;
241        Ok(ValidatedJwt {
242            header: header.clone(),
243            claims: typed,
244            key,
245            key_reference: reference,
246        })
247    }
248}
249
250impl<R: fmt::Debug, V: fmt::Debug> fmt::Debug for JwtValidator<R, V> {
251    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252        f.debug_struct("JwtValidator")
253            .field("resolver", &self.resolver)
254            .field("verifier", &self.verifier)
255            .finish()
256    }
257}
258
259/// Whether the header `typ` `found` is the media type `expected`: compared ignoring ASCII case,
260/// with an `application/` prefix on either side optional (RFC 7515 §4.1.9).
261fn same_typ(found: &str, expected: &str) -> bool {
262    fn bare(typ: &str) -> &str {
263        match typ.get(..12) {
264            Some(prefix) if prefix.eq_ignore_ascii_case("application/") => &typ[12..],
265            _ => typ,
266        }
267    }
268    bare(found).eq_ignore_ascii_case(bare(expected))
269}
270
271/// Whether `claims` has the claim `name`, registered or not.
272fn has_claim(claims: &JwtClaims, name: &str) -> bool {
273    match name {
274        "iss" => claims.iss.is_some(),
275        "sub" => claims.sub.is_some(),
276        "aud" => claims.aud.is_some(),
277        "exp" => claims.exp.is_some(),
278        "nbf" => claims.nbf.is_some(),
279        "iat" => claims.iat.is_some(),
280        "jti" => claims.jti.is_some(),
281        other => claims.additional.contains_key(other),
282    }
283}
284
285/// A JWT that passed [`JwtValidator::validate`]: its header, its claims and the key that signed
286/// it.
287#[derive(Debug, Clone)]
288#[non_exhaustive]
289pub struct ValidatedJwt<T = JwtClaims> {
290    /// The JWS header.
291    pub header: JwsHeader,
292    /// The claims, as a [`JwtClaims`] set or the type [`JwtValidator::validate_as`] read them
293    /// into.
294    pub claims: T,
295    /// The public key that verified the signature.
296    pub key: Jwk,
297    /// Where the header said the key was.
298    pub key_reference: KeyReference,
299}
300
301/// Whether the key that signed a token must belong to its issuer.
302#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
303#[non_exhaustive]
304pub enum IssuerBinding {
305    /// The key must belong to `iss` ([`KeyReference::binds_to`]): a `kid` DID URL of the DID
306    /// `iss`, a key of the key set of the HTTPS `iss`, or a key by value whose `did:jwk` or
307    /// thumbprint is `iss`, or which comes with no `iss` at all.
308    #[default]
309    KeyOfIssuer,
310    /// Any key the [`KeyResolver`] resolves is accepted, whatever `iss` says: for a resolver
311    /// that only resolves keys the application trusts, such as an authorization server's
312    /// signing keys behind a `kid` that is a DID URL of a DID other than its `iss` URL.
313    ///
314    /// Never with a resolver that returns a key the token itself carries, such as
315    /// [`JwkResolver`](super::JwkResolver) for a `jwk` header, or one that resolves any DID:
316    /// the issuer binding is then the only thing tying the key to `iss`, and without it anyone
317    /// can sign a token with their own key.
318    Any,
319}
320
321/// Options of [`JwtValidator`]: what the token must be, and when.
322///
323/// The default accepts every algorithm the crate verifies, any `typ`, any issuer and audience,
324/// with the key bound to `iss`, at the current time without leeway, and requires no claim.
325#[derive(Debug, Clone, Default, PartialEq, Eq)]
326#[non_exhaustive]
327pub struct JwtValidationOptions {
328    /// The algorithms accepted; every algorithm when empty.
329    pub algorithms: BTreeSet<JwsAlgorithm>,
330    /// The header `typ` the token must have, if any.
331    pub typ: Option<String>,
332    /// The `iss` the token must have, if any.
333    pub issuer: Option<String>,
334    /// The audience the token's `aud` must include, if any.
335    pub audience: Option<String>,
336    /// The role the signing key is resolved for: [`KeyRole::Issuer`] (`assertionMethod` in a
337    /// DID document) by default, [`KeyRole::Subject`] (`authentication`) for a token that
338    /// proves possession.
339    pub role: KeyRole,
340    /// Whether the signing key must belong to `iss`.
341    pub issuer_binding: IssuerBinding,
342    /// Claims that must be present, registered or not.
343    pub required_claims: BTreeSet<String>,
344    /// The validation time in seconds since the Unix epoch; the current time when `None`.
345    pub now: Option<i64>,
346    /// How far `exp`, `nbf` and `iat` may miss the validation time and still pass.
347    pub leeway: Duration,
348}
349
350impl JwtValidationOptions {
351    /// The default options.
352    pub fn new() -> Self {
353        Self::default()
354    }
355
356    /// These options also accepting `alg`. Once one algorithm is named, only the named ones
357    /// are accepted.
358    #[must_use]
359    pub fn with_algorithm(mut self, alg: JwsAlgorithm) -> Self {
360        self.algorithms.insert(alg);
361        self
362    }
363
364    /// These options requiring the header `typ` to be `typ`.
365    #[must_use]
366    pub fn with_typ(mut self, typ: impl Into<String>) -> Self {
367        self.typ = Some(typ.into());
368        self
369    }
370
371    /// These options requiring `iss` to be `issuer`.
372    #[must_use]
373    pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
374        self.issuer = Some(issuer.into());
375        self
376    }
377
378    /// These options requiring `aud` to include `audience`.
379    #[must_use]
380    pub fn with_audience(mut self, audience: impl Into<String>) -> Self {
381        self.audience = Some(audience.into());
382        self
383    }
384
385    /// These options resolving the signing key for `role`.
386    #[must_use]
387    pub fn with_role(mut self, role: KeyRole) -> Self {
388        self.role = role;
389        self
390    }
391
392    /// These options with `binding`.
393    #[must_use]
394    pub fn with_issuer_binding(mut self, binding: IssuerBinding) -> Self {
395        self.issuer_binding = binding;
396        self
397    }
398
399    /// These options also requiring every claim of `claims`.
400    #[must_use]
401    pub fn with_required_claims<I, S>(mut self, claims: I) -> Self
402    where
403        I: IntoIterator<Item = S>,
404        S: Into<String>,
405    {
406        self.required_claims
407            .extend(claims.into_iter().map(Into::into));
408        self
409    }
410
411    /// These options validating at `now`, in seconds since the Unix epoch.
412    #[must_use]
413    pub fn at(mut self, now: i64) -> Self {
414        self.now = Some(now);
415        self
416    }
417
418    /// These options with `leeway`.
419    #[must_use]
420    pub fn with_leeway(mut self, leeway: Duration) -> Self {
421        self.leeway = leeway;
422        self
423    }
424
425    /// The validation time, in seconds since the Unix epoch.
426    pub fn now(&self) -> i64 {
427        self.now.unwrap_or_else(|| {
428            let since_epoch = SystemTime::now()
429                .duration_since(UNIX_EPOCH)
430                .unwrap_or(Duration::ZERO);
431            i64::try_from(since_epoch.as_secs()).unwrap_or(i64::MAX)
432        })
433    }
434
435    /// Whether `alg` is accepted.
436    fn allows(&self, alg: JwsAlgorithm) -> bool {
437        self.algorithms.is_empty() || self.algorithms.contains(&alg)
438    }
439
440    /// Checks `exp`, `nbf` and `iat` at the validation time, within the leeway.
441    fn check_times(&self, claims: &JwtClaims) -> Result<(), JwtValidationError> {
442        let now = self.now();
443        let leeway = i64::try_from(self.leeway.as_secs()).unwrap_or(i64::MAX);
444        let latest = now.saturating_add(leeway);
445        let earliest = now.saturating_sub(leeway);
446        if let Some(expired_at) = claims.exp
447            && expired_at <= earliest
448        {
449            return Err(JwtValidationError::Expired { expired_at, now });
450        }
451        if let Some(not_before) = claims.nbf
452            && not_before > latest
453        {
454            return Err(JwtValidationError::NotYetValid { not_before, now });
455        }
456        if let Some(issued_at) = claims.iat
457            && issued_at > latest
458        {
459            return Err(JwtValidationError::IssuedInFuture { issued_at, now });
460        }
461        Ok(())
462    }
463}
464
465/// Why a JWT did not pass [`JwtValidator::validate`].
466///
467/// Times are `NumericDate` seconds since the Unix epoch.
468#[derive(Debug, thiserror::Error)]
469#[non_exhaustive]
470pub enum JwtValidationError {
471    /// The token is not a compact JWS this crate decodes.
472    #[error("invalid JWS: {0}")]
473    Jws(#[from] JwsError),
474    /// The header `alg` is not one the options accept.
475    #[error("algorithm `{0}` is not accepted")]
476    DisallowedAlgorithm(JwsAlgorithm),
477    /// The header `typ` is not the one the options require, or is missing.
478    #[error("header `typ` is {found:?}, not `{expected}`")]
479    UnexpectedTyp {
480        /// The `typ` required.
481        expected: String,
482        /// The `typ` found.
483        found: Option<String>,
484    },
485    /// The payload is not a JSON claims set; the source is the JSON error.
486    #[error("the payload is not a JWT claims set: {0}")]
487    Payload(#[source] serde_json::Error),
488    /// The header names no key this crate can look for.
489    #[error(transparent)]
490    KeyReference(#[from] KeyReferenceError),
491    /// The key the token names could not be resolved.
492    #[error("resolving key {key}: {source}")]
493    KeyResolution {
494        /// Where the token says its key is.
495        key: Box<KeyReference>,
496        /// What went wrong.
497        #[source]
498        source: KeyResolutionError,
499    },
500    /// The signature does not verify under the resolved key.
501    #[error("signature: {0}")]
502    Signature(#[from] VerificationError),
503    /// The key that signed the token does not belong to its `iss`.
504    #[error("signing key {key} does not belong to issuer {issuer:?}")]
505    KeyNotIssuers {
506        /// The `iss`, if the token has one.
507        issuer: Option<String>,
508        /// Where the token says its key is.
509        key: Box<KeyReference>,
510    },
511    /// `iss` is not the expected issuer.
512    #[error("issuer {found:?} is not {expected:?}")]
513    IssuerMismatch {
514        /// The issuer expected.
515        expected: String,
516        /// The `iss` found.
517        found: Option<String>,
518    },
519    /// `aud` does not include the expected audience.
520    #[error("audience {found:?} does not include {expected:?}")]
521    AudienceMismatch {
522        /// The audience expected.
523        expected: String,
524        /// The `aud` found.
525        found: Option<OneOrMany<String>>,
526    },
527    /// The token has expired.
528    #[error("expired at {expired_at} (now is {now})")]
529    Expired {
530        /// The `exp`.
531        expired_at: i64,
532        /// The validation time.
533        now: i64,
534    },
535    /// The token is not valid yet.
536    #[error("not valid before {not_before} (now is {now})")]
537    NotYetValid {
538        /// The `nbf`.
539        not_before: i64,
540        /// The validation time.
541        now: i64,
542    },
543    /// The token claims to have been issued in the future.
544    #[error("issued at {issued_at}, after now ({now})")]
545    IssuedInFuture {
546        /// The `iat`.
547        issued_at: i64,
548        /// The validation time.
549        now: i64,
550    },
551    /// A claim the options require is missing.
552    #[error("missing claim `{0}`")]
553    MissingClaim(String),
554    /// The claims do not fit the type [`JwtValidator::validate_as`] reads them into; the source
555    /// is the JSON error.
556    #[error("the claims do not fit the expected type: {0}")]
557    Claims(#[source] serde_json::Error),
558}