Skip to main content

oauth_resource_server/
http_layer.rs

1//! A `tower` authentication layer for any HTTP stack built on the `http` crate's
2//! types — hyper, tonic, or a `tower` service of your own — whatever its body
3//! types: [`HttpAuthLayer`] (built with [`HttpAuthLayerBuilder`]) wraps a
4//! `Service<http::Request<ReqBody>, Response = http::Response<ResBody>>` for any
5//! `ReqBody` and any `ResBody`.
6//!
7//! ```
8//! use http::{Request, Response};
9//! use oauth_resource_server::http_layer::HttpAuthLayer;
10//! use tower::{ServiceBuilder, service_fn};
11//!
12//! let auth = HttpAuthLayer::builder()
13//!     .static_token("example-static-key")
14//!     .build()
15//!     .unwrap();
16//! let service = ServiceBuilder::new().layer(auth).service(service_fn(
17//!     |_request: Request<String>| async { Ok::<_, std::convert::Infallible>(Response::new(String::from("ok"))) },
18//! ));
19//! # let _ = service;
20//! ```
21//!
22//! On an axum app use [`crate::axum::AuthLayer`] instead (feature `axum`, which
23//! turns this feature on): it is the same check, with axum's `Response`, the
24//! `require_auth` middleware form, and the axum extractors. Behind an
25//! [`HttpAuthLayer`] alone those extractors return the value it inserted
26//! (`AuthorizedToken`, `Credential`, `StaticTokenMatch`, and their `Option`
27//! forms), but answer 500 when there is none — even `Option<..>` behind an
28//! `optional()` pass-through, never `None` — because they cannot tell it ran.
29//!
30//! # Behavior
31//!
32//! Identical to the axum layer, because both run the same code: every
33//! configured [`CredentialSource`] contributes one candidate to
34//! [`crate::authenticate()`]; an accepted request gets the [`Credential`] (and,
35//! for an OAuth token, the [`AuthorizedToken`]; for a static token, the
36//! [`StaticTokenMatch`] naming which one) inserted into its extensions;
37//! a refused one gets the status and `WWW-Authenticate` challenge
38//! [`crate::refusal()`] describes — the validator's challenge on every 401 and
39//! 403 when OAuth is configured, otherwise the
40//! [`static_challenge`](HttpAuthLayerBuilder::static_challenge)
41//! ([`crate::DEFAULT_STATIC_CHALLENGE`] unless set). The refusal's body is
42//! `ResBody::default()` (empty, for the usual body types) unless
43//! [`on_reject`](HttpAuthLayerBuilder::on_reject) builds one; its status and
44//! challenge are set after that callback runs, so it cannot drop or contradict
45//! them. [`optional`](HttpAuthLayerBuilder::optional) passes a request that
46//! presents no credential through, exactly as the axum layer's does.
47//!
48//! **Fail-closed by construction.** [`HttpAuthLayerBuilder::build`] refuses to
49//! build without a static token or an OAuth validator
50//! ([`AuthLayerError::NoCredential`]); the only layer that lets every request
51//! through is [`HttpAuthLayer::allow_unauthenticated`], asked for by name (or a
52//! [`StaticTokenDecision::Unauthenticated`] handed to
53//! [`HttpAuthLayerBuilder::build_with_decision`]). Every configured source
54//! header is marked sensitive (`http::HeaderValue::set_sensitive`) on the
55//! request before the callback and the inner service see it (an
56//! `allow_unauthenticated` layer, which has no sources, marks `Authorization`),
57//! and the layer's
58//! `Debug` never prints a static token (the builder's single one shows as
59//! `<redacted>`, a [`StaticTokens`] set as its count and labels).
60//!
61//! # Per-route scopes
62//!
63//! [`HttpAuthLayerBuilder::require_scopes`] requires more scopes of every
64//! credential the layer accepts, and [`RequireScopes`] (placed behind either
65//! layer) of the routes it wraps, on top of the validator's own. Both layers
66//! mark every request they pass (a private marker holding the layer's
67//! challenges and refusal builder), so [`RequireScopes`] — and the `mcp`
68//! feature's `McpToolScopes` — refuse with that layer's own status,
69//! challenge and `on_reject` body, with a 403 challenge naming the scopes
70//! the request needed; without a layer in front they answer 500.
71//!
72//! # Logging
73//!
74//! The same outcomes at the same levels as the axum layer, with target
75//! `oauth_resource_server::http_layer`: an accepted OAuth token at `debug`
76//! (principal, subject, scopes — never the token); an accepted static token
77//! at `debug` (its label, never the token); a request with no credential
78//! at `debug` when OAuth is configured; a request with no credential passed
79//! through by an [`optional`](HttpAuthLayerBuilder::optional) layer at `debug`;
80//! any other refusal at `warn`, with the reason when OAuth is configured. The
81//! reason goes to the log only, never to the caller. A route-level scope
82//! refusal ([`RequireScopes`], `McpToolScopes`) is logged at `info` with the
83//! required and present scopes; a wiring no request can satisfy at `error`.
84//!
85//! Every one of these events (the route-level refusals included) also
86//! carries the stable, low-cardinality fields `auth.outcome` (`accepted`,
87//! `rejected`, `passed_through`), `auth.mechanism` (`static`, `oauth`,
88//! `none`) and, on a refusal, `auth.reason` (an `InvalidTokenKind` label,
89//! `missing`, `insufficient_scope` or `misconfigured`) and `auth.status`
90//! (401, 403 or 500); an accepted labeled static token adds
91//! `auth.static_label`. Unlike the message text, their names and values are
92//! covered by semver — the README's "Observability" section lists them all,
93//! with the spans and the `metrics` feature's counters.
94//!
95//! # Naming
96//!
97//! `HttpAuthLayer`, not `AuthLayer`: with the `axum` feature on, both layers
98//! are in scope in one application, and two `AuthLayer`s would read as the
99//! same type under two paths. The `Http` prefix names what it is generic over
100//! — `http::Request<B>` for any `B`. The module is `http_layer`, not `tower`
101//! (the feature is still `tower`): a crate-root module named `tower` would
102//! make `tower` ambiguous in a downstream module that glob-imports this
103//! crate's root and also uses the `tower` crate.
104//! [`CredentialSource`], [`RejectContext`] and [`AuthLayerError`] are shared by
105//! both layers and are also reachable under `oauth_resource_server::axum`.
106
107use std::future::Future;
108use std::pin::Pin;
109use std::sync::Arc;
110use std::task::{Context, Poll};
111
112use http::header::{AUTHORIZATION, WWW_AUTHENTICATE};
113use http::request::Parts;
114use http::{HeaderMap, HeaderName, HeaderValue, Request, Response, StatusCode};
115use tracing::{debug, error, info, warn};
116use zeroize::Zeroizing;
117
118use crate::authenticate::{
119    Credential, StaticTokenMatch, StaticTokens, authenticate_with_static_tokens,
120};
121use crate::config::is_scope_token;
122use crate::observe::{
123    self, Mechanism, Outcome, REASON_MISCONFIGURED, REASON_NONE, Stage, count_request,
124};
125use crate::policy::StaticTokenDecision;
126use crate::refusal::{BARE_INSUFFICIENT_SCOPE_CHALLENGE, DEFAULT_STATIC_CHALLENGE, select};
127use crate::token::{AuthorizedToken, TokenRejection, missing_scopes};
128use crate::validator::OAuthValidator;
129
130/// Where a request may carry a credential. Each configured source contributes
131/// at most one candidate — the header's FIRST value; a request that repeats the
132/// header has the later values ignored, not refused — and every candidate is
133/// checked independently (see [`crate::authenticate()`]): a bad credential in
134/// one source never masks a good one in another.
135///
136/// A header value that is not visible ASCII contributes no candidate. Every
137/// configured source header is marked sensitive
138/// (`http::HeaderValue::set_sensitive`) on the request, before the callback and
139/// the inner service see it, so `Debug` output and tracing layers print it as
140/// `Sensitive`.
141///
142/// Used by both layers: [`HttpAuthLayerBuilder::sources`] here, and the axum
143/// layer's `AuthLayerBuilder::sources` (also reachable as
144/// `oauth_resource_server::axum::CredentialSource`).
145#[derive(Debug, Clone, PartialEq, Eq)]
146#[non_exhaustive]
147pub enum CredentialSource {
148    /// `<header>: Bearer <token>` (RFC 6750 §2.1). The scheme is matched
149    /// case-insensitively (RFC 9110 §11.1): `bearer x` is the same credential as
150    /// `Bearer x`. The token is the rest of the value after the first space,
151    /// trimmed. Any other scheme contributes no candidate.
152    Bearer(HeaderName),
153    /// `<header>: <token>`: the whole value, verbatim — for an API-key header
154    /// such as `X-Api-Key`.
155    Raw(HeaderName),
156}
157
158impl CredentialSource {
159    /// `Authorization: Bearer <token>`, the default and only source unless the
160    /// layer's builder is given `sources`.
161    pub fn authorization_bearer() -> Self {
162        Self::Bearer(AUTHORIZATION)
163    }
164
165    /// This source's candidate in `headers`, if the header is present, valid
166    /// visible ASCII and (for [`CredentialSource::Bearer`]) uses the `Bearer`
167    /// scheme. May be blank; [`crate::authenticate()`] treats blank as absent.
168    pub(crate) fn candidate<'h>(&self, headers: &'h HeaderMap) -> Option<&'h str> {
169        headers.get(self.header_name()).and_then(|v| self.parse(v))
170    }
171
172    /// The candidate one value of this source's header carries: `None` when
173    /// the value is not visible ASCII, otherwise the (possibly blank) token.
174    fn parse<'h>(&self, value: &'h HeaderValue) -> Option<&'h str> {
175        let value = value.to_str().ok()?;
176        Some(match self {
177            Self::Bearer(_) => bearer_credential(value),
178            Self::Raw(_) => value,
179        })
180    }
181
182    /// Whether EVERY value of this source's header — not just the first, which
183    /// is the only one ever authenticated — is readable and blank. Used only by
184    /// an `optional()` layer, and deliberately stricter than the candidate
185    /// parsing ([`bearer_credential`], unchanged): an unreadable
186    /// (non-visible-ASCII) value, and for a [`CredentialSource::Bearer`] source
187    /// any value [`names_a_token`] says carries a token, count as presented.
188    pub(crate) fn presents_nothing(&self, headers: &HeaderMap) -> bool {
189        headers.get_all(self.header_name()).iter().all(|v| {
190            let Ok(value) = v.to_str() else {
191                return false;
192            };
193            match self {
194                Self::Raw(_) => value.trim().is_empty(),
195                Self::Bearer(_) => {
196                    !names_a_token(value) && bearer_credential(value).trim().is_empty()
197                }
198            }
199        })
200    }
201
202    /// The header this source reads.
203    pub(crate) fn header_name(&self) -> &HeaderName {
204        match self {
205            Self::Bearer(name) | Self::Raw(name) => name,
206        }
207    }
208}
209
210/// Whether a `Bearer`-source header value carries a token that an `optional()`
211/// layer must not read as "no credential", even though [`bearer_credential`]
212/// yields no candidate from it:
213///
214/// - any value whose auth-scheme is `DPoP` (case-insensitive) — a
215///   sender-constrained token this crate cannot accept (RFC 9449), which must
216///   be refused rather than served as anonymous;
217/// - a `Bearer` scheme separated from a non-blank rest by SP **or HTAB** (RFC
218///   9110 §11.4 allows only SP; the strict parsing is not widened, the value
219///   is only counted as presented).
220///
221/// Leading SP/HTAB is skipped. Every other scheme (`Basic`, …) still counts as
222/// no bearer credential.
223pub(crate) fn names_a_token(value: &str) -> bool {
224    let value = value.trim_start_matches([' ', '\t']);
225    let (scheme, rest) = value
226        .find([' ', '\t'])
227        .map_or((value, ""), |i| value.split_at(i));
228    scheme.eq_ignore_ascii_case("dpop")
229        || (scheme.eq_ignore_ascii_case("bearer") && !rest.trim().is_empty())
230}
231
232/// The credential from a `Bearer <token>` header value, or `""`.
233///
234/// The auth-scheme is matched case-insensitively (RFC 9110 §11.1, RFC 6750 §2.1
235/// examples notwithstanding) — `bearer x` is the same credential as `Bearer x`,
236/// and refusing it would be a spurious 401 for a client that lower-cases scheme
237/// names. The token itself is taken verbatim, minus surrounding spaces.
238pub(crate) fn bearer_credential(header: &str) -> &str {
239    match header.split_once(' ') {
240        Some((scheme, token)) if scheme.eq_ignore_ascii_case("bearer") => token.trim(),
241        _ => "",
242    }
243}
244
245/// What an `on_reject` callback ([`HttpAuthLayerBuilder::on_reject`], or the
246/// axum layer's `AuthLayerBuilder::on_reject`) is told about a refusal.
247///
248/// `#[non_exhaustive]`: read its fields; more may be added without a breaking
249/// change.
250///
251/// Its `Debug` prints the rejection, the status, the method, the URI's path
252/// (any query as `?***`: a client may send an RFC 6750 §2.3 `access_token`
253/// there) and version, and the request's header NAMES — never a header value,
254/// so `tracing::warn!(?cx, "refused")` cannot log the presented credential
255/// (which, for an insufficient-scope refusal, is a validly signed, unexpired
256/// token).
257#[non_exhaustive]
258pub struct RejectContext<'a> {
259    /// Why the request was refused. [`TokenRejection::Invalid`]'s reason is for
260    /// logs only — never put it in the response.
261    pub rejection: &'a TokenRejection,
262    /// The status the response will carry (401, or 403 for insufficient scope),
263    /// whatever the callback sets.
264    pub status: StatusCode,
265    /// The refused request's method, URI, version, headers and extensions — for
266    /// content negotiation (`Accept`), or a per-path error shape.
267    pub request: &'a Parts,
268}
269
270impl std::fmt::Debug for RejectContext<'_> {
271    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272        let header_names: Vec<&str> = self
273            .request
274            .headers
275            .keys()
276            .map(HeaderName::as_str)
277            .collect();
278        f.debug_struct("RejectContext")
279            .field("rejection", self.rejection)
280            .field("status", &self.status)
281            .field("method", &self.request.method)
282            // The path only: a query may carry a credential (RFC 6750 §2.3
283            // `access_token`), so it shows as `?***`, as `redact_url` would.
284            .field("uri", &redacted_request_uri(&self.request.uri))
285            .field("version", &self.request.version)
286            .field("header_names", &header_names)
287            .finish_non_exhaustive()
288    }
289}
290
291/// `uri`'s path, plus `?***` when it has a query: what a `Debug` of a request
292/// may show. The query can carry a bearer token (RFC 6750 §2.3).
293fn redacted_request_uri(uri: &http::Uri) -> String {
294    match uri.query() {
295        Some(_) => format!("{}?***", uri.path()),
296        None => uri.path().to_string(),
297    }
298}
299
300/// Why a layer's builder refused to build ([`HttpAuthLayerBuilder`], or the
301/// axum layer's `AuthLayerBuilder`, which reaches this type as
302/// `oauth_resource_server::axum::AuthLayerError`).
303#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
304#[non_exhaustive]
305pub enum AuthLayerError {
306    /// Neither a (non-blank) static token nor an OAuth validator was given (a
307    /// whitespace-only static token counts as none: it could never match). A
308    /// layer that could accept nothing would lock every route; one that
309    /// accepted everything must be asked for by name.
310    #[error(
311        "no credential is configured: give a static token and/or an OAuth validator \
312         (the layer's allow_unauthenticated constructor is the explicit opt-out)"
313    )]
314    NoCredential,
315    /// The builder's `sources` was given an empty list, so no request could
316    /// ever present a credential.
317    #[error("no credential source is configured: a request could never present a credential")]
318    NoSources,
319    /// `build_with_decision`: the decision was made with OAuth on, but no
320    /// OAuth validator was given.
321    #[error(
322        "the static-token decision was made with OAuth enabled, but no OAuth validator \
323         was given"
324    )]
325    DecisionNeedsOAuth,
326    /// `build_with_decision`: the decision was made with OAuth off, but an
327    /// OAuth validator was given.
328    #[error(
329        "the static-token decision was made with OAuth disabled, but an OAuth validator \
330         was given"
331    )]
332    DecisionWithoutOAuth,
333    /// The OAuth validator's challenge is not a valid HTTP header value, so
334    /// refusals could not carry `WWW-Authenticate`. [`crate::OAuthConfig::resolve`]
335    /// refuses every config that would cause this (a control or non-ASCII
336    /// character in `resource`, a scope that is not a scope-token); only a
337    /// hand-edited [`crate::ResolvedOAuthConfig`] reaches it.
338    #[error(
339        "the OAuth WWW-Authenticate challenge is not a valid HTTP header value — check \
340         the resource URL and scopes for control or non-ASCII characters"
341    )]
342    InvalidChallenge,
343    // Added last: a new variant before an existing one would renumber its
344    // implicit discriminant.
345    /// `build_with_decision`: the builder was given a non-empty
346    /// `static_tokens` set, but the decision carries no static token and does
347    /// not ignore one (`OAuthOnly` or `Unauthenticated`), so
348    /// [`crate::static_token_policy`] was never told a static token exists.
349    /// Pass the current token to the policy too; a decision that carries it
350    /// then accepts the whole set.
351    #[error(
352        "static tokens were given, but the static-token decision was made without a static \
353         token: pass the current static token to static_token_policy as well"
354    )]
355    DecisionWithoutStaticToken,
356    /// A `require_scopes` entry is not an RFC 6749 §3.3 scope-token (it is
357    /// empty, or holds a space, `"`, `\`, a control or non-ASCII
358    /// character). No token can carry such a scope, so every request would
359    /// be refused.
360    #[error(
361        "a required scope is not a valid scope (printable ASCII with no space, '\"' or '\\', \
362         RFC 6749 §3.3)"
363    )]
364    InvalidScope,
365    /// `require_scopes` was given without an OAuth validator and without
366    /// `static_token_bypasses_scopes`: a static token carries no scopes, so
367    /// no request could ever pass.
368    #[error(
369        "required scopes were given, but there is no OAuth validator and static tokens do not \
370         bypass scopes: no request could ever pass"
371    )]
372    ScopesNeedOAuth,
373    /// `build_with_decision` with `StaticTokenDecision::Unauthenticated` on a
374    /// builder given `require_scopes`: that decision builds the
375    /// `allow_unauthenticated` pass-through, which checks nothing, so the
376    /// scopes would be silently dropped.
377    #[error(
378        "required scopes were given, but the static-token decision allows unauthenticated \
379         requests, which checks no scope"
380    )]
381    ScopesWithoutAuthentication,
382}
383
384/// The enforcing part of a layer, shared by [`HttpAuthLayer`] and the axum
385/// `AuthLayer`: which credentials are accepted, where they are read from, and
386/// the pre-rendered challenges. Built only by [`Gate::build`], so the
387/// fail-closed checks cannot be skipped by either layer.
388pub(crate) struct Gate {
389    /// Every accepted static token: the builder's `static_token` and
390    /// `static_tokens`, merged ([`StaticTokens::merged`]); `None` when neither
391    /// holds a (non-empty) token.
392    pub(crate) static_tokens: Option<StaticTokens>,
393    pub(crate) oauth: Option<Arc<OAuthValidator>>,
394    pub(crate) sources: Vec<CredentialSource>,
395    /// The validator's pre-rendered challenges, `(invalid_token,
396    /// insufficient_scope)`; `Some` exactly when OAuth is configured (a
397    /// challenge that is not a valid header value fails the build instead).
398    oauth_challenges: Option<(HeaderValue, HeaderValue)>,
399    /// The challenge for a 401 when OAuth is off; `None` when the application
400    /// opted out with `static_challenge(None)`.
401    pub(crate) static_challenge: Option<HeaderValue>,
402    /// `optional()`: pass a request that presents no credential through
403    /// instead of refusing it.
404    pub(crate) optional: bool,
405    /// The builder's `require_scopes`, deduplicated: checked on every
406    /// accepted credential, on top of the validator's own required scopes.
407    pub(crate) required_scopes: Vec<String>,
408    /// The builder's `static_token_bypasses_scopes`: a static token passes
409    /// `required_scopes` instead of being refused with 403.
410    pub(crate) static_bypasses_scopes: bool,
411    /// Every scope an OAuth token must carry to pass this layer: the
412    /// validator's `required_scopes` followed by `required_scopes`. Every
413    /// 403 challenge this gate sends names these (plus a route-level
414    /// requirement's own), so a client that re-authorizes for exactly that
415    /// set passes.
416    scope_floor: Vec<String>,
417}
418
419/// What [`Gate::admit`] decided about a request.
420pub(crate) enum Admission {
421    /// A static token was accepted (and inserted, with its
422    /// [`StaticTokenMatch`]).
423    Static,
424    /// An OAuth token was accepted (and inserted); returned for the caller's
425    /// log line.
426    OAuth(AuthorizedToken),
427    /// `optional()`: no credential was presented; nothing was inserted.
428    PassedThrough,
429    /// Refused, by the mechanism named (the `auth.mechanism` log field).
430    Refused(TokenRejection, Mechanism),
431}
432
433/// The `debug` line (and `metrics` count) for an accepted static token,
434/// with the matched entry's log-safe label as `auth.static_label` (absent
435/// for an unlabeled entry). Logged under the calling layer's module target.
436macro_rules! log_static_accepted {
437    ($parts:expr) => {{
438        let parts: &http::request::Parts = $parts;
439        crate::observe::count_request(
440            crate::observe::Stage::Layer,
441            crate::observe::Outcome::Accepted,
442            crate::observe::Mechanism::Static,
443            crate::observe::REASON_NONE,
444        );
445        match parts
446            .extensions
447            .get::<crate::authenticate::StaticTokenMatch>()
448            .and_then(crate::authenticate::StaticTokenMatch::label)
449        {
450            Some(label) => tracing::debug!(
451                path = %parts.uri.path(),
452                auth.outcome = crate::observe::Outcome::Accepted.as_str(),
453                auth.mechanism = crate::observe::Mechanism::Static.as_str(),
454                auth.static_label = label,
455                "Static bearer auth accepted"
456            ),
457            None => tracing::debug!(
458                path = %parts.uri.path(),
459                auth.outcome = crate::observe::Outcome::Accepted.as_str(),
460                auth.mechanism = crate::observe::Mechanism::Static.as_str(),
461                "Static bearer auth accepted"
462            ),
463        }
464    }};
465}
466pub(crate) use log_static_accepted;
467
468/// The `debug` line (and `metrics` count) for an accepted OAuth token:
469/// principal, subject (both through `for_log`) and scopes — never the token.
470macro_rules! log_oauth_accepted {
471    ($parts:expr, $token:expr) => {{
472        let parts: &http::request::Parts = $parts;
473        let token: &crate::token::AuthorizedToken = $token;
474        crate::observe::count_request(
475            crate::observe::Stage::Layer,
476            crate::observe::Outcome::Accepted,
477            crate::observe::Mechanism::OAuth,
478            crate::observe::REASON_NONE,
479        );
480        tracing::debug!(
481            path = %parts.uri.path(),
482            principal = ?token.principal.as_deref().map(crate::token::for_log),
483            subject = ?token.subject.as_deref().map(crate::token::for_log),
484            scopes = ?crate::token::scopes_for_log(&token.scopes),
485            auth.outcome = crate::observe::Outcome::Accepted.as_str(),
486            auth.mechanism = crate::observe::Mechanism::OAuth.as_str(),
487            "OAuth bearer auth accepted"
488        );
489    }};
490}
491pub(crate) use log_oauth_accepted;
492
493/// The `debug` line (and `metrics` count) for an `optional()` layer's
494/// pass-through of a request with no credential.
495macro_rules! log_passed_through {
496    ($parts:expr) => {{
497        let parts: &http::request::Parts = $parts;
498        crate::observe::count_request(
499            crate::observe::Stage::Layer,
500            crate::observe::Outcome::PassedThrough,
501            crate::observe::Mechanism::None,
502            crate::observe::REASON_NONE,
503        );
504        tracing::debug!(
505            path = %parts.uri.path(),
506            auth.outcome = crate::observe::Outcome::PassedThrough.as_str(),
507            auth.mechanism = crate::observe::Mechanism::None.as_str(),
508            "No credential presented; optional auth passes the request through"
509        );
510    }};
511}
512pub(crate) use log_passed_through;
513
514/// A layer's own refusal, logged at the level the module docs give (and
515/// counted with `metrics`): `debug` for no credential when OAuth is
516/// configured — every OAuth client's first request (401 → read
517/// `resource_metadata` → authorize) looks like that —, `warn` otherwise.
518/// A macro so the event's target stays the calling layer's module.
519macro_rules! log_layer_refusal {
520    ($gate:expr, $parts:expr, $rejection:expr, $mechanism:expr, $stage:expr) => {{
521        let gate: &crate::http_layer::Gate = $gate;
522        let parts: &http::request::Parts = $parts;
523        let rejection: &crate::token::TokenRejection = $rejection;
524        let mechanism: crate::observe::Mechanism = $mechanism;
525        let path = parts.uri.path();
526        let reason = crate::observe::reason(rejection);
527        let status = crate::observe::status(rejection);
528        crate::observe::count_request($stage, crate::observe::Outcome::Rejected, mechanism, reason);
529        match (&gate.oauth, rejection) {
530            (None, _) => tracing::warn!(
531                path = %path,
532                auth.outcome = crate::observe::Outcome::Rejected.as_str(),
533                auth.mechanism = mechanism.as_str(),
534                auth.reason = reason,
535                auth.status = status,
536                "Bearer auth rejected"
537            ),
538            (Some(_), crate::token::TokenRejection::Missing) => {
539                tracing::debug!(
540                    path = %path,
541                    auth.outcome = crate::observe::Outcome::Rejected.as_str(),
542                    auth.mechanism = mechanism.as_str(),
543                    auth.reason = reason,
544                    auth.status = status,
545                    "No bearer credential presented"
546                );
547            }
548            (Some(_), _) => {
549                tracing::warn!(
550                    path = %path,
551                    reason = ?rejection,
552                    auth.outcome = crate::observe::Outcome::Rejected.as_str(),
553                    auth.mechanism = mechanism.as_str(),
554                    auth.reason = reason,
555                    auth.status = status,
556                    "OAuth bearer auth rejected"
557                );
558            }
559        }
560    }};
561}
562pub(crate) use log_layer_refusal;
563
564impl Gate {
565    /// The fail-closed build both layers' builders end in.
566    ///
567    /// `static_token` and `static_tokens` are merged into one set (a repeated
568    /// secret counts once); an empty string and an empty set both count as
569    /// no static token, so neither satisfies the fail-closed check.
570    #[allow(clippy::too_many_arguments)] // one per builder setting
571    pub(crate) fn build(
572        static_token: Option<Zeroizing<String>>,
573        static_tokens: Option<StaticTokens>,
574        oauth: Option<Arc<OAuthValidator>>,
575        sources: Option<Vec<CredentialSource>>,
576        static_challenge: Option<Option<HeaderValue>>,
577        optional: bool,
578        required_scopes: Vec<String>,
579        static_bypasses_scopes: bool,
580    ) -> Result<Self, AuthLayerError> {
581        let static_tokens = StaticTokens::merged(static_tokens, static_token);
582        if static_tokens.is_none() && oauth.is_none() {
583            return Err(AuthLayerError::NoCredential);
584        }
585        let sources = sources.unwrap_or_else(|| vec![CredentialSource::authorization_bearer()]);
586        if sources.is_empty() {
587            return Err(AuthLayerError::NoSources);
588        }
589        let required_scopes =
590            checked_scopes(required_scopes).map_err(|_| AuthLayerError::InvalidScope)?;
591        // Fail closed: only a static token could reach these routes, and a
592        // static token has no scopes, so nothing ever would.
593        if !required_scopes.is_empty() && oauth.is_none() && !static_bypasses_scopes {
594            return Err(AuthLayerError::ScopesNeedOAuth);
595        }
596        // Fail closed here too: an OAuth layer whose 401s could not carry
597        // `resource_metadata` would leave hosted clients unable to start the
598        // authorization flow, which is worse than refusing to start.
599        let header = |challenge: String| {
600            HeaderValue::from_str(&challenge).map_err(|_| AuthLayerError::InvalidChallenge)
601        };
602        // A validator that fell back to its safe challenges was built from a
603        // config whose challenges are not header values: exactly the configs
604        // this check refused before the fallback existed, so it still does.
605        if oauth.as_ref().is_some_and(|v| v.challenge_fell_back()) {
606            return Err(AuthLayerError::InvalidChallenge);
607        }
608        let required: Vec<&str> = required_scopes.iter().map(String::as_str).collect();
609        let scope_floor: Vec<String> = match &oauth {
610            Some(v) => v
611                .scopes_with_floor(&required)
612                .into_iter()
613                .map(str::to_owned)
614                .collect(),
615            None => required_scopes.clone(),
616        };
617        let oauth_challenges = match &oauth {
618            // With scopes of its own, every 403 this layer sends names them
619            // after the validator's (`insufficient_scope_challenge()` is the
620            // same call with the validator's alone).
621            Some(v) => Some((
622                header(v.invalid_token_challenge())?,
623                header(if required_scopes.is_empty() {
624                    v.insufficient_scope_challenge()
625                } else {
626                    let floor: Vec<&str> = scope_floor.iter().map(String::as_str).collect();
627                    v.insufficient_scope_challenge_for(&floor, None)
628                })?,
629            )),
630            None => None,
631        };
632        let static_challenge = static_challenge
633            .unwrap_or_else(|| Some(HeaderValue::from_static(DEFAULT_STATIC_CHALLENGE)));
634        Ok(Self {
635            static_tokens,
636            oauth,
637            sources,
638            oauth_challenges,
639            static_challenge,
640            optional,
641            required_scopes,
642            static_bypasses_scopes,
643            scope_floor,
644        })
645    }
646
647    /// The agreement `build_with_decision` requires between a decision and
648    /// whether a validator was given.
649    pub(crate) fn check_decision(
650        decision: &StaticTokenDecision,
651        has_oauth: bool,
652    ) -> Result<(), AuthLayerError> {
653        match (decision.oauth_enabled(), has_oauth) {
654            (true, false) => Err(AuthLayerError::DecisionNeedsOAuth),
655            (false, true) => Err(AuthLayerError::DecisionWithoutOAuth),
656            _ => Ok(()),
657        }
658    }
659
660    /// The static tokens `build_with_decision` builds with: the decision's
661    /// token (it replaces the builder's `static_token`, as it always has),
662    /// and the builder's `static_tokens` set according to the decision.
663    ///
664    /// - `StaticOnly`/`StaticAndOAuth`: the set is kept, and merged with the
665    ///   decision's token by [`Gate::build`].
666    /// - `StaticIgnored` (`accept_static_bearer: false`): the set is dropped
667    ///   with the token — the setting wins over every static token.
668    /// - `OAuthOnly`/`Unauthenticated` with a non-empty set:
669    ///   [`AuthLayerError::DecisionWithoutStaticToken`]. The policy decided
670    ///   without being told a static token exists, so the decision cannot
671    ///   speak for the set: honouring it would silently drop configured keys
672    ///   (or, for `Unauthenticated`, open the routes despite them).
673    ///
674    /// Call after [`Gate::check_decision`], so its errors come first.
675    pub(crate) fn decision_tokens(
676        decision: StaticTokenDecision,
677        static_tokens: Option<StaticTokens>,
678    ) -> Result<(Option<Zeroizing<String>>, Option<StaticTokens>), AuthLayerError> {
679        let static_tokens = static_tokens.filter(|s| !s.is_empty());
680        match decision {
681            StaticTokenDecision::StaticOnly(t) | StaticTokenDecision::StaticAndOAuth(t) => {
682                Ok((Some(Zeroizing::new(t)), static_tokens))
683            }
684            StaticTokenDecision::StaticIgnored => Ok((None, None)),
685            _ if static_tokens.is_some() => Err(AuthLayerError::DecisionWithoutStaticToken),
686            _ => Ok((None, None)),
687        }
688    }
689
690    /// Authenticate the request whose `parts` these are: mark the source
691    /// headers sensitive, run [`authenticate`] over one candidate per source,
692    /// and insert what was accepted into the extensions. Logs nothing; each
693    /// layer logs the outcome under its own target.
694    pub(crate) async fn admit(&self, parts: &mut Parts) -> Admission {
695        // An optional layer's pass-through must mean "this layer accepted
696        // nothing": drop whatever an outer layer inserted, so `Option<..>`
697        // never hands the handler a credential this layer did not validate.
698        // Strict layers keep accumulating, as they always have.
699        if self.optional {
700            parts.extensions.remove::<Credential>();
701            parts.extensions.remove::<AuthorizedToken>();
702            parts.extensions.remove::<StaticTokenMatch>();
703        }
704        // The credential must not reach a `Debug` of the request — ours
705        // (`RejectContext`), a tracing layer's, or a handler's.
706        for (name, value) in parts.headers.iter_mut() {
707            if self.sources.iter().any(|s| s.header_name() == name) {
708                value.set_sensitive(true);
709            }
710        }
711        let result = {
712            let headers = &parts.headers;
713            let candidates = self.sources.iter().filter_map(|s| s.candidate(headers));
714            authenticate_with_static_tokens(
715                candidates,
716                self.static_tokens.as_ref(),
717                self.oauth.as_deref(),
718            )
719            .await
720        };
721        match result {
722            // The layer's own `require_scopes`, checked before anything is
723            // inserted: a static token has no scopes, so it is refused unless
724            // the layer opted in; an OAuth token needs every one (the same
725            // matching as the validator's own scope check).
726            Ok((Credential::StaticToken, _))
727                if !self.required_scopes.is_empty() && !self.static_bypasses_scopes =>
728            {
729                Admission::Refused(TokenRejection::InsufficientScope, Mechanism::Static)
730            }
731            Ok((Credential::OAuth(token), _))
732                if !missing_scopes(
733                    &token.scopes,
734                    self.required_scopes.iter().map(String::as_str),
735                )
736                .is_empty() =>
737            {
738                Admission::Refused(TokenRejection::InsufficientScope, Mechanism::OAuth)
739            }
740            Ok((Credential::StaticToken, matched)) => {
741                parts.extensions.insert(Credential::StaticToken);
742                // Always `Some` for a static match; the fallback keeps the
743                // pairing (a `StaticTokenMatch` whenever `StaticToken`) even so.
744                parts
745                    .extensions
746                    .insert(matched.unwrap_or_else(StaticTokenMatch::unlabeled));
747                Admission::Static
748            }
749            Ok((Credential::OAuth(token), _)) => {
750                // `StaticTokenMatch` describes the innermost accepted
751                // `Credential`, so an outer layer's must not outlive it.
752                parts.extensions.remove::<StaticTokenMatch>();
753                parts.extensions.insert(token.clone());
754                parts.extensions.insert(Credential::OAuth(token.clone()));
755                Admission::OAuth(token)
756            }
757            // `optional()`: pass through only what `authenticate` found no
758            // credential in AND where no source header holds anything at all
759            // beyond blanks — an unreadable value, or a non-blank later value
760            // of a repeated header, is refused like any other `Missing`. An
761            // invalid or insufficient credential never reaches this arm:
762            // `authenticate` reports `Missing` only with no non-blank candidate.
763            Err(TokenRejection::Missing)
764                if self.optional
765                    && self
766                        .sources
767                        .iter()
768                        .all(|s| s.presents_nothing(&parts.headers)) =>
769            {
770                Admission::PassedThrough
771            }
772            Err(rejection) => {
773                let mechanism = Mechanism::of_rejection(&rejection);
774                Admission::Refused(rejection, mechanism)
775            }
776        }
777    }
778
779    /// The status and challenge for `rejection`: [`crate::refusal()`]'s
780    /// decision ([`select`]), over this layer's pre-validated header values.
781    pub(crate) fn status_and_challenge(
782        &self,
783        rejection: &TokenRejection,
784    ) -> (StatusCode, Option<&HeaderValue>) {
785        self.status_and_challenge_with(rejection, None)
786    }
787
788    /// [`Gate::status_and_challenge`], with `insufficient` (a per-request
789    /// 403 challenge from [`Gate::scope_challenge`]) in place of the layer's
790    /// own `insufficient_scope` challenge when it is `Some`. The same
791    /// [`select`], so neither the status nor which challenge can differ from
792    /// the layer's own refusals — except that without OAuth a per-request 403
793    /// carries `insufficient` (`Bearer error="insufficient_scope"`, RFC 6750
794    /// §3.1) rather than the static 401 challenge `select` would pick, which
795    /// names the wrong error. A layer's own refusals never pass one.
796    pub(crate) fn status_and_challenge_with<'a>(
797        &'a self,
798        rejection: &TokenRejection,
799        insufficient: Option<&'a HeaderValue>,
800    ) -> (StatusCode, Option<&'a HeaderValue>) {
801        let (status, challenge) = select(
802            rejection,
803            self.oauth_challenges
804                .as_ref()
805                .map(|(i, s)| (i, insufficient.unwrap_or(s))),
806            self.static_challenge.as_ref(),
807        );
808        let challenge = match (rejection, &self.oauth_challenges, insufficient) {
809            (TokenRejection::InsufficientScope, None, Some(bare)) => Some(bare),
810            _ => challenge,
811        };
812        // `select` only ever yields 401 or 403, both valid.
813        let status = StatusCode::from_u16(status).unwrap_or(StatusCode::UNAUTHORIZED);
814        (status, challenge)
815    }
816
817    /// The 403 challenge for a request that needs `extra` on top of this
818    /// layer's scopes: the validator's
819    /// [`insufficient_scope_challenge_for`](OAuthValidator::insufficient_scope_challenge_for)
820    /// over `scope_floor` followed by `extra` — for the same scopes, exactly
821    /// what [`crate::refusal_for_scopes`] sends. Without OAuth, the bare
822    /// `Bearer error="insufficient_scope"` (there is no `resource_metadata`
823    /// to point at, and no scope a static token could step up to).
824    pub(crate) fn scope_challenge(&self, extra: &[String]) -> Option<HeaderValue> {
825        let Some(validator) = self.oauth.as_ref() else {
826            return Some(HeaderValue::from_static(BARE_INSUFFICIENT_SCOPE_CHALLENGE));
827        };
828        let mut all: Vec<&str> = self.scope_floor.iter().map(String::as_str).collect();
829        for scope in extra {
830            if !all.contains(&scope.as_str()) {
831                all.push(scope);
832            }
833        }
834        // Always a header value (the validator guarantees it); `None` would
835        // only fall back to the layer's own 403 challenge.
836        HeaderValue::from_str(&validator.insufficient_scope_challenge_for(&all, None)).ok()
837    }
838
839    /// Fix a refusal's status and `WWW-Authenticate` on `response`, whatever an
840    /// `on_reject` callback put there. With a challenge, `insert` replaces every
841    /// `WWW-Authenticate` value the callback set; with none (no OAuth and
842    /// `static_challenge(None)`), the callback's headers are left as they are.
843    pub(crate) fn finish<B>(
844        &self,
845        rejection: &TokenRejection,
846        response: Response<B>,
847    ) -> Response<B> {
848        self.finish_with(rejection, None, response)
849    }
850
851    /// [`Gate::finish`] with a per-request 403 challenge; see
852    /// [`Gate::status_and_challenge_with`].
853    pub(crate) fn finish_with<B>(
854        &self,
855        rejection: &TokenRejection,
856        insufficient: Option<&HeaderValue>,
857        mut response: Response<B>,
858    ) -> Response<B> {
859        let (status, challenge) = self.status_and_challenge_with(rejection, insufficient);
860        *response.status_mut() = status;
861        if let Some(value) = challenge {
862            response
863                .headers_mut()
864                .insert(WWW_AUTHENTICATE, value.clone());
865        }
866        response
867    }
868}
869
870/// Builds the response for a refusal (its body and any extra headers); see
871/// [`HttpAuthLayerBuilder::on_reject`]. Implemented for [`EmptyRefusal`] (the
872/// default) and for every `Fn(RejectContext<'_>) -> http::Response<B>`.
873///
874/// Sealed: it can be named in bounds, but not implemented outside this crate,
875/// since only [`HttpAuthLayerBuilder::on_reject`] (which takes a closure) can
876/// install a refusal builder.
877pub trait RefusalResponse<B>: sealed::Sealed<B> {
878    /// The response for this refusal, before the layer sets its status and
879    /// `WWW-Authenticate` challenge.
880    fn refusal_response(&self, cx: RejectContext<'_>) -> Response<B>;
881}
882
883/// The default refusal: `B::default()` as the body (empty for the usual body
884/// types), no extra headers.
885#[derive(Debug, Clone, Copy, Default)]
886pub struct EmptyRefusal;
887
888mod sealed {
889    /// The private supertrait that seals [`super::RefusalResponse`].
890    pub trait Sealed<B> {}
891
892    impl<B: Default> Sealed<B> for super::EmptyRefusal {}
893
894    impl<B, F> Sealed<B> for F where F: Fn(super::RejectContext<'_>) -> http::Response<B> {}
895}
896
897impl<B: Default> RefusalResponse<B> for EmptyRefusal {
898    fn refusal_response(&self, _cx: RejectContext<'_>) -> Response<B> {
899        Response::new(B::default())
900    }
901}
902
903impl<B, F> RefusalResponse<B> for F
904where
905    F: Fn(RejectContext<'_>) -> Response<B>,
906{
907    fn refusal_response(&self, cx: RejectContext<'_>) -> Response<B> {
908        self(cx)
909    }
910}
911
912/// A scope given to a route-level requirement ([`RequireScopes::try_new`],
913/// the `mcp` feature's `McpToolScopes::try_default`/`try_tool`) that is not
914/// an RFC 6749 §3.3 scope-token: empty, or holding a space, `"`, `\`, a
915/// control or non-ASCII character. No token can carry such a scope, so the
916/// requirement could never be met.
917///
918/// Scopes are configuration, not secrets: `Display` and
919/// [`scope`](Self::scope) name the offending value.
920///
921/// `#[non_exhaustive]`: read it through its accessor.
922#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
923#[error(
924    "{scope:?} is not a valid scope (printable ASCII with no space, '\"' or '\\', RFC 6749 §3.3)"
925)]
926#[non_exhaustive]
927pub struct InvalidScope {
928    scope: String,
929}
930
931impl InvalidScope {
932    /// The scope that was refused, verbatim.
933    pub fn scope(&self) -> &str {
934        &self.scope
935    }
936}
937
938/// `scopes` deduplicated in order, or the first entry that is not an RFC 6749
939/// §3.3 scope-token (which no token can carry).
940pub(crate) fn checked_scopes(
941    scopes: impl IntoIterator<Item = impl Into<String>>,
942) -> Result<Vec<String>, InvalidScope> {
943    let mut out: Vec<String> = Vec::new();
944    for scope in scopes {
945        let scope = scope.into();
946        if !is_scope_token(&scope) {
947            return Err(InvalidScope { scope });
948        }
949        if !out.contains(&scope) {
950            out.push(scope);
951        }
952    }
953    Ok(out)
954}
955
956/// Whether every entry of `scopes` is an RFC 6749 §3.3 scope-token, in a
957/// `const` context: `axum::Scoped` checks its `ScopeSet` with it at compile
958/// time. The same rule as `config::is_scope_token`.
959#[cfg(feature = "axum")]
960pub(crate) const fn all_scope_tokens(scopes: &[&str]) -> bool {
961    let mut i = 0;
962    while i < scopes.len() {
963        let bytes = scopes[i].as_bytes();
964        if bytes.is_empty() {
965            return false;
966        }
967        let mut j = 0;
968        while j < bytes.len() {
969            let b = bytes[j];
970            if !(b == 0x21 || (b >= 0x23 && b <= 0x5B) || (b >= 0x5D && b <= 0x7E)) {
971                return false;
972            }
973            j += 1;
974        }
975        i += 1;
976    }
977    true
978}
979
980/// Mark every `Authorization` value sensitive: what an
981/// `allow_unauthenticated` layer (which has no sources of its own) does, so a
982/// credential a client sends anyway never reaches a `Debug` of the request
983/// unmarked — a tracing layer's, or a handler's.
984pub(crate) fn mark_authorization_sensitive(headers: &mut http::HeaderMap) {
985    for (name, value) in headers.iter_mut() {
986        if name == http::header::AUTHORIZATION {
987            value.set_sensitive(true);
988        }
989    }
990}
991
992/// Inserted into a request's extensions by every layer it passes — the axum
993/// `AuthLayer` and [`HttpAuthLayer`] alike — so a route-level scope check
994/// behind either ([`RequireScopes`], the `mcp` feature's `McpToolScopes`)
995/// can refuse with that layer's own status and challenge, and can tell "a
996/// layer ran" from "no layer ran" (a server misconfiguration: 500). `None`:
997/// an `allow_unauthenticated` layer. The type is private, so nothing outside
998/// this crate can insert, read or forge it.
999#[derive(Clone)]
1000pub(crate) struct GateRan(pub(crate) Option<Arc<Gate>>);
1001
1002/// Builds the body of a route-level refusal the way the layer that ran
1003/// builds its own (`on_reject`), for the response body type `B` the layer
1004/// was built for. Inserted next to [`GateRan`]; a route-level check whose
1005/// body type differs finds none and sends `B::default()` instead.
1006pub(crate) struct RefusalBody<B> {
1007    /// The layer's refusal builder, type-erased (the axum layer, or an
1008    /// [`HttpAuthLayer`]'s `R`).
1009    pub(crate) source: Arc<dyn std::any::Any + Send + Sync>,
1010    /// Downcasts `source` and builds the response; `None` when it has no
1011    /// callback of its own.
1012    pub(crate) build:
1013        fn(&(dyn std::any::Any + Send + Sync), RejectContext<'_>) -> Option<Response<B>>,
1014}
1015
1016impl<B> Clone for RefusalBody<B> {
1017    fn clone(&self) -> Self {
1018        Self {
1019            source: Arc::clone(&self.source),
1020            build: self.build,
1021        }
1022    }
1023}
1024
1025/// [`RefusalBody::build`] for an [`HttpAuthLayer<R>`].
1026fn http_refusal_body<R, B>(
1027    source: &(dyn std::any::Any + Send + Sync),
1028    cx: RejectContext<'_>,
1029) -> Option<Response<B>>
1030where
1031    R: RefusalResponse<B> + 'static,
1032{
1033    source.downcast_ref::<R>().map(|r| r.refusal_response(cx))
1034}
1035
1036/// What a route-level scope requirement decides about a request that passed
1037/// a layer.
1038pub(crate) enum ScopeVerdict {
1039    /// Serve it.
1040    Pass,
1041    /// Refuse it: `Missing` (no credential, under an optional or
1042    /// `allow_unauthenticated` layer) or `InsufficientScope`.
1043    Refuse(TokenRejection),
1044    /// No layer ran: a server misconfiguration, refused with 500.
1045    NoLayer,
1046}
1047
1048/// One per-request claim requirement (the `mcp` feature's `McpToolScopes`
1049/// builds them; oauth-resource-server#53): the verified top-level claim
1050/// `claim` must hold at least one of `any_of`, per
1051/// [`AuthorizedToken::has_claim_value`] (a string equal to it, or an array
1052/// with that string element; exact, case-sensitive). Clauses are all-of;
1053/// values within one are any-of. Two clauses naming the same claim are never
1054/// merged — that would widen access. Its `Debug` holds configured values: it
1055/// is never logged (a refusal logs the claim names only).
1056#[derive(Clone, Debug, PartialEq, Eq)]
1057pub(crate) struct ClaimClause {
1058    /// The top-level claim name, matched literally (a dot is part of it).
1059    pub(crate) claim: String,
1060    /// The accepted values, non-blank and deduplicated.
1061    pub(crate) any_of: Vec<String>,
1062}
1063
1064impl ClaimClause {
1065    /// Whether `token` holds at least one of the accepted values.
1066    pub(crate) fn satisfied_by(&self, token: &AuthorizedToken) -> bool {
1067        self.any_of
1068            .iter()
1069            .any(|value| token.has_claim_value(&self.claim, value))
1070    }
1071}
1072
1073/// Judge `parts` against a route-level requirement of `required` (every
1074/// scope, all-of) and `claims` (every clause, all-of). Reads the innermost
1075/// [`Credential`] a layer accepted — not an [`AuthorizedToken`], which an
1076/// outer layer may have inserted: an OAuth token must carry every scope (the
1077/// same matching as the validator's) and satisfy every claim clause; a static
1078/// token has neither scopes nor claims, so it passes only with
1079/// `static_bypasses`; no credential is `Missing`. A failed claim clause is
1080/// `InsufficientScope` too (RFC 6750 has no claim error). An empty
1081/// requirement (no scope and no clause) passes whatever the layer let
1082/// through. Without a layer marker it is always `NoLayer`, whatever the
1083/// requirement: that wiring mistake must surface, never pass.
1084pub(crate) fn judge_scopes(
1085    parts: &Parts,
1086    required: &[String],
1087    claims: &[ClaimClause],
1088    static_bypasses: bool,
1089) -> ScopeVerdict {
1090    if parts.extensions.get::<GateRan>().is_none() {
1091        return ScopeVerdict::NoLayer;
1092    }
1093    if required.is_empty() && claims.is_empty() {
1094        return ScopeVerdict::Pass;
1095    }
1096    match parts.extensions.get::<Credential>() {
1097        Some(Credential::OAuth(token)) => {
1098            if missing_scopes(&token.scopes, required.iter().map(String::as_str)).is_empty()
1099                && claims.iter().all(|clause| clause.satisfied_by(token))
1100            {
1101                ScopeVerdict::Pass
1102            } else {
1103                ScopeVerdict::Refuse(TokenRejection::InsufficientScope)
1104            }
1105        }
1106        Some(Credential::StaticToken) if static_bypasses => ScopeVerdict::Pass,
1107        Some(_) => ScopeVerdict::Refuse(TokenRejection::InsufficientScope),
1108        None => ScopeVerdict::Refuse(TokenRejection::Missing),
1109    }
1110}
1111
1112/// The response for a route-level scope refusal (`verdict` from
1113/// [`judge_scopes`]), built exactly as the layer that ran builds its own:
1114/// its `on_reject` body (when the body type matches, else `B::default()`),
1115/// then [`Gate::finish_with`] — the layer's status and challenge, with a 403
1116/// naming `required` on top of the layer's scopes
1117/// ([`Gate::scope_challenge`]). Logs every refusal (target
1118/// `oauth_resource_server::http_layer`); a wiring no request can ever
1119/// satisfy is logged at `error`:
1120///
1121/// - no layer at all: 500, empty body;
1122/// - an `allow_unauthenticated` layer: 401 with
1123///   [`DEFAULT_STATIC_CHALLENGE`], as the axum extractors answer there;
1124/// - an OAuth-less layer asked for scopes: its own 403.
1125pub(crate) fn scope_refusal<B: Default + 'static>(
1126    parts: &Parts,
1127    rejection: &TokenRejection,
1128    required: &[String],
1129    what: &'static str,
1130) -> Response<B> {
1131    scope_refusal_with_claims(parts, rejection, required, &[], what)
1132}
1133
1134/// [`scope_refusal`] for a requirement that also had claim clauses
1135/// (`McpToolScopes`). The response is byte-identical to the scope-only one
1136/// for the same `required` scopes: the 403 challenge names scopes only, and
1137/// no claim name or value ever enters `WWW-Authenticate`. With clauses, the
1138/// refusal log line adds a `required_claims` field holding the claim NAMES
1139/// only, never a configured or presented value.
1140pub(crate) fn scope_refusal_with_claims<B: Default + 'static>(
1141    parts: &Parts,
1142    rejection: &TokenRejection,
1143    required: &[String],
1144    claims: &[ClaimClause],
1145    what: &'static str,
1146) -> Response<B> {
1147    let claim_names: Vec<&str> = claims.iter().map(|c| c.claim.as_str()).collect();
1148    let path = parts.uri.path();
1149    let mechanism = Mechanism::of_request(parts.extensions.get::<Credential>(), rejection);
1150    // `Scoped` and the axum extractors are the handler-side callers; the
1151    // others are route layers.
1152    let stage = if matches!(
1153        what,
1154        "Scoped" | "AuthorizedToken" | "Credential" | "StaticTokenMatch"
1155    ) {
1156        Stage::Handler
1157    } else {
1158        Stage::Route
1159    };
1160    let Some(GateRan(gate)) = parts.extensions.get::<GateRan>() else {
1161        error!(
1162            path = %path,
1163            what,
1164            auth.outcome = Outcome::Rejected.as_str(),
1165            auth.mechanism = mechanism.as_str(),
1166            auth.reason = REASON_MISCONFIGURED,
1167            auth.status = 500u16,
1168            "Server misconfiguration: a scope requirement ran on a route no authentication \
1169             layer covers; refusing the request"
1170        );
1171        count_request(stage, Outcome::Rejected, mechanism, REASON_MISCONFIGURED);
1172        let mut response = Response::new(B::default());
1173        *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
1174        return response;
1175    };
1176    let Some(gate) = gate else {
1177        error!(
1178            path = %path,
1179            what,
1180            auth.outcome = Outcome::Rejected.as_str(),
1181            auth.mechanism = mechanism.as_str(),
1182            auth.reason = REASON_MISCONFIGURED,
1183            auth.status = 401u16,
1184            "Server misconfiguration: a scope requirement needs a credential, but its \
1185             authentication layer allows unauthenticated requests; refusing the request"
1186        );
1187        count_request(stage, Outcome::Rejected, mechanism, REASON_MISCONFIGURED);
1188        let mut response = Response::new(B::default());
1189        *response.status_mut() = StatusCode::UNAUTHORIZED;
1190        response.headers_mut().insert(
1191            WWW_AUTHENTICATE,
1192            HeaderValue::from_static(DEFAULT_STATIC_CHALLENGE),
1193        );
1194        return response;
1195    };
1196    let status = observe::status(rejection);
1197    let reason = match (rejection, &gate.oauth) {
1198        (TokenRejection::InsufficientScope, None) => REASON_MISCONFIGURED,
1199        _ => observe::reason(rejection),
1200    };
1201    count_request(stage, Outcome::Rejected, mechanism, reason);
1202    match (rejection, &gate.oauth) {
1203        (TokenRejection::InsufficientScope, None) if !claim_names.is_empty() => error!(
1204            path = %path,
1205            what,
1206            required = ?required,
1207            required_claims = ?claim_names,
1208            auth.outcome = Outcome::Rejected.as_str(),
1209            auth.mechanism = mechanism.as_str(),
1210            auth.reason = reason,
1211            auth.status = status,
1212            "Server misconfiguration: the route requires scopes or claim values, but its \
1213             authentication layer has no OAuth validator, so no credential can carry them; \
1214             refusing the request"
1215        ),
1216        (TokenRejection::InsufficientScope, None) => error!(
1217            path = %path,
1218            what,
1219            required = ?required,
1220            auth.outcome = Outcome::Rejected.as_str(),
1221            auth.mechanism = mechanism.as_str(),
1222            auth.reason = reason,
1223            auth.status = status,
1224            "Server misconfiguration: the route requires scopes, but its authentication layer \
1225             has no OAuth validator, so no credential can carry them; refusing the request"
1226        ),
1227        (TokenRejection::InsufficientScope, Some(_)) if !claim_names.is_empty() => {
1228            let present = match parts.extensions.get::<Credential>() {
1229                Some(Credential::OAuth(token)) => token.scopes.clone(),
1230                _ => Vec::new(),
1231            };
1232            // The claim names only: a configured value, and above all a
1233            // presented one (a group membership), never reaches the log.
1234            info!(
1235                path = %path,
1236                what,
1237                required = ?required,
1238                required_claims = ?claim_names,
1239                present = ?crate::token::scopes_for_log(&present),
1240                static_token = matches!(parts.extensions.get::<Credential>(), Some(Credential::StaticToken)),
1241                auth.outcome = Outcome::Rejected.as_str(),
1242                auth.mechanism = mechanism.as_str(),
1243                auth.reason = reason,
1244                auth.status = status,
1245                "The credential lacks the scopes or claim values this route requires"
1246            );
1247        }
1248        (TokenRejection::InsufficientScope, Some(_)) => {
1249            let present = match parts.extensions.get::<Credential>() {
1250                Some(Credential::OAuth(token)) => token.scopes.clone(),
1251                _ => Vec::new(),
1252            };
1253            // Info, as for the validator's own scope refusal: scopes are not
1254            // secret, and `present` next to `required` is the diagnosis.
1255            info!(
1256                path = %path,
1257                what,
1258                required = ?required,
1259                present = ?crate::token::scopes_for_log(&present),
1260                static_token = matches!(parts.extensions.get::<Credential>(), Some(Credential::StaticToken)),
1261                auth.outcome = Outcome::Rejected.as_str(),
1262                auth.mechanism = mechanism.as_str(),
1263                auth.reason = reason,
1264                auth.status = status,
1265                "The credential lacks the scopes this route requires"
1266            );
1267        }
1268        (TokenRejection::Missing, Some(_)) => {
1269            debug!(
1270                path = %path,
1271                what,
1272                auth.outcome = Outcome::Rejected.as_str(),
1273                auth.mechanism = mechanism.as_str(),
1274                auth.reason = reason,
1275                auth.status = status,
1276                "No bearer credential presented"
1277            );
1278        }
1279        _ => warn!(
1280            path = %path,
1281            what,
1282            reason = ?rejection,
1283            auth.outcome = Outcome::Rejected.as_str(),
1284            auth.mechanism = mechanism.as_str(),
1285            auth.reason = reason,
1286            auth.status = status,
1287            "Bearer auth rejected"
1288        ),
1289    }
1290    let insufficient = match rejection {
1291        TokenRejection::InsufficientScope => gate.scope_challenge(required),
1292        _ => None,
1293    };
1294    let (status, _) = gate.status_and_challenge_with(rejection, insufficient.as_ref());
1295    let response = parts
1296        .extensions
1297        .get::<RefusalBody<B>>()
1298        .and_then(|body| {
1299            (body.build)(
1300                &*body.source,
1301                RejectContext {
1302                    rejection,
1303                    status,
1304                    request: parts,
1305                },
1306            )
1307        })
1308        .unwrap_or_else(|| Response::new(B::default()));
1309    gate.finish_with(rejection, insufficient.as_ref(), response)
1310}
1311
1312/// A route-level scope requirement: a `tower::Layer` for the routes (or
1313/// services) that need more than the authentication layer in front of them
1314/// requires — say, a write scope on the routes that write. It adds no key
1315/// cache and no validation of its own: it reads the credential that layer
1316/// accepted from the request's extensions and checks it against its scopes
1317/// (all-of, the same matching as the validator's own check,
1318/// [`AuthorizedToken::require_scopes`]).
1319///
1320/// Place it INSIDE (behind) an authentication layer — the axum `AuthLayer`
1321/// or an [`HttpAuthLayer`], which both mark every request they pass:
1322///
1323/// | The request… | Answer |
1324/// |---|---|
1325/// | carries an OAuth token with every scope | served |
1326/// | carries an OAuth token missing one | 403, with the layer's refusal body and a challenge naming the layer's scopes followed by these ([`crate::refusal_for_scopes`]'s, for the same scopes) |
1327/// | carries a static token | 403 the same way — a static token has no scopes — unless [`static_token_bypasses_scopes`](Self::static_token_bypasses_scopes) |
1328/// | carries no credential (an [`optional`](HttpAuthLayerBuilder::optional) layer passed it through) | the layer's own 401 and challenge |
1329/// | passed an `allow_unauthenticated` layer with no credential | 401 with [`crate::DEFAULT_STATIC_CHALLENGE`], logged at `error` |
1330/// | passed NO authentication layer (mounted outside it) | 500, empty body, logged at `error` — never served |
1331///
1332/// An empty requirement serves every request the layer let through (a 500
1333/// without a layer all the same). Nothing in the credential or the scopes
1334/// reaches the response beyond the challenge; refusals are logged under
1335/// `oauth_resource_server::http_layer` (403 at `info`, with the required and
1336/// present scopes).
1337///
1338/// The credential judged is the innermost [`Credential`] a layer accepted.
1339/// A layer checks static tokens first, so a request presenting both a
1340/// static token and an OAuth token (in two sources) is judged by the static
1341/// token.
1342///
1343/// The same type is `oauth_resource_server::axum::RequireScopes`. Under
1344/// axum, the layer's refusal body comes from its `on_reject`; behind an
1345/// [`HttpAuthLayer`], from its [`on_reject`](HttpAuthLayerBuilder::on_reject)
1346/// when the response body types match, else `ResBody::default()`.
1347///
1348/// # Examples
1349///
1350/// Behind an [`HttpAuthLayer`], on any `tower` stack:
1351///
1352/// ```
1353/// use std::sync::Arc;
1354///
1355/// use http::{Request, Response};
1356/// use oauth_resource_server::OAuthValidator;
1357/// use oauth_resource_server::http_layer::{HttpAuthLayer, RequireScopes};
1358/// use tower::{ServiceBuilder, service_fn};
1359///
1360/// # fn service(oauth: Arc<OAuthValidator>) {
1361/// let writes = ServiceBuilder::new()
1362///     // Outermost first: authenticate, then require the write scope.
1363///     .layer(HttpAuthLayer::builder().oauth(oauth).build().unwrap())
1364///     .layer(RequireScopes::new(["docs:write"]))
1365///     .service(service_fn(|_request: Request<String>| async {
1366///         Ok::<_, std::convert::Infallible>(Response::new(String::from("written")))
1367///     }));
1368/// # let _ = writes;
1369/// # }
1370/// ```
1371///
1372/// Under axum (feature `axum`, as `oauth_resource_server::axum::RequireScopes`),
1373/// `.route_layer(RequireScopes::new(["docs:write"]))` on the routes that
1374/// need it, before (so inside) `.route_layer(auth)`.
1375///
1376/// # Panics
1377///
1378/// [`RequireScopes::new`] panics when a scope is not an RFC 6749 §3.3
1379/// scope-token (empty, or holding a space, `"`, `\`, a control or non-ASCII
1380/// character): no token can carry one, so the route could never be reached.
1381/// Use it for scopes written as literals in code; for scopes read from
1382/// configuration use [`RequireScopes::try_new`], which returns the error
1383/// instead.
1384#[derive(Clone, Debug)]
1385pub struct RequireScopes {
1386    scopes: Arc<[String]>,
1387    static_bypasses: bool,
1388}
1389
1390impl RequireScopes {
1391    /// Require every scope in `scopes` (deduplicated). For literals in code;
1392    /// see [`RequireScopes::try_new`] for scopes from configuration.
1393    ///
1394    /// # Panics
1395    ///
1396    /// When a scope is not an RFC 6749 §3.3 scope-token; see the type's docs.
1397    pub fn new(scopes: impl IntoIterator<Item = impl Into<String>>) -> Self {
1398        Self::try_new(scopes).unwrap_or_else(|e| panic!("RequireScopes::new: {e}"))
1399    }
1400
1401    /// [`RequireScopes::new`] for scopes read from configuration: the same
1402    /// requirement, or the first scope that is not a scope-token.
1403    ///
1404    /// # Errors
1405    ///
1406    /// [`InvalidScope`], naming the offending scope.
1407    ///
1408    /// # Examples
1409    ///
1410    /// ```
1411    /// use oauth_resource_server::http_layer::RequireScopes;
1412    ///
1413    /// assert!(RequireScopes::try_new(["docs:write"]).is_ok());
1414    /// let err = RequireScopes::try_new(["docs:write", "two words"]).unwrap_err();
1415    /// assert_eq!(err.scope(), "two words");
1416    /// ```
1417    pub fn try_new(
1418        scopes: impl IntoIterator<Item = impl Into<String>>,
1419    ) -> Result<Self, InvalidScope> {
1420        Ok(Self {
1421            scopes: checked_scopes(scopes)?.into(),
1422            static_bypasses: false,
1423        })
1424    }
1425
1426    /// Let a static token through instead of refusing it with 403.
1427    ///
1428    /// # Security
1429    ///
1430    /// A static token then reaches these routes whatever they require —
1431    /// the static token is treated as holding every scope. Use it only where
1432    /// the static token is meant to be a full-access key.
1433    pub fn static_token_bypasses_scopes(mut self) -> Self {
1434        self.static_bypasses = true;
1435        self
1436    }
1437
1438    /// The scopes required, deduplicated, in the order given.
1439    pub fn scopes(&self) -> &[String] {
1440        &self.scopes
1441    }
1442}
1443
1444impl<S> tower_layer::Layer<S> for RequireScopes {
1445    type Service = RequireScopesService<S>;
1446
1447    fn layer(&self, inner: S) -> Self::Service {
1448        RequireScopesService {
1449            require: self.clone(),
1450            inner,
1451        }
1452    }
1453}
1454
1455/// The service a [`RequireScopes`] wraps another in.
1456#[derive(Clone, Debug)]
1457pub struct RequireScopesService<S> {
1458    require: RequireScopes,
1459    inner: S,
1460}
1461
1462impl<S, ReqBody, ResBody> tower_service::Service<Request<ReqBody>> for RequireScopesService<S>
1463where
1464    S: tower_service::Service<Request<ReqBody>, Response = Response<ResBody>>
1465        + Clone
1466        + Send
1467        + 'static,
1468    S::Future: Send + 'static,
1469    ReqBody: Send + 'static,
1470    ResBody: Default + 'static,
1471{
1472    type Response = Response<ResBody>;
1473    type Error = S::Error;
1474    type Future =
1475        Pin<Box<dyn Future<Output = Result<Response<ResBody>, S::Error>> + Send + 'static>>;
1476
1477    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
1478        self.inner.poll_ready(cx)
1479    }
1480
1481    fn call(&mut self, request: Request<ReqBody>) -> Self::Future {
1482        let clone = self.inner.clone();
1483        let mut inner = std::mem::replace(&mut self.inner, clone);
1484        let require = self.require.clone();
1485        Box::pin(async move {
1486            // Decided before the inner call and never held across it, so the
1487            // future is `Send` without `ResBody: Send`.
1488            let (parts, body) = request.into_parts();
1489            let verdict = judge_scopes(&parts, &require.scopes, &[], require.static_bypasses);
1490            let rejection = match verdict {
1491                ScopeVerdict::Pass => return inner.call(Request::from_parts(parts, body)).await,
1492                ScopeVerdict::Refuse(rejection) => rejection,
1493                ScopeVerdict::NoLayer => TokenRejection::Missing,
1494            };
1495            Ok(scope_refusal(
1496                &parts,
1497                &rejection,
1498                &require.scopes,
1499                "RequireScopes",
1500            ))
1501        })
1502    }
1503}
1504
1505/// A `tower::Layer` that authenticates every request to the service it wraps,
1506/// for any `http::Request<ReqBody>` / `http::Response<ResBody>` service; see
1507/// the [module docs](self). Cheap to clone (two `Arc`s).
1508///
1509/// `R` is what builds a refusal's response: [`EmptyRefusal`] unless
1510/// [`HttpAuthLayerBuilder::on_reject`] set a callback.
1511///
1512/// Built once at startup. Nothing in it hot-reloads: a changed static token or
1513/// OAuth config takes effect when a new layer is built, which in practice means
1514/// a restart.
1515pub struct HttpAuthLayer<R = EmptyRefusal> {
1516    mode: Arc<HttpMode>,
1517    on_reject: Arc<R>,
1518}
1519
1520enum HttpMode {
1521    Enforce(Arc<Gate>),
1522    AllowUnauthenticated,
1523}
1524
1525impl<R> Clone for HttpAuthLayer<R> {
1526    fn clone(&self) -> Self {
1527        Self {
1528            mode: Arc::clone(&self.mode),
1529            on_reject: Arc::clone(&self.on_reject),
1530        }
1531    }
1532}
1533
1534/// Hand-written so the static token never reaches a log line through `{:?}`.
1535impl<R> std::fmt::Debug for HttpAuthLayer<R> {
1536    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1537        match &*self.mode {
1538            HttpMode::AllowUnauthenticated => f
1539                .debug_struct("HttpAuthLayer")
1540                .field("allow_unauthenticated", &true)
1541                .finish(),
1542            HttpMode::Enforce(g) => f
1543                .debug_struct("HttpAuthLayer")
1544                .field("static_tokens", &g.static_tokens)
1545                .field("oauth", &g.oauth)
1546                .field("sources", &g.sources)
1547                .field("on_reject", &std::any::type_name::<R>())
1548                .field("static_challenge", &g.static_challenge)
1549                .field("optional", &g.optional)
1550                .finish(),
1551        }
1552    }
1553}
1554
1555impl HttpAuthLayer {
1556    /// Start building an enforcing layer. Give it a static token, an OAuth
1557    /// validator, or both; optionally the credential sources (default:
1558    /// `Authorization: Bearer`), the refusal body
1559    /// ([`on_reject`](HttpAuthLayerBuilder::on_reject)), and
1560    /// [`optional`](HttpAuthLayerBuilder::optional). To honour
1561    /// `accept_static_bearer`, finish with
1562    /// [`build_with_decision`](HttpAuthLayerBuilder::build_with_decision) and a
1563    /// [`crate::static_token_policy`] decision.
1564    ///
1565    /// # Examples
1566    ///
1567    /// ```
1568    /// use http::HeaderName;
1569    /// use oauth_resource_server::http_layer::{AuthLayerError, CredentialSource, HttpAuthLayer};
1570    ///
1571    /// let auth = HttpAuthLayer::builder()
1572    ///     .static_token("example-static-key")
1573    ///     .sources([
1574    ///         CredentialSource::authorization_bearer(),
1575    ///         CredentialSource::Raw(HeaderName::from_static("x-api-key")),
1576    ///     ])
1577    ///     .build()
1578    ///     .unwrap();
1579    /// # let _ = auth;
1580    ///
1581    /// // Fail closed: no credential configured is an error, not a pass-through.
1582    /// assert_eq!(
1583    ///     HttpAuthLayer::builder().build().unwrap_err(),
1584    ///     AuthLayerError::NoCredential
1585    /// );
1586    /// ```
1587    pub fn builder() -> HttpAuthLayerBuilder {
1588        HttpAuthLayerBuilder::default()
1589    }
1590
1591    /// A layer that lets EVERY request through, unauthenticated, and inserts no
1592    /// credential into request extensions. The explicit opt-out; the only
1593    /// other way to get it is a [`StaticTokenDecision::Unauthenticated`] handed
1594    /// to [`HttpAuthLayer::from_decision`] or
1595    /// [`HttpAuthLayerBuilder::build_with_decision`].
1596    ///
1597    /// # Security
1598    ///
1599    /// Every request reaches the wrapped service. Use it only where something
1600    /// else (a trusted network, a proxy that authenticates) stands in front.
1601    /// It still marks every `Authorization` header value sensitive
1602    /// (`http::HeaderValue::set_sensitive`), so a credential a client sends
1603    /// anyway is not printed by a `Debug` of the request downstream.
1604    pub fn allow_unauthenticated() -> Self {
1605        Self {
1606            mode: Arc::new(HttpMode::AllowUnauthenticated),
1607            on_reject: Arc::new(EmptyRefusal),
1608        }
1609    }
1610
1611    /// The layer a [`crate::static_token_policy`] decision calls for, with the
1612    /// default source and refusal body. Shorthand for
1613    /// `HttpAuthLayer::builder().optional_oauth(oauth).build_with_decision(decision)`.
1614    ///
1615    /// # Errors
1616    ///
1617    /// See [`HttpAuthLayerBuilder::build_with_decision`].
1618    pub fn from_decision(
1619        decision: StaticTokenDecision,
1620        oauth: Option<Arc<OAuthValidator>>,
1621    ) -> Result<Self, AuthLayerError> {
1622        Self::builder()
1623            .optional_oauth(oauth)
1624            .build_with_decision(decision)
1625    }
1626}
1627
1628impl<R> HttpAuthLayer<R> {
1629    /// Whether this is the [`HttpAuthLayer::allow_unauthenticated`] pass-through.
1630    pub fn allows_unauthenticated(&self) -> bool {
1631        matches!(*self.mode, HttpMode::AllowUnauthenticated)
1632    }
1633
1634    /// The OAuth validator, when one is configured.
1635    pub fn oauth(&self) -> Option<&Arc<OAuthValidator>> {
1636        match &*self.mode {
1637            HttpMode::Enforce(g) => g.oauth.as_ref(),
1638            HttpMode::AllowUnauthenticated => None,
1639        }
1640    }
1641
1642    /// Authenticate `request`: the request to pass on (with the credential in
1643    /// its extensions), or the refusal to answer with.
1644    async fn check<ReqBody, ResBody>(
1645        &self,
1646        request: Request<ReqBody>,
1647    ) -> Result<Request<ReqBody>, Response<ResBody>>
1648    where
1649        R: RefusalResponse<ResBody> + Send + Sync + 'static,
1650        ResBody: 'static,
1651    {
1652        let gate = match &*self.mode {
1653            HttpMode::AllowUnauthenticated => {
1654                count_request(
1655                    Stage::Layer,
1656                    Outcome::PassedThrough,
1657                    Mechanism::None,
1658                    REASON_NONE,
1659                );
1660                let mut request = request;
1661                mark_authorization_sensitive(request.headers_mut());
1662                self.mark::<ResBody>(request.extensions_mut(), None);
1663                return Ok(request);
1664            }
1665            HttpMode::Enforce(gate) => gate,
1666        };
1667        let (mut parts, body) = request.into_parts();
1668        match gate.admit(&mut parts).await {
1669            Admission::Static => log_static_accepted!(&parts),
1670            Admission::OAuth(token) => log_oauth_accepted!(&parts, &token),
1671            Admission::PassedThrough => log_passed_through!(&parts),
1672            Admission::Refused(rejection, mechanism) => {
1673                log_layer_refusal!(gate, &parts, &rejection, mechanism, Stage::Layer);
1674                let (status, _) = gate.status_and_challenge(&rejection);
1675                let response = self.on_reject.refusal_response(RejectContext {
1676                    rejection: &rejection,
1677                    status,
1678                    request: &parts,
1679                });
1680                return Err(gate.finish(&rejection, response));
1681            }
1682        }
1683        self.mark::<ResBody>(&mut parts.extensions, Some(Arc::clone(gate)));
1684        Ok(Request::from_parts(parts, body))
1685    }
1686
1687    /// Insert the markers a route-level scope check behind this layer reads
1688    /// ([`GateRan`], and this layer's refusal builder for `ResBody`).
1689    fn mark<ResBody>(&self, extensions: &mut http::Extensions, gate: Option<Arc<Gate>>)
1690    where
1691        R: RefusalResponse<ResBody> + Send + Sync + 'static,
1692        ResBody: 'static,
1693    {
1694        // An `allow_unauthenticated` layer inside an enforcing one leaves the
1695        // outer marker in place: the credential a route check behind both
1696        // judges is the outer layer's, so its refusal must be too (a 403
1697        // step-up with the outer challenge, never the open layer's 401).
1698        if gate.is_none() && extensions.get::<GateRan>().is_some() {
1699            return;
1700        }
1701        extensions.insert(GateRan(gate));
1702        extensions.insert(RefusalBody::<ResBody> {
1703            source: Arc::clone(&self.on_reject) as Arc<dyn std::any::Any + Send + Sync>,
1704            build: http_refusal_body::<R, ResBody>,
1705        });
1706    }
1707}
1708
1709/// Builder for an enforcing [`HttpAuthLayer`]; see [`HttpAuthLayer::builder`].
1710pub struct HttpAuthLayerBuilder<R = EmptyRefusal> {
1711    static_token: Option<Zeroizing<String>>,
1712    static_tokens: Option<StaticTokens>,
1713    oauth: Option<Arc<OAuthValidator>>,
1714    sources: Option<Vec<CredentialSource>>,
1715    /// `None`: not set, so [`DEFAULT_STATIC_CHALLENGE`].
1716    static_challenge: Option<Option<HeaderValue>>,
1717    optional: bool,
1718    required_scopes: Vec<String>,
1719    static_bypasses_scopes: bool,
1720    on_reject: R,
1721}
1722
1723impl Default for HttpAuthLayerBuilder {
1724    fn default() -> Self {
1725        Self {
1726            static_token: None,
1727            static_tokens: None,
1728            oauth: None,
1729            sources: None,
1730            static_challenge: None,
1731            optional: false,
1732            required_scopes: Vec::new(),
1733            static_bypasses_scopes: false,
1734            on_reject: EmptyRefusal,
1735        }
1736    }
1737}
1738
1739/// Hand-written so the static token never reaches a log line through `{:?}`.
1740impl<R> std::fmt::Debug for HttpAuthLayerBuilder<R> {
1741    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1742        f.debug_struct("HttpAuthLayerBuilder")
1743            .field(
1744                "static_token",
1745                &self.static_token.as_ref().map(|_| "<redacted>"),
1746            )
1747            .field("static_tokens", &self.static_tokens)
1748            .field("oauth", &self.oauth)
1749            .field("sources", &self.sources)
1750            .field("on_reject", &std::any::type_name::<R>())
1751            .field("static_challenge", &self.static_challenge)
1752            .field("optional", &self.optional)
1753            .field("required_scopes", &self.required_scopes)
1754            .field("static_bypasses_scopes", &self.static_bypasses_scopes)
1755            .finish()
1756    }
1757}
1758
1759impl<R> HttpAuthLayerBuilder<R> {
1760    /// Accept this static token (compared in constant time). An empty string
1761    /// counts as no token.
1762    ///
1763    /// # Security
1764    ///
1765    /// Setting the token here bypasses `accept_static_bearer`, which only
1766    /// [`crate::static_token_policy`] reads; with OAuth configured, prefer
1767    /// [`HttpAuthLayerBuilder::build_with_decision`]. The token's length is not
1768    /// hidden by the comparison, and `Debug` output shows it as `<redacted>`.
1769    pub fn static_token(mut self, token: impl Into<String>) -> Self {
1770        self.static_token = Some(Zeroizing::new(token.into()));
1771        self
1772    }
1773
1774    /// [`HttpAuthLayerBuilder::static_token`] when `Some`.
1775    pub fn optional_static_token(mut self, token: Option<String>) -> Self {
1776        self.static_token = token.map(Zeroizing::new);
1777        self
1778    }
1779
1780    /// Accept every token in `tokens` (each compared in constant time, every
1781    /// entry every time; see [`StaticTokens`]), replacing a set given
1782    /// earlier. On a match the request's extensions get
1783    /// [`Credential::StaticToken`] and the [`StaticTokenMatch`] naming the
1784    /// entry's label.
1785    ///
1786    /// Combines with the other static-token settings exactly as the axum
1787    /// layer's `AuthLayerBuilder::static_tokens` does: with
1788    /// [`static_token`](Self::static_token), both are accepted (a secret in
1789    /// both counts once, under the set's label); with
1790    /// [`build_with_decision`](Self::build_with_decision), see that method.
1791    /// An empty set counts as no static token, so it does not satisfy the
1792    /// fail-closed build on its own.
1793    ///
1794    /// # Security
1795    ///
1796    /// Like [`static_token`](Self::static_token), this bypasses
1797    /// `accept_static_bearer` unless the layer is built with
1798    /// [`build_with_decision`](Self::build_with_decision). `Debug` output
1799    /// shows the count and labels, never a secret.
1800    ///
1801    /// # Examples
1802    ///
1803    /// ```
1804    /// use oauth_resource_server::StaticTokens;
1805    /// use oauth_resource_server::http_layer::HttpAuthLayer;
1806    ///
1807    /// let tokens = StaticTokens::new()
1808    ///     .with(Some("current"), "example-key-old")
1809    ///     .and_then(|t| t.with(Some("next"), "example-key-new"))
1810    ///     .unwrap();
1811    /// let auth = HttpAuthLayer::builder().static_tokens(tokens).build().unwrap();
1812    /// # let _ = auth;
1813    /// ```
1814    pub fn static_tokens(mut self, tokens: StaticTokens) -> Self {
1815        self.static_tokens = Some(tokens);
1816        self
1817    }
1818
1819    /// [`HttpAuthLayerBuilder::static_tokens`] when `Some`; `None` clears a
1820    /// set given earlier.
1821    pub fn optional_static_tokens(mut self, tokens: Option<StaticTokens>) -> Self {
1822        self.static_tokens = tokens;
1823        self
1824    }
1825
1826    /// Accept OAuth access tokens this validator accepts.
1827    pub fn oauth(mut self, validator: Arc<OAuthValidator>) -> Self {
1828        self.oauth = Some(validator);
1829        self
1830    }
1831
1832    /// [`HttpAuthLayerBuilder::oauth`] when `Some`.
1833    pub fn optional_oauth(mut self, validator: Option<Arc<OAuthValidator>>) -> Self {
1834        self.oauth = validator;
1835        self
1836    }
1837
1838    /// Where to read credentials from, replacing the default
1839    /// `[CredentialSource::authorization_bearer()]`. Every source is checked,
1840    /// whatever the others hold.
1841    pub fn sources(mut self, sources: impl IntoIterator<Item = CredentialSource>) -> Self {
1842        self.sources = Some(sources.into_iter().collect());
1843        self
1844    }
1845
1846    /// The `WWW-Authenticate` challenge every 401 carries when NO OAuth
1847    /// validator is configured; with one, the validator's challenges are used
1848    /// and this is ignored. Default: [`crate::DEFAULT_STATIC_CHALLENGE`].
1849    ///
1850    /// `None` sends no challenge at all and leaves any `WWW-Authenticate` an
1851    /// [`on_reject`](Self::on_reject) callback set untouched. That departs from
1852    /// RFC 9110 §15.5.2 (a 401 MUST carry a challenge); use it only to keep an
1853    /// existing API's responses unchanged.
1854    pub fn static_challenge(mut self, challenge: Option<HeaderValue>) -> Self {
1855        self.static_challenge = Some(challenge);
1856        self
1857    }
1858
1859    /// Let a request that presents NO credential through, unauthenticated, with
1860    /// nothing inserted into its extensions; a credential that is presented but
1861    /// refused is refused exactly as without this. "No credential" is decided
1862    /// exactly as by the axum layer's `optional()` (see its documentation): every
1863    /// value of every configured source header absent or blank, where a value
1864    /// that is not visible ASCII, a non-blank later value of a repeated header,
1865    /// a `DPoP`-scheme value and a tab-separated `Bearer` token all count as
1866    /// presented. Any [`Credential`]/[`AuthorizedToken`] an outer layer
1867    /// inserted is removed first.
1868    ///
1869    /// # Security
1870    ///
1871    /// Not a way around the fail-closed build: [`build`](Self::build) still
1872    /// requires a static token or an OAuth validator. Every handler behind an
1873    /// optional layer must treat a request with no [`Credential`] in its
1874    /// extensions as unauthenticated.
1875    pub fn optional(mut self) -> Self {
1876        self.optional = true;
1877        self
1878    }
1879
1880    /// Require every scope in `scopes` (all-of) of every credential this
1881    /// layer accepts, on top of the validator's own `required_scopes` —
1882    /// replacing scopes given earlier. The same validator, so the same key
1883    /// cache: no second validator is needed for routes that need more.
1884    ///
1885    /// Behaves exactly as the axum layer's `AuthLayerBuilder::require_scopes`:
1886    /// an OAuth token missing one is refused with 403 and a challenge naming
1887    /// the validator's required scopes followed by these (see
1888    /// [`crate::refusal_for_scopes`]); a static token — it has no scopes — is
1889    /// refused the same way unless
1890    /// [`static_token_bypasses_scopes`](Self::static_token_bypasses_scopes);
1891    /// an [`optional`](Self::optional) layer still passes a request that
1892    /// presents nothing. A layer that lets everything through
1893    /// ([`HttpAuthLayer::allow_unauthenticated`]) checks nothing, this
1894    /// included, which is why [`build_with_decision`](Self::build_with_decision)
1895    /// refuses an `Unauthenticated` decision on a builder with scopes
1896    /// ([`AuthLayerError::ScopesWithoutAuthentication`]) rather than drop them.
1897    ///
1898    /// For a requirement on some routes only, put a [`RequireScopes`] layer
1899    /// on them instead, behind this one.
1900    ///
1901    /// # Examples
1902    ///
1903    /// ```no_run
1904    /// use std::sync::Arc;
1905    ///
1906    /// use oauth_resource_server::OAuthValidator;
1907    /// use oauth_resource_server::http_layer::HttpAuthLayer;
1908    ///
1909    /// # fn layers(oauth: Arc<OAuthValidator>) {
1910    /// let writes = HttpAuthLayer::builder()
1911    ///     .oauth(oauth)
1912    ///     .require_scopes(["docs:write"])
1913    ///     .build()
1914    ///     .unwrap();
1915    /// # let _ = writes;
1916    /// # }
1917    /// ```
1918    pub fn require_scopes(mut self, scopes: impl IntoIterator<Item = impl Into<String>>) -> Self {
1919        self.required_scopes = scopes.into_iter().map(Into::into).collect();
1920        self
1921    }
1922
1923    /// Let a static token pass [`require_scopes`](Self::require_scopes)
1924    /// instead of refusing it with 403: the static token counts as holding
1925    /// every scope. Without `require_scopes` it changes nothing.
1926    ///
1927    /// # Security
1928    ///
1929    /// Opt in only where the static token is meant to be a full-access key.
1930    pub fn static_token_bypasses_scopes(mut self) -> Self {
1931        self.static_bypasses_scopes = true;
1932        self
1933    }
1934
1935    /// Build a refusal's response (its body and any extra headers, such as
1936    /// `Content-Type`) — for an API whose errors are, say, JSON. Without it a
1937    /// refusal's body is `ResBody::default()`.
1938    ///
1939    /// The callback shapes the response only; it cannot change the outcome.
1940    /// Whatever it returns, the status is set to [`RejectContext::status`] and
1941    /// `WWW-Authenticate` to the layer's challenge, replacing any the callback
1942    /// set (only with `static_challenge(None)` and no OAuth are the callback's
1943    /// headers left as they are). Never put [`TokenRejection::Invalid`]'s
1944    /// reason in the body.
1945    ///
1946    /// # Examples
1947    ///
1948    /// ```
1949    /// use http::{Response, header::CONTENT_TYPE};
1950    /// use oauth_resource_server::http_layer::{HttpAuthLayer, RejectContext};
1951    ///
1952    /// let auth = HttpAuthLayer::builder()
1953    ///     .static_token("example-static-key")
1954    ///     .on_reject(|cx: RejectContext<'_>| {
1955    ///         Response::builder()
1956    ///             .header(CONTENT_TYPE, "application/json")
1957    ///             .body(format!(r#"{{"error":"{}"}}"#, cx.status.as_u16()))
1958    ///             .unwrap()
1959    ///     })
1960    ///     .build()
1961    ///     .unwrap();
1962    /// # let _ = auth;
1963    /// ```
1964    pub fn on_reject<B, F>(self, f: F) -> HttpAuthLayerBuilder<F>
1965    where
1966        F: Fn(RejectContext<'_>) -> Response<B> + Send + Sync + 'static,
1967    {
1968        HttpAuthLayerBuilder {
1969            static_token: self.static_token,
1970            static_tokens: self.static_tokens,
1971            oauth: self.oauth,
1972            sources: self.sources,
1973            static_challenge: self.static_challenge,
1974            optional: self.optional,
1975            required_scopes: self.required_scopes,
1976            static_bypasses_scopes: self.static_bypasses_scopes,
1977            on_reject: f,
1978        }
1979    }
1980
1981    /// Build the layer a [`crate::static_token_policy`] decision calls for,
1982    /// keeping this builder's other settings. The decision's static token (if
1983    /// any) replaces one set on this builder;
1984    /// [`StaticTokenDecision::Unauthenticated`] yields the
1985    /// [`allow_unauthenticated`](HttpAuthLayer::allow_unauthenticated)
1986    /// pass-through.
1987    ///
1988    /// A [`static_tokens`](Self::static_tokens) set follows the decision, as
1989    /// for the axum layer's `AuthLayerBuilder::build_with_decision`: kept, and
1990    /// merged with the decision's token, when the decision carries one
1991    /// (`StaticOnly`/`StaticAndOAuth`); dropped with it on `StaticIgnored`
1992    /// (`accept_static_bearer: false` wins); refused alongside `OAuthOnly` or
1993    /// `Unauthenticated`, which were decided without any static token.
1994    ///
1995    /// # Errors
1996    ///
1997    /// [`AuthLayerError::DecisionNeedsOAuth`] when the decision was made with
1998    /// OAuth on and no validator was given,
1999    /// [`AuthLayerError::DecisionWithoutOAuth`] when it was made with OAuth off
2000    /// (including `Unauthenticated`) and one was given, then
2001    /// [`AuthLayerError::DecisionWithoutStaticToken`] for a non-empty
2002    /// `static_tokens` set with an `OAuthOnly` or `Unauthenticated` decision,
2003    /// then [`AuthLayerError::ScopesWithoutAuthentication`] for
2004    /// [`require_scopes`](Self::require_scopes) with an `Unauthenticated`
2005    /// decision. Otherwise as [`HttpAuthLayerBuilder::build`].
2006    pub fn build_with_decision(
2007        mut self,
2008        decision: StaticTokenDecision,
2009    ) -> Result<HttpAuthLayer<R>, AuthLayerError> {
2010        Gate::check_decision(&decision, self.oauth.is_some())?;
2011        let unauthenticated = decision == StaticTokenDecision::Unauthenticated;
2012        let (token, tokens) = Gate::decision_tokens(decision, self.static_tokens.take())?;
2013        if unauthenticated && !self.required_scopes.is_empty() {
2014            return Err(AuthLayerError::ScopesWithoutAuthentication);
2015        }
2016        if unauthenticated {
2017            return Ok(HttpAuthLayer {
2018                mode: Arc::new(HttpMode::AllowUnauthenticated),
2019                on_reject: Arc::new(self.on_reject),
2020            });
2021        }
2022        self.static_token = token;
2023        self.static_tokens = tokens;
2024        self.build()
2025    }
2026
2027    /// Build the layer.
2028    ///
2029    /// # Errors
2030    ///
2031    /// [`AuthLayerError::NoCredential`] with neither a non-blank static token
2032    /// (from [`static_token`](Self::static_token) or a non-empty
2033    /// [`static_tokens`](Self::static_tokens) set) nor an OAuth validator;
2034    /// [`AuthLayerError::NoSources`] with an empty
2035    /// source list; [`AuthLayerError::InvalidChallenge`] when the validator's
2036    /// challenge is not a valid header value (only reachable from a
2037    /// hand-edited resolved config); [`AuthLayerError::InvalidScope`] when a
2038    /// [`require_scopes`](Self::require_scopes) entry is not a scope-token;
2039    /// [`AuthLayerError::ScopesNeedOAuth`] for `require_scopes` with no OAuth
2040    /// validator and no
2041    /// [`static_token_bypasses_scopes`](Self::static_token_bypasses_scopes).
2042    pub fn build(self) -> Result<HttpAuthLayer<R>, AuthLayerError> {
2043        let gate = Gate::build(
2044            self.static_token,
2045            self.static_tokens,
2046            self.oauth,
2047            self.sources,
2048            self.static_challenge,
2049            self.optional,
2050            self.required_scopes,
2051            self.static_bypasses_scopes,
2052        )?;
2053        Ok(HttpAuthLayer {
2054            mode: Arc::new(HttpMode::Enforce(Arc::new(gate))),
2055            on_reject: Arc::new(self.on_reject),
2056        })
2057    }
2058}
2059
2060impl<S, R> tower_layer::Layer<S> for HttpAuthLayer<R> {
2061    type Service = HttpAuthService<S, R>;
2062
2063    fn layer(&self, inner: S) -> Self::Service {
2064        HttpAuthService {
2065            layer: self.clone(),
2066            inner,
2067        }
2068    }
2069}
2070
2071/// The service an [`HttpAuthLayer`] wraps another in.
2072pub struct HttpAuthService<S, R = EmptyRefusal> {
2073    layer: HttpAuthLayer<R>,
2074    inner: S,
2075}
2076
2077impl<S: Clone, R> Clone for HttpAuthService<S, R> {
2078    fn clone(&self) -> Self {
2079        Self {
2080            layer: self.layer.clone(),
2081            inner: self.inner.clone(),
2082        }
2083    }
2084}
2085
2086impl<S: std::fmt::Debug, R> std::fmt::Debug for HttpAuthService<S, R> {
2087    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2088        f.debug_struct("HttpAuthService")
2089            .field("layer", &self.layer)
2090            .field("inner", &self.inner)
2091            .finish()
2092    }
2093}
2094
2095impl<S, R, ReqBody, ResBody> tower_service::Service<Request<ReqBody>> for HttpAuthService<S, R>
2096where
2097    S: tower_service::Service<Request<ReqBody>, Response = Response<ResBody>>
2098        + Clone
2099        + Send
2100        + 'static,
2101    S::Future: Send + 'static,
2102    R: RefusalResponse<ResBody> + Send + Sync + 'static,
2103    ReqBody: Send + 'static,
2104    ResBody: 'static,
2105{
2106    type Response = Response<ResBody>;
2107    type Error = S::Error;
2108    type Future =
2109        Pin<Box<dyn Future<Output = Result<Response<ResBody>, S::Error>> + Send + 'static>>;
2110
2111    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
2112        self.inner.poll_ready(cx)
2113    }
2114
2115    fn call(&mut self, request: Request<ReqBody>) -> Self::Future {
2116        // Call the instance `poll_ready` was driven on and leave a fresh clone in
2117        // its place (the usual tower pattern for a service moved into a future).
2118        let clone = self.inner.clone();
2119        let mut inner = std::mem::replace(&mut self.inner, clone);
2120        let layer = self.layer.clone();
2121        Box::pin(async move {
2122            // Bound first, so no `ResBody` is held across the inner call and
2123            // the future is `Send` without requiring `ResBody: Send`.
2124            let request = match layer.check(request).await {
2125                Ok(request) => request,
2126                Err(refusal) => return Ok(refusal),
2127            };
2128            inner.call(request).await
2129        })
2130    }
2131}
2132
2133#[cfg(test)]
2134mod tests {
2135    use ::tower::{ServiceExt, service_fn};
2136
2137    use super::*;
2138    use crate::testing;
2139
2140    const STATIC: &str = "secret";
2141
2142    fn validator(jwks_uri: &str) -> Arc<OAuthValidator> {
2143        Arc::new(OAuthValidator::new(&testing::resolved_config(jwks_uri)).unwrap())
2144    }
2145
2146    fn unscoped_token() -> String {
2147        testing::mint(
2148            testing::KEY_A_PEM,
2149            testing::KID_A,
2150            &serde_json::json!({
2151                "iss": testing::ISSUER, "aud": testing::AUDIENCE,
2152                "exp": testing::now() + 3600, "scope": "openid profile",
2153            }),
2154        )
2155    }
2156
2157    fn expired_token() -> String {
2158        testing::mint(
2159            testing::KEY_A_PEM,
2160            testing::KID_A,
2161            &serde_json::json!({
2162                "iss": testing::ISSUER, "aud": testing::AUDIENCE,
2163                "exp": testing::now() - 3600, "scope": "mcp:read",
2164            }),
2165        )
2166    }
2167
2168    /// A plain (non-axum) inner service over `String` bodies: 200 with a
2169    /// description of what the layer inserted, after asserting every
2170    /// credential header reached it marked sensitive.
2171    async fn inner(request: Request<String>) -> Result<Response<String>, std::convert::Infallible> {
2172        for name in ["authorization", "x-api-key"] {
2173            for value in request.headers().get_all(name) {
2174                assert!(
2175                    value.is_sensitive(),
2176                    "{name} must reach the service sensitive"
2177                );
2178            }
2179        }
2180        let token = request.extensions().get::<AuthorizedToken>();
2181        let credential = request.extensions().get::<Credential>();
2182        let body = match (credential, token) {
2183            (Some(Credential::OAuth(_)), Some(t)) => format!("oauth {:?}", t.subject),
2184            (Some(Credential::StaticToken), None) => "static".to_string(),
2185            (None, None) => "anonymous".to_string(),
2186            other => panic!("unexpected extensions: {other:?}"),
2187        };
2188        Ok(Response::new(body))
2189    }
2190
2191    async fn send<R>(layer: &HttpAuthLayer<R>, headers: &[(&str, &str)]) -> Response<String>
2192    where
2193        R: RefusalResponse<String> + Send + Sync + 'static,
2194    {
2195        let mut request = Request::builder().uri("/test");
2196        for (name, value) in headers {
2197            request = request.header(*name, *value);
2198        }
2199        let service = tower_layer::Layer::layer(layer, service_fn(inner));
2200        service
2201            .oneshot(request.body(String::new()).unwrap())
2202            .await
2203            .unwrap()
2204    }
2205
2206    fn challenge<B>(response: &Response<B>) -> Option<&str> {
2207        response
2208            .headers()
2209            .get(WWW_AUTHENTICATE)
2210            .map(|v| v.to_str().unwrap())
2211    }
2212
2213    #[test]
2214    fn the_builder_fails_closed() {
2215        assert_eq!(
2216            HttpAuthLayer::builder().build().unwrap_err(),
2217            AuthLayerError::NoCredential
2218        );
2219        assert_eq!(
2220            HttpAuthLayer::builder()
2221                .static_token("")
2222                .build()
2223                .unwrap_err(),
2224            AuthLayerError::NoCredential
2225        );
2226        assert_eq!(
2227            HttpAuthLayer::builder().optional().build().unwrap_err(),
2228            AuthLayerError::NoCredential
2229        );
2230        assert_eq!(
2231            HttpAuthLayer::builder()
2232                .static_token(STATIC)
2233                .sources([])
2234                .build()
2235                .unwrap_err(),
2236            AuthLayerError::NoSources
2237        );
2238        assert_eq!(
2239            HttpAuthLayer::builder()
2240                .build_with_decision(StaticTokenDecision::OAuthOnly)
2241                .unwrap_err(),
2242            AuthLayerError::DecisionNeedsOAuth
2243        );
2244        let built = HttpAuthLayer::builder()
2245            .static_token(STATIC)
2246            .optional()
2247            .build()
2248            .unwrap();
2249        assert!(!built.allows_unauthenticated() && built.oauth().is_none());
2250        assert!(
2251            HttpAuthLayer::builder()
2252                .build_with_decision(StaticTokenDecision::Unauthenticated)
2253                .unwrap()
2254                .allows_unauthenticated()
2255        );
2256    }
2257
2258    #[test]
2259    fn a_validator_on_its_fallback_challenges_fails_the_build_as_for_axum() {
2260        let mut cfg = testing::resolved_config("http://127.0.0.1:1/jwks");
2261        cfg.resource = "https://api.example.test/v1\r\nX-Injected: 1".into();
2262        let v = Arc::new(OAuthValidator::new(&cfg).unwrap());
2263        assert_eq!(
2264            HttpAuthLayer::builder()
2265                .static_token(STATIC)
2266                .oauth(Arc::clone(&v))
2267                .build()
2268                .unwrap_err(),
2269            AuthLayerError::InvalidChallenge
2270        );
2271        #[cfg(feature = "axum")]
2272        assert_eq!(
2273            crate::axum::AuthLayer::builder()
2274                .oauth(v)
2275                .build()
2276                .unwrap_err(),
2277            AuthLayerError::InvalidChallenge
2278        );
2279    }
2280
2281    #[tokio::test]
2282    async fn only_the_explicit_opt_out_passes_everything() {
2283        // Like the axum layer's pass-through, it reads no header, but it marks
2284        // `Authorization` sensitive: a client may send one anyway.
2285        let layer = HttpAuthLayer::allow_unauthenticated();
2286        let service = tower_layer::Layer::layer(
2287            &layer,
2288            service_fn(|request: Request<String>| async move {
2289                let inserted = request.extensions().get::<Credential>().is_some()
2290                    || request.extensions().get::<AuthorizedToken>().is_some();
2291                assert!(!inserted, "a pass-through inserts nothing");
2292                for value in request.headers().get_all("authorization") {
2293                    assert!(value.is_sensitive(), "Authorization must be sensitive");
2294                }
2295                Ok::<_, std::convert::Infallible>(Response::new(String::from("anonymous")))
2296            }),
2297        );
2298        for authorization in [None, Some("Bearer junk")] {
2299            let mut request = Request::builder().uri("/test");
2300            if let Some(value) = authorization {
2301                request = request.header("authorization", value);
2302            }
2303            let response = service
2304                .clone()
2305                .oneshot(request.body(String::new()).unwrap())
2306                .await
2307                .unwrap();
2308            assert_eq!(response.status(), StatusCode::OK);
2309            assert_eq!(response.body(), "anonymous");
2310        }
2311    }
2312
2313    #[tokio::test]
2314    async fn oauth_accepts_a_valid_token_and_refuses_the_rest_with_the_validators_challenge() {
2315        let jwks = testing::spawn_jwks_server("200 OK", testing::jwks_body()).await;
2316        let v = validator(&jwks.url);
2317        let layer = HttpAuthLayer::builder()
2318            .oauth(Arc::clone(&v))
2319            .build()
2320            .unwrap();
2321
2322        let bearer = format!("Bearer {}", testing::valid_token());
2323        let ok = send(&layer, &[("authorization", &bearer)]).await;
2324        assert_eq!(ok.status(), StatusCode::OK);
2325        assert!(ok.body().starts_with("oauth "), "{}", ok.body());
2326        assert_eq!(challenge(&ok), None);
2327
2328        let cases = [
2329            (None, 401, v.invalid_token_challenge()),
2330            (
2331                Some(format!("Bearer {}", expired_token())),
2332                401,
2333                v.invalid_token_challenge(),
2334            ),
2335            (
2336                Some("Bearer not-a-jwt".to_string()),
2337                401,
2338                v.invalid_token_challenge(),
2339            ),
2340            (
2341                Some(format!("Bearer {}", unscoped_token())),
2342                403,
2343                v.insufficient_scope_challenge(),
2344            ),
2345        ];
2346        for (header, status, expected) in cases {
2347            let headers: Vec<(&str, &str)> = header
2348                .iter()
2349                .map(|h| ("authorization", h.as_str()))
2350                .collect();
2351            let response = send(&layer, &headers).await;
2352            assert_eq!(response.status().as_u16(), status, "{header:?}");
2353            assert_eq!(challenge(&response), Some(expected.as_str()), "{header:?}");
2354            assert_eq!(
2355                response.body(),
2356                "",
2357                "the default body is ResBody::default()"
2358            );
2359        }
2360    }
2361
2362    #[tokio::test]
2363    async fn static_only_sends_the_static_challenge_unless_opted_out() {
2364        let default = HttpAuthLayer::builder()
2365            .static_token(STATIC)
2366            .build()
2367            .unwrap();
2368        let ok = send(&default, &[("authorization", "Bearer secret")]).await;
2369        assert_eq!(
2370            (ok.status(), ok.body().as_str()),
2371            (StatusCode::OK, "static")
2372        );
2373        for headers in [&[][..], &[("authorization", "Bearer wrong")][..]] {
2374            let refused = send(&default, headers).await;
2375            assert_eq!(refused.status(), StatusCode::UNAUTHORIZED);
2376            assert_eq!(challenge(&refused), Some(DEFAULT_STATIC_CHALLENGE));
2377        }
2378
2379        let custom = HttpAuthLayer::builder()
2380            .static_token(STATIC)
2381            .static_challenge(Some(HeaderValue::from_static("ApiKey realm=\"x\"")))
2382            .build()
2383            .unwrap();
2384        assert_eq!(
2385            challenge(&send(&custom, &[]).await),
2386            Some("ApiKey realm=\"x\"")
2387        );
2388
2389        let none = HttpAuthLayer::builder()
2390            .static_token(STATIC)
2391            .static_challenge(None)
2392            .build()
2393            .unwrap();
2394        let refused = send(&none, &[]).await;
2395        assert_eq!(refused.status(), StatusCode::UNAUTHORIZED);
2396        assert_eq!(challenge(&refused), None);
2397    }
2398
2399    #[tokio::test]
2400    async fn every_source_header_is_sensitive_for_the_callback_and_the_service() {
2401        let layer = HttpAuthLayer::builder()
2402            .static_token(STATIC)
2403            .sources([
2404                CredentialSource::authorization_bearer(),
2405                CredentialSource::Raw(HeaderName::from_static("x-api-key")),
2406            ])
2407            .on_reject(|cx: RejectContext<'_>| {
2408                for name in ["authorization", "x-api-key"] {
2409                    assert!(cx.request.headers[name].is_sensitive(), "{name}");
2410                }
2411                assert!(!format!("{cx:?}").contains("wrong"));
2412                Response::new(String::from("refused"))
2413            })
2414            .build()
2415            .unwrap();
2416        // `inner` asserts sensitivity on the accepted path.
2417        let ok = send(
2418            &layer,
2419            &[("authorization", "Bearer wrong"), ("x-api-key", STATIC)],
2420        )
2421        .await;
2422        assert_eq!(ok.status(), StatusCode::OK);
2423        let refused = send(
2424            &layer,
2425            &[("authorization", "Bearer wrong"), ("x-api-key", "wrong")],
2426        )
2427        .await;
2428        assert_eq!(refused.status(), StatusCode::UNAUTHORIZED);
2429        assert_eq!(refused.body(), "refused");
2430    }
2431
2432    #[tokio::test]
2433    async fn on_reject_shapes_the_body_but_not_the_status_or_the_challenge() {
2434        let jwks = testing::spawn_jwks_server("200 OK", testing::jwks_body()).await;
2435        let v = validator(&jwks.url);
2436        let layer = HttpAuthLayer::builder()
2437            .oauth(Arc::clone(&v))
2438            .on_reject(|cx: RejectContext<'_>| {
2439                Response::builder()
2440                    .status(StatusCode::IM_A_TEAPOT)
2441                    .header(WWW_AUTHENTICATE, "Basic realm=\"nope\"")
2442                    .header("content-type", "application/json")
2443                    .body(format!("{{\"status\":{}}}", cx.status.as_u16()))
2444                    .unwrap()
2445            })
2446            .build()
2447            .unwrap();
2448        let bearer = format!("Bearer {}", unscoped_token());
2449        let refused = send(&layer, &[("authorization", &bearer)]).await;
2450        assert_eq!(refused.status(), StatusCode::FORBIDDEN);
2451        let challenges: Vec<_> = refused.headers().get_all(WWW_AUTHENTICATE).iter().collect();
2452        assert_eq!(challenges, [v.insufficient_scope_challenge().as_str()]);
2453        assert_eq!(refused.headers()["content-type"], "application/json");
2454        assert_eq!(refused.body(), "{\"status\":403}");
2455    }
2456
2457    #[tokio::test]
2458    async fn an_optional_layer_passes_only_a_request_presenting_nothing() {
2459        let layer = HttpAuthLayer::builder()
2460            .static_token(STATIC)
2461            .optional()
2462            .build()
2463            .unwrap();
2464        for headers in [
2465            &[][..],
2466            &[("authorization", "Bearer ")][..],
2467            &[("authorization", "Basic x")][..],
2468        ] {
2469            let response = send(&layer, headers).await;
2470            assert_eq!(
2471                (response.status(), response.body().as_str()),
2472                (StatusCode::OK, "anonymous")
2473            );
2474        }
2475        for value in ["Bearer wrong", "DPoP x", "Bearer\tx"] {
2476            let response = send(&layer, &[("authorization", value)]).await;
2477            assert_eq!(response.status(), StatusCode::UNAUTHORIZED, "{value:?}");
2478            assert_eq!(challenge(&response), Some(DEFAULT_STATIC_CHALLENGE));
2479        }
2480    }
2481
2482    #[test]
2483    fn debug_never_prints_the_static_token() {
2484        let builder = HttpAuthLayer::builder().static_token("hunter2");
2485        assert!(!format!("{builder:?}").contains("hunter2"));
2486        let layer = builder.build().unwrap();
2487        let rendered = format!("{layer:?}");
2488        assert!(
2489            !rendered.contains("hunter2") && rendered.contains("<redacted>"),
2490            "{rendered}"
2491        );
2492        let service = tower_layer::Layer::layer(&layer, "inner");
2493        assert!(!format!("{service:?}").contains("hunter2"));
2494    }
2495
2496    // ── static_tokens ────────────────────────────────────────────────────────
2497
2498    fn rotation() -> StaticTokens {
2499        StaticTokens::new()
2500            .with(Some("current"), "key-current")
2501            .and_then(|t| t.with(Some("next"), "key-next"))
2502            .unwrap()
2503    }
2504
2505    /// Status, every header, and a body naming the credential and the static
2506    /// match the layer inserted.
2507    type Seen = (u16, Vec<(String, String)>, String);
2508
2509    async fn seen<R>(layer: &HttpAuthLayer<R>, headers: &[(&str, &str)]) -> Seen
2510    where
2511        R: RefusalResponse<String> + Send + Sync + 'static,
2512    {
2513        let mut request = Request::builder().uri("/test");
2514        for (name, value) in headers {
2515            request = request.header(*name, *value);
2516        }
2517        let service = tower_layer::Layer::layer(
2518            layer,
2519            service_fn(|request: Request<String>| async move {
2520                let body = format!(
2521                    "{:?} {:?}",
2522                    request.extensions().get::<Credential>(),
2523                    request.extensions().get::<StaticTokenMatch>(),
2524                );
2525                Ok::<_, std::convert::Infallible>(Response::new(body))
2526            }),
2527        );
2528        let response = service
2529            .oneshot(request.body(String::new()).unwrap())
2530            .await
2531            .unwrap();
2532        let headers = response
2533            .headers()
2534            .iter()
2535            .map(|(k, v)| (k.to_string(), v.to_str().unwrap().to_string()))
2536            .collect();
2537        (response.status().as_u16(), headers, response.into_body())
2538    }
2539
2540    #[tokio::test]
2541    async fn a_one_entry_set_answers_exactly_like_static_token() {
2542        let jwks = testing::spawn_jwks_server("200 OK", testing::jwks_body()).await;
2543        let v = validator(&jwks.url);
2544        let valid = format!("Bearer {}", testing::valid_token());
2545        let unscoped = format!("Bearer {}", unscoped_token());
2546        let requests: Vec<Vec<(&str, &str)>> = vec![
2547            vec![],
2548            vec![("authorization", "Bearer secret")],
2549            vec![("authorization", "bearer secret")],
2550            vec![("authorization", "Bearer wrong")],
2551            vec![("authorization", "Bearer ")],
2552            vec![("x-api-key", "secret")],
2553            vec![("authorization", "Bearer wrong"), ("x-api-key", "secret")],
2554            vec![("authorization", valid.as_str())],
2555            vec![("authorization", unscoped.as_str())],
2556        ];
2557        for (oauth, optional) in [(None, false), (Some(&v), false), (None, true)] {
2558            let sources = [
2559                CredentialSource::authorization_bearer(),
2560                CredentialSource::Raw(HeaderName::from_static("x-api-key")),
2561            ];
2562            let mut old = HttpAuthLayer::builder()
2563                .static_token(STATIC)
2564                .optional_oauth(oauth.cloned())
2565                .sources(sources.clone());
2566            let mut new = HttpAuthLayer::builder()
2567                .static_tokens(StaticTokens::single(STATIC).unwrap())
2568                .optional_oauth(oauth.cloned())
2569                .sources(sources);
2570            if optional {
2571                old = old.optional();
2572                new = new.optional();
2573            }
2574            let (old, new) = (old.build().unwrap(), new.build().unwrap());
2575            for headers in &requests {
2576                let a = seen(&old, headers).await;
2577                let b = seen(&new, headers).await;
2578                assert_eq!(a, b, "oauth={} {headers:.40?}", oauth.is_some());
2579                if a.2.starts_with("Some(StaticToken)") {
2580                    assert!(
2581                        a.2.ends_with("Some(StaticTokenMatch { label: None })"),
2582                        "{a:?}"
2583                    );
2584                }
2585            }
2586        }
2587    }
2588
2589    #[tokio::test]
2590    async fn every_token_in_a_set_is_accepted_with_its_label() {
2591        let layer = HttpAuthLayer::builder()
2592            .static_tokens(rotation())
2593            .build()
2594            .unwrap();
2595        for (secret, label) in [("key-current", "current"), ("key-next", "next")] {
2596            let (status, _, body) =
2597                seen(&layer, &[("authorization", &format!("Bearer {secret}"))]).await;
2598            assert_eq!(status, 200);
2599            assert_eq!(
2600                body,
2601                format!("Some(StaticToken) Some(StaticTokenMatch {{ label: Some({label:?}) }})")
2602            );
2603        }
2604        let (status, headers, _) = seen(&layer, &[("authorization", "Bearer key-old")]).await;
2605        assert_eq!(status, 401);
2606        assert!(headers.contains(&(
2607            "www-authenticate".to_string(),
2608            DEFAULT_STATIC_CHALLENGE.to_string()
2609        )));
2610    }
2611
2612    #[tokio::test]
2613    async fn static_token_and_static_tokens_are_merged() {
2614        let layer = HttpAuthLayer::builder()
2615            .static_token("key-next")
2616            .static_tokens(rotation())
2617            .build()
2618            .unwrap();
2619        // The secret given both ways counts once, under the set's label.
2620        let (_, _, body) = seen(&layer, &[("authorization", "Bearer key-next")]).await;
2621        assert!(body.ends_with("label: Some(\"next\") })"), "{body}");
2622        let layer = HttpAuthLayer::builder()
2623            .static_token("key-extra")
2624            .static_tokens(rotation())
2625            .build()
2626            .unwrap();
2627        for secret in ["key-current", "key-next", "key-extra"] {
2628            let (status, _, _) =
2629                seen(&layer, &[("authorization", &format!("Bearer {secret}"))]).await;
2630            assert_eq!(status, 200, "{secret}");
2631        }
2632        let (_, _, body) = seen(&layer, &[("authorization", "Bearer key-extra")]).await;
2633        assert!(body.ends_with("label: None })"), "{body}");
2634    }
2635
2636    /// A whitespace-only static token can never match (blank candidates are
2637    /// discarded before any comparison), so it counts as no static token:
2638    /// alone it is `NoCredential` rather than a layer that silently admits
2639    /// nobody; next to OAuth it is dropped.
2640    #[tokio::test]
2641    async fn a_whitespace_static_token_is_no_credential() {
2642        for blank in ["   ", "\t", " \r\n "] {
2643            for builder in [
2644                HttpAuthLayer::builder().static_token(blank),
2645                HttpAuthLayer::builder().static_token(blank).optional(),
2646                HttpAuthLayer::builder()
2647                    .static_token(blank)
2648                    .static_tokens(StaticTokens::new()),
2649            ] {
2650                assert_eq!(
2651                    builder.build().unwrap_err(),
2652                    AuthLayerError::NoCredential,
2653                    "{blank:?}"
2654                );
2655            }
2656            assert_eq!(
2657                HttpAuthLayer::builder()
2658                    .build_with_decision(StaticTokenDecision::StaticOnly(blank.into()))
2659                    .unwrap_err(),
2660                AuthLayerError::NoCredential
2661            );
2662        }
2663        let v = validator("http://127.0.0.1:1/jwks");
2664        let layer = HttpAuthLayer::builder()
2665            .oauth(v)
2666            .static_token("   ")
2667            .build()
2668            .unwrap();
2669        let refused = send(&layer, &[("authorization", "Bearer    ")]).await;
2670        assert_eq!(refused.status(), StatusCode::UNAUTHORIZED);
2671    }
2672
2673    #[test]
2674    fn an_empty_set_is_no_credential() {
2675        for builder in [
2676            HttpAuthLayer::builder().static_tokens(StaticTokens::new()),
2677            HttpAuthLayer::builder()
2678                .static_tokens(StaticTokens::new())
2679                .static_token(""),
2680            HttpAuthLayer::builder()
2681                .static_tokens(StaticTokens::new())
2682                .optional(),
2683            HttpAuthLayer::builder()
2684                .static_tokens(rotation())
2685                .optional_static_tokens(None),
2686        ] {
2687            assert_eq!(builder.build().unwrap_err(), AuthLayerError::NoCredential);
2688        }
2689    }
2690
2691    #[tokio::test]
2692    async fn static_tokens_follow_the_decision() {
2693        let v = validator("http://127.0.0.1:1/jwks");
2694        let accepts = |layer: HttpAuthLayer| async move {
2695            let mut accepted = Vec::new();
2696            for secret in ["key-current", "key-next", "decided"] {
2697                let (status, _, body) =
2698                    seen(&layer, &[("authorization", &format!("Bearer {secret}"))]).await;
2699                if status == 200 {
2700                    accepted.push(format!(
2701                        "{secret}={}",
2702                        body.rsplit("label: ").next().unwrap()
2703                    ));
2704                }
2705            }
2706            accepted
2707        };
2708
2709        // A decision carrying a token keeps the set and merges the token.
2710        for (decision, oauth) in [
2711            (StaticTokenDecision::StaticOnly("decided".into()), None),
2712            (
2713                StaticTokenDecision::StaticAndOAuth("decided".into()),
2714                Some(Arc::clone(&v)),
2715            ),
2716        ] {
2717            let layer = HttpAuthLayer::builder()
2718                .optional_oauth(oauth)
2719                .static_tokens(rotation())
2720                .build_with_decision(decision)
2721                .unwrap();
2722            assert_eq!(
2723                accepts(layer).await,
2724                [
2725                    "key-current=Some(\"current\") })",
2726                    "key-next=Some(\"next\") })",
2727                    "decided=None })"
2728                ]
2729            );
2730        }
2731        // The decision's token already in the set counts once, labeled.
2732        let layer = HttpAuthLayer::builder()
2733            .static_tokens(rotation())
2734            .build_with_decision(StaticTokenDecision::StaticOnly("key-current".into()))
2735            .unwrap();
2736        assert_eq!(
2737            accepts(layer).await,
2738            [
2739                "key-current=Some(\"current\") })",
2740                "key-next=Some(\"next\") })"
2741            ]
2742        );
2743
2744        // `accept_static_bearer: false` drops the set with the token.
2745        let layer = HttpAuthLayer::builder()
2746            .oauth(Arc::clone(&v))
2747            .static_tokens(rotation())
2748            .build_with_decision(StaticTokenDecision::StaticIgnored)
2749            .unwrap();
2750        assert!(accepts(layer).await.is_empty());
2751
2752        // A decision made without any static token cannot speak for a set.
2753        assert_eq!(
2754            HttpAuthLayer::builder()
2755                .oauth(Arc::clone(&v))
2756                .static_tokens(rotation())
2757                .build_with_decision(StaticTokenDecision::OAuthOnly)
2758                .unwrap_err(),
2759            AuthLayerError::DecisionWithoutStaticToken
2760        );
2761        assert_eq!(
2762            HttpAuthLayer::builder()
2763                .static_tokens(rotation())
2764                .build_with_decision(StaticTokenDecision::Unauthenticated)
2765                .unwrap_err(),
2766            AuthLayerError::DecisionWithoutStaticToken
2767        );
2768        // The validator mismatch is reported first.
2769        assert_eq!(
2770            HttpAuthLayer::builder()
2771                .static_tokens(rotation())
2772                .build_with_decision(StaticTokenDecision::OAuthOnly)
2773                .unwrap_err(),
2774            AuthLayerError::DecisionNeedsOAuth
2775        );
2776        // An empty set is no set.
2777        assert!(
2778            HttpAuthLayer::builder()
2779                .static_tokens(StaticTokens::new())
2780                .build_with_decision(StaticTokenDecision::Unauthenticated)
2781                .unwrap()
2782                .allows_unauthenticated()
2783        );
2784        assert!(
2785            HttpAuthLayer::builder()
2786                .oauth(Arc::clone(&v))
2787                .static_tokens(StaticTokens::new())
2788                .build_with_decision(StaticTokenDecision::OAuthOnly)
2789                .is_ok()
2790        );
2791    }
2792
2793    #[tokio::test]
2794    async fn an_optional_layer_with_several_tokens() {
2795        let layer = HttpAuthLayer::builder()
2796            .static_tokens(rotation())
2797            .optional()
2798            .build()
2799            .unwrap();
2800        let (status, _, body) = seen(&layer, &[]).await;
2801        assert_eq!((status, body.as_str()), (200, "None None"));
2802        for (secret, label) in [("key-current", "current"), ("key-next", "next")] {
2803            let (status, _, body) =
2804                seen(&layer, &[("authorization", &format!("Bearer {secret}"))]).await;
2805            assert_eq!(status, 200);
2806            assert!(body.ends_with(&format!("Some({label:?}) }})")), "{body}");
2807        }
2808        let (status, _, _) = seen(&layer, &[("authorization", "Bearer key-old")]).await;
2809        assert_eq!(status, 401);
2810    }
2811
2812    #[test]
2813    fn debug_never_prints_a_token_from_a_set() {
2814        let builder = HttpAuthLayer::builder()
2815            .static_token("hunter2-single")
2816            .static_tokens(
2817                StaticTokens::new()
2818                    .with(Some("current"), "hunter2-current")
2819                    .unwrap(),
2820            );
2821        let rendered = format!("{builder:?}");
2822        assert!(
2823            !rendered.contains("hunter2") && rendered.contains("current"),
2824            "{rendered}"
2825        );
2826        let layer = builder.build().unwrap();
2827        let rendered = format!("{layer:?}");
2828        assert!(
2829            !rendered.contains("hunter2") && rendered.contains("len: 2"),
2830            "{rendered}"
2831        );
2832        let service = tower_layer::Layer::layer(&layer, "inner");
2833        assert!(!format!("{service:?}").contains("hunter2"));
2834    }
2835}