huskarl_resource_server/validator/mod.rs
1//! Access token validation: the [`AccessTokenValidator`] trait and ready-made
2//! implementations of it.
3//!
4//! A validator turns an incoming request's headers into a [`ValidatedRequest`]
5//! (or a typed rejection). Pick the one matching how your tokens are verified:
6//! [`rfc9068::Rfc9068Validator`] for self-contained RFC 9068 JWT access tokens,
7//! [`introspection::IntrospectionValidator`] for RFC 7662 introspection, or
8//! [`custom::CustomValidator`] for an authorization server that follows neither.
9//! See [choosing a validator](crate::_docs::explanation::choosing_a_validator)
10//! for the trade-offs, and [`multi_issuer`] to accept more than one issuer.
11
12mod binding;
13mod common;
14pub mod custom;
15pub mod dpop_nonce;
16pub mod dpop_proof;
17pub mod error;
18pub mod extract;
19pub mod introspection;
20pub mod metadata;
21pub mod multi_issuer;
22pub mod observe;
23pub mod rfc9068;
24
25use crate::{
26 core::{
27 jwt::{ConfirmationClaim, validator::ValidatedJwt},
28 platform::{Duration, MaybeSendBoxFuture, MaybeSendSync, SystemTime},
29 },
30 error::ToRfc6750Error,
31};
32
33/// Default clock-skew leeway applied to the temporal checks (`exp`, `nbf`,
34/// `iat`, `DPoP` proof freshness). RFC 9449 §11.1 tells servers to accept
35/// proofs in a reasonable window around the current time; the same reasoning
36/// applies to access-token validation. Ten seconds absorbs real-world NTP
37/// drift without meaningfully weakening proof freshness — override it with
38/// the `clock_leeway` builder option on each validator.
39pub const DEFAULT_CLOCK_LEEWAY: Duration = Duration::from_secs(10);
40
41/// A trait for validators that authenticate and validate access tokens from HTTP requests.
42///
43/// Implementations handle token extraction from request headers, JWT validation,
44/// and sender-constraint binding checks (`DPoP`, mTLS).
45///
46/// The `outcome` field of [`ValidationResult`] is `Ok(None)` when no authentication header is
47/// present (unauthenticated request), `Ok(Some(_))` when a valid token is found, and `Err(_)`
48/// when a token is present but invalid.
49pub trait AccessTokenValidator: MaybeSendSync {
50 /// The application-specific claims type extracted from the token.
51 type Claims: MaybeSendSync;
52 /// The error type returned when validation fails.
53 type Error: ToRfc6750Error;
54
55 /// Validates an access token from the given HTTP request headers.
56 ///
57 /// `uri` must be the **absolute external target URI** the client addressed
58 /// (scheme, authority, and path); it is compared against the `htu` claim of
59 /// any `DPoP` proof (RFC 9449 §4.3). A non-absolute URI fails `DPoP`
60 /// validation with a server-side integration error, never a per-request
61 /// mismatch; deployments that never accept `DPoP` tokens do not use `uri`.
62 /// Behind TLS-terminating or rewriting proxies only the deployment knows this
63 /// URI — [validating DPoP-bound tokens](crate::_docs::guide::dpop) covers
64 /// reconstructing it.
65 ///
66 /// Returns a boxed future so the trait is object-safe: heterogeneous
67 /// validators can be stored as `Box<dyn AccessTokenValidator<…>>` (see
68 /// [`multi_issuer`]). Concrete validators also expose an inherent `validate_request`
69 /// that returns an unboxed future for zero-cost direct use.
70 fn validate_request<'a>(
71 &'a self,
72 headers: &'a http::HeaderMap,
73 method: &'a http::Method,
74 uri: &'a http::Uri,
75 client_cert_der: Option<&'a [u8]>,
76 ) -> MaybeSendBoxFuture<'a, ValidationResult<Self::Claims, Self::Error>>;
77}
78
79/// The result of an [`AccessTokenValidator::validate_request`] call.
80///
81/// To turn an unauthenticated or failed result into the matching HTTP
82/// response (status code, `WWW-Authenticate` challenges, `DPoP-Nonce`
83/// header), use [`rejection`](Self::rejection) — see the
84/// [`rejection`](crate::rejection) module.
85#[derive(Debug)]
86pub struct ValidationResult<C, E> {
87 /// The outcome of the validation.
88 pub outcome: Result<Option<ValidatedRequest<C>>, E>,
89 /// A `DPoP` nonce to include in the response `DPoP-Nonce` header, if any.
90 ///
91 /// Set on failures that demand a nonce (`use_dpop_nonce`) **and on
92 /// successful validations** whose nonce is approaching expiry — echo it
93 /// in both cases, or clients lose nonce freshness and pay a retry.
94 /// [`rejection`](Self::rejection) carries it through automatically on
95 /// the error path; on success it remains yours to send.
96 pub dpop_nonce: Option<String>,
97}
98
99/// A validated access token request, containing the parsed claims and other metadata.
100///
101/// Returned by [`super::AccessTokenValidator::validate_request`].
102#[derive(Debug)]
103pub struct ValidatedRequest<Claims> {
104 /// The issuer of the token, if present.
105 pub issuer: Option<String>,
106 /// The subject of the token, if present.
107 pub subject: Option<String>,
108 /// The audience of the token.
109 pub audience: Vec<String>,
110 /// The token ID, if present.
111 pub jti: Option<String>,
112 /// The issued-at timestamp, if present.
113 pub issued_at: Option<SystemTime>,
114 /// The expiration timestamp, if present.
115 pub expiration: Option<SystemTime>,
116 /// The key confirmation claim (`cnf`, RFC 7800), if present.
117 ///
118 /// Binds the token to a `DPoP` key (`jkt`, RFC 9449) or mTLS certificate
119 /// (`x5t#S256`, RFC 8705).
120 pub cnf: Option<ConfirmationClaim>,
121 /// Additional claims beyond the registered token claim set.
122 ///
123 /// Format-specific and extension claims (e.g. RFC 9068 `client_id` and the
124 /// RFC 9396 `authorization_details`) live here, in the typed claims type —
125 /// not as fields on this struct, which carries only the universal token set.
126 pub claims: Claims,
127 /// Raw introspection JWT (RFC 9701), if the authorization server returned one.
128 ///
129 /// Populated only when using [`crate::validator::introspection::IntrospectionValidator`] and the
130 /// AS responded with `application/token-introspection+jwt`. Useful for forwarding to
131 /// downstream services.
132 pub introspection_jwt: Option<String>,
133}
134
135impl<C> From<ValidatedJwt<C>> for ValidatedRequest<C> {
136 fn from(jwt: ValidatedJwt<C>) -> Self {
137 Self {
138 issuer: jwt.issuer,
139 subject: jwt.subject,
140 audience: jwt.audience,
141 jti: jwt.jti,
142 issued_at: jwt.issued_at,
143 expiration: jwt.expiration,
144 cnf: jwt.cnf,
145 claims: jwt.claims,
146 introspection_jwt: None,
147 }
148 }
149}