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}