huskarl-resource-server 0.9.0

OAuth2 resource server (JWT validation) support for the huskarl ecosystem.
Documentation
//! Access token validation: the [`AccessTokenValidator`] trait and ready-made
//! implementations of it.
//!
//! A validator turns an incoming request's headers into a [`ValidatedRequest`]
//! (or a typed rejection). Pick the one matching how your tokens are verified:
//! [`rfc9068::Rfc9068Validator`] for self-contained RFC 9068 JWT access tokens,
//! [`introspection::IntrospectionValidator`] for RFC 7662 introspection, or
//! [`custom::CustomValidator`] for an authorization server that follows neither.
//! See [choosing a validator](crate::_docs::explanation::choosing_a_validator)
//! for the trade-offs, and [`multi_issuer`] to accept more than one issuer.

mod binding;
mod common;
pub mod custom;
pub mod dpop_nonce;
pub mod dpop_proof;
pub mod error;
pub mod extract;
pub mod introspection;
pub mod metadata;
pub mod multi_issuer;
pub mod observe;
pub mod rfc9068;

use crate::{
    core::{
        jwt::{ConfirmationClaim, validator::ValidatedJwt},
        platform::{Duration, MaybeSendBoxFuture, MaybeSendSync, SystemTime},
    },
    error::ToRfc6750Error,
};

/// Default clock-skew leeway applied to the temporal checks (`exp`, `nbf`,
/// `iat`, `DPoP` proof freshness). RFC 9449 §11.1 tells servers to accept
/// proofs in a reasonable window around the current time; the same reasoning
/// applies to access-token validation. Ten seconds absorbs real-world NTP
/// drift without meaningfully weakening proof freshness — override it with
/// the `clock_leeway` builder option on each validator.
pub const DEFAULT_CLOCK_LEEWAY: Duration = Duration::from_secs(10);

/// A trait for validators that authenticate and validate access tokens from HTTP requests.
///
/// Implementations handle token extraction from request headers, JWT validation,
/// and sender-constraint binding checks (`DPoP`, mTLS).
///
/// The `outcome` field of [`ValidationResult`] is `Ok(None)` when no authentication header is
/// present (unauthenticated request), `Ok(Some(_))` when a valid token is found, and `Err(_)`
/// when a token is present but invalid.
pub trait AccessTokenValidator: MaybeSendSync {
    /// The application-specific claims type extracted from the token.
    type Claims: MaybeSendSync;
    /// The error type returned when validation fails.
    type Error: ToRfc6750Error;

    /// Validates an access token from the given HTTP request headers.
    ///
    /// `uri` must be the **absolute external target URI** the client addressed
    /// (scheme, authority, and path); it is compared against the `htu` claim of
    /// any `DPoP` proof (RFC 9449 §4.3). A non-absolute URI fails `DPoP`
    /// validation with a server-side integration error, never a per-request
    /// mismatch; deployments that never accept `DPoP` tokens do not use `uri`.
    /// Behind TLS-terminating or rewriting proxies only the deployment knows this
    /// URI — [validating DPoP-bound tokens](crate::_docs::guide::dpop) covers
    /// reconstructing it.
    ///
    /// Returns a boxed future so the trait is object-safe: heterogeneous
    /// validators can be stored as `Box<dyn AccessTokenValidator<…>>` (see
    /// [`multi_issuer`]). Concrete validators also expose an inherent `validate_request`
    /// that returns an unboxed future for zero-cost direct use.
    fn validate_request<'a>(
        &'a self,
        headers: &'a http::HeaderMap,
        method: &'a http::Method,
        uri: &'a http::Uri,
        client_cert_der: Option<&'a [u8]>,
    ) -> MaybeSendBoxFuture<'a, ValidationResult<Self::Claims, Self::Error>>;
}

/// The result of an [`AccessTokenValidator::validate_request`] call.
///
/// To turn an unauthenticated or failed result into the matching HTTP
/// response (status code, `WWW-Authenticate` challenges, `DPoP-Nonce`
/// header), use [`rejection`](Self::rejection) — see the
/// [`rejection`](crate::rejection) module.
#[derive(Debug)]
pub struct ValidationResult<C, E> {
    /// The outcome of the validation.
    pub outcome: Result<Option<ValidatedRequest<C>>, E>,
    /// A `DPoP` nonce to include in the response `DPoP-Nonce` header, if any.
    ///
    /// Set on failures that demand a nonce (`use_dpop_nonce`) **and on
    /// successful validations** whose nonce is approaching expiry — echo it
    /// in both cases, or clients lose nonce freshness and pay a retry.
    /// [`rejection`](Self::rejection) carries it through automatically on
    /// the error path; on success it remains yours to send.
    pub dpop_nonce: Option<String>,
}

/// A validated access token request, containing the parsed claims and other metadata.
///
/// Returned by [`super::AccessTokenValidator::validate_request`].
#[derive(Debug)]
pub struct ValidatedRequest<Claims> {
    /// The issuer of the token, if present.
    pub issuer: Option<String>,
    /// The subject of the token, if present.
    pub subject: Option<String>,
    /// The audience of the token.
    pub audience: Vec<String>,
    /// The token ID, if present.
    pub jti: Option<String>,
    /// The issued-at timestamp, if present.
    pub issued_at: Option<SystemTime>,
    /// The expiration timestamp, if present.
    pub expiration: Option<SystemTime>,
    /// The key confirmation claim (`cnf`, RFC 7800), if present.
    ///
    /// Binds the token to a `DPoP` key (`jkt`, RFC 9449) or mTLS certificate
    /// (`x5t#S256`, RFC 8705).
    pub cnf: Option<ConfirmationClaim>,
    /// Additional claims beyond the registered token claim set.
    ///
    /// Format-specific and extension claims (e.g. RFC 9068 `client_id` and the
    /// RFC 9396 `authorization_details`) live here, in the typed claims type —
    /// not as fields on this struct, which carries only the universal token set.
    pub claims: Claims,
    /// Raw introspection JWT (RFC 9701), if the authorization server returned one.
    ///
    /// Populated only when using [`crate::validator::introspection::IntrospectionValidator`] and the
    /// AS responded with `application/token-introspection+jwt`. Useful for forwarding to
    /// downstream services.
    pub introspection_jwt: Option<String>,
}

impl<C> From<ValidatedJwt<C>> for ValidatedRequest<C> {
    fn from(jwt: ValidatedJwt<C>) -> Self {
        Self {
            issuer: jwt.issuer,
            subject: jwt.subject,
            audience: jwt.audience,
            jti: jwt.jti,
            issued_at: jwt.issued_at,
            expiration: jwt.expiration,
            cnf: jwt.cnf,
            claims: jwt.claims,
            introspection_jwt: None,
        }
    }
}