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/// Judge `parts` against a route-level requirement of `required` (every
1049/// scope, all-of). Reads the innermost [`Credential`] a layer accepted — not
1050/// an [`AuthorizedToken`], which an outer layer may have inserted: an OAuth
1051/// token must carry every scope (the same matching as the validator's); a
1052/// static token has none, so it passes only with `static_bypasses`; no
1053/// credential is `Missing`. An empty requirement passes whatever the layer
1054/// let through. Without a layer marker it is always `NoLayer`, whatever the
1055/// requirement: that wiring mistake must surface, never pass.
1056pub(crate) fn judge_scopes(
1057    parts: &Parts,
1058    required: &[String],
1059    static_bypasses: bool,
1060) -> ScopeVerdict {
1061    if parts.extensions.get::<GateRan>().is_none() {
1062        return ScopeVerdict::NoLayer;
1063    }
1064    if required.is_empty() {
1065        return ScopeVerdict::Pass;
1066    }
1067    match parts.extensions.get::<Credential>() {
1068        Some(Credential::OAuth(token)) => {
1069            if missing_scopes(&token.scopes, required.iter().map(String::as_str)).is_empty() {
1070                ScopeVerdict::Pass
1071            } else {
1072                ScopeVerdict::Refuse(TokenRejection::InsufficientScope)
1073            }
1074        }
1075        Some(Credential::StaticToken) if static_bypasses => ScopeVerdict::Pass,
1076        Some(_) => ScopeVerdict::Refuse(TokenRejection::InsufficientScope),
1077        None => ScopeVerdict::Refuse(TokenRejection::Missing),
1078    }
1079}
1080
1081/// The response for a route-level scope refusal (`verdict` from
1082/// [`judge_scopes`]), built exactly as the layer that ran builds its own:
1083/// its `on_reject` body (when the body type matches, else `B::default()`),
1084/// then [`Gate::finish_with`] — the layer's status and challenge, with a 403
1085/// naming `required` on top of the layer's scopes
1086/// ([`Gate::scope_challenge`]). Logs every refusal (target
1087/// `oauth_resource_server::http_layer`); a wiring no request can ever
1088/// satisfy is logged at `error`:
1089///
1090/// - no layer at all: 500, empty body;
1091/// - an `allow_unauthenticated` layer: 401 with
1092///   [`DEFAULT_STATIC_CHALLENGE`], as the axum extractors answer there;
1093/// - an OAuth-less layer asked for scopes: its own 403.
1094pub(crate) fn scope_refusal<B: Default + 'static>(
1095    parts: &Parts,
1096    rejection: &TokenRejection,
1097    required: &[String],
1098    what: &'static str,
1099) -> Response<B> {
1100    let path = parts.uri.path();
1101    let mechanism = Mechanism::of_request(parts.extensions.get::<Credential>(), rejection);
1102    // `Scoped` and the axum extractors are the handler-side callers; the
1103    // others are route layers.
1104    let stage = if matches!(
1105        what,
1106        "Scoped" | "AuthorizedToken" | "Credential" | "StaticTokenMatch"
1107    ) {
1108        Stage::Handler
1109    } else {
1110        Stage::Route
1111    };
1112    let Some(GateRan(gate)) = parts.extensions.get::<GateRan>() else {
1113        error!(
1114            path = %path,
1115            what,
1116            auth.outcome = Outcome::Rejected.as_str(),
1117            auth.mechanism = mechanism.as_str(),
1118            auth.reason = REASON_MISCONFIGURED,
1119            auth.status = 500u16,
1120            "Server misconfiguration: a scope requirement ran on a route no authentication \
1121             layer covers; refusing the request"
1122        );
1123        count_request(stage, Outcome::Rejected, mechanism, REASON_MISCONFIGURED);
1124        let mut response = Response::new(B::default());
1125        *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
1126        return response;
1127    };
1128    let Some(gate) = gate else {
1129        error!(
1130            path = %path,
1131            what,
1132            auth.outcome = Outcome::Rejected.as_str(),
1133            auth.mechanism = mechanism.as_str(),
1134            auth.reason = REASON_MISCONFIGURED,
1135            auth.status = 401u16,
1136            "Server misconfiguration: a scope requirement needs a credential, but its \
1137             authentication layer allows unauthenticated requests; refusing the request"
1138        );
1139        count_request(stage, Outcome::Rejected, mechanism, REASON_MISCONFIGURED);
1140        let mut response = Response::new(B::default());
1141        *response.status_mut() = StatusCode::UNAUTHORIZED;
1142        response.headers_mut().insert(
1143            WWW_AUTHENTICATE,
1144            HeaderValue::from_static(DEFAULT_STATIC_CHALLENGE),
1145        );
1146        return response;
1147    };
1148    let status = observe::status(rejection);
1149    let reason = match (rejection, &gate.oauth) {
1150        (TokenRejection::InsufficientScope, None) => REASON_MISCONFIGURED,
1151        _ => observe::reason(rejection),
1152    };
1153    count_request(stage, Outcome::Rejected, mechanism, reason);
1154    match (rejection, &gate.oauth) {
1155        (TokenRejection::InsufficientScope, None) => error!(
1156            path = %path,
1157            what,
1158            required = ?required,
1159            auth.outcome = Outcome::Rejected.as_str(),
1160            auth.mechanism = mechanism.as_str(),
1161            auth.reason = reason,
1162            auth.status = status,
1163            "Server misconfiguration: the route requires scopes, but its authentication layer \
1164             has no OAuth validator, so no credential can carry them; refusing the request"
1165        ),
1166        (TokenRejection::InsufficientScope, Some(_)) => {
1167            let present = match parts.extensions.get::<Credential>() {
1168                Some(Credential::OAuth(token)) => token.scopes.clone(),
1169                _ => Vec::new(),
1170            };
1171            // Info, as for the validator's own scope refusal: scopes are not
1172            // secret, and `present` next to `required` is the diagnosis.
1173            info!(
1174                path = %path,
1175                what,
1176                required = ?required,
1177                present = ?crate::token::scopes_for_log(&present),
1178                static_token = matches!(parts.extensions.get::<Credential>(), Some(Credential::StaticToken)),
1179                auth.outcome = Outcome::Rejected.as_str(),
1180                auth.mechanism = mechanism.as_str(),
1181                auth.reason = reason,
1182                auth.status = status,
1183                "The credential lacks the scopes this route requires"
1184            );
1185        }
1186        (TokenRejection::Missing, Some(_)) => {
1187            debug!(
1188                path = %path,
1189                what,
1190                auth.outcome = Outcome::Rejected.as_str(),
1191                auth.mechanism = mechanism.as_str(),
1192                auth.reason = reason,
1193                auth.status = status,
1194                "No bearer credential presented"
1195            );
1196        }
1197        _ => warn!(
1198            path = %path,
1199            what,
1200            reason = ?rejection,
1201            auth.outcome = Outcome::Rejected.as_str(),
1202            auth.mechanism = mechanism.as_str(),
1203            auth.reason = reason,
1204            auth.status = status,
1205            "Bearer auth rejected"
1206        ),
1207    }
1208    let insufficient = match rejection {
1209        TokenRejection::InsufficientScope => gate.scope_challenge(required),
1210        _ => None,
1211    };
1212    let (status, _) = gate.status_and_challenge_with(rejection, insufficient.as_ref());
1213    let response = parts
1214        .extensions
1215        .get::<RefusalBody<B>>()
1216        .and_then(|body| {
1217            (body.build)(
1218                &*body.source,
1219                RejectContext {
1220                    rejection,
1221                    status,
1222                    request: parts,
1223                },
1224            )
1225        })
1226        .unwrap_or_else(|| Response::new(B::default()));
1227    gate.finish_with(rejection, insufficient.as_ref(), response)
1228}
1229
1230/// A route-level scope requirement: a `tower::Layer` for the routes (or
1231/// services) that need more than the authentication layer in front of them
1232/// requires — say, a write scope on the routes that write. It adds no key
1233/// cache and no validation of its own: it reads the credential that layer
1234/// accepted from the request's extensions and checks it against its scopes
1235/// (all-of, the same matching as the validator's own check,
1236/// [`AuthorizedToken::require_scopes`]).
1237///
1238/// Place it INSIDE (behind) an authentication layer — the axum `AuthLayer`
1239/// or an [`HttpAuthLayer`], which both mark every request they pass:
1240///
1241/// | The request… | Answer |
1242/// |---|---|
1243/// | carries an OAuth token with every scope | served |
1244/// | 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) |
1245/// | carries a static token | 403 the same way — a static token has no scopes — unless [`static_token_bypasses_scopes`](Self::static_token_bypasses_scopes) |
1246/// | carries no credential (an [`optional`](HttpAuthLayerBuilder::optional) layer passed it through) | the layer's own 401 and challenge |
1247/// | passed an `allow_unauthenticated` layer with no credential | 401 with [`crate::DEFAULT_STATIC_CHALLENGE`], logged at `error` |
1248/// | passed NO authentication layer (mounted outside it) | 500, empty body, logged at `error` — never served |
1249///
1250/// An empty requirement serves every request the layer let through (a 500
1251/// without a layer all the same). Nothing in the credential or the scopes
1252/// reaches the response beyond the challenge; refusals are logged under
1253/// `oauth_resource_server::http_layer` (403 at `info`, with the required and
1254/// present scopes).
1255///
1256/// The credential judged is the innermost [`Credential`] a layer accepted.
1257/// A layer checks static tokens first, so a request presenting both a
1258/// static token and an OAuth token (in two sources) is judged by the static
1259/// token.
1260///
1261/// The same type is `oauth_resource_server::axum::RequireScopes`. Under
1262/// axum, the layer's refusal body comes from its `on_reject`; behind an
1263/// [`HttpAuthLayer`], from its [`on_reject`](HttpAuthLayerBuilder::on_reject)
1264/// when the response body types match, else `ResBody::default()`.
1265///
1266/// # Examples
1267///
1268/// Behind an [`HttpAuthLayer`], on any `tower` stack:
1269///
1270/// ```
1271/// use std::sync::Arc;
1272///
1273/// use http::{Request, Response};
1274/// use oauth_resource_server::OAuthValidator;
1275/// use oauth_resource_server::http_layer::{HttpAuthLayer, RequireScopes};
1276/// use tower::{ServiceBuilder, service_fn};
1277///
1278/// # fn service(oauth: Arc<OAuthValidator>) {
1279/// let writes = ServiceBuilder::new()
1280///     // Outermost first: authenticate, then require the write scope.
1281///     .layer(HttpAuthLayer::builder().oauth(oauth).build().unwrap())
1282///     .layer(RequireScopes::new(["docs:write"]))
1283///     .service(service_fn(|_request: Request<String>| async {
1284///         Ok::<_, std::convert::Infallible>(Response::new(String::from("written")))
1285///     }));
1286/// # let _ = writes;
1287/// # }
1288/// ```
1289///
1290/// Under axum (feature `axum`, as `oauth_resource_server::axum::RequireScopes`),
1291/// `.route_layer(RequireScopes::new(["docs:write"]))` on the routes that
1292/// need it, before (so inside) `.route_layer(auth)`.
1293///
1294/// # Panics
1295///
1296/// [`RequireScopes::new`] panics when a scope is not an RFC 6749 §3.3
1297/// scope-token (empty, or holding a space, `"`, `\`, a control or non-ASCII
1298/// character): no token can carry one, so the route could never be reached.
1299/// Use it for scopes written as literals in code; for scopes read from
1300/// configuration use [`RequireScopes::try_new`], which returns the error
1301/// instead.
1302#[derive(Clone, Debug)]
1303pub struct RequireScopes {
1304    scopes: Arc<[String]>,
1305    static_bypasses: bool,
1306}
1307
1308impl RequireScopes {
1309    /// Require every scope in `scopes` (deduplicated). For literals in code;
1310    /// see [`RequireScopes::try_new`] for scopes from configuration.
1311    ///
1312    /// # Panics
1313    ///
1314    /// When a scope is not an RFC 6749 §3.3 scope-token; see the type's docs.
1315    pub fn new(scopes: impl IntoIterator<Item = impl Into<String>>) -> Self {
1316        Self::try_new(scopes).unwrap_or_else(|e| panic!("RequireScopes::new: {e}"))
1317    }
1318
1319    /// [`RequireScopes::new`] for scopes read from configuration: the same
1320    /// requirement, or the first scope that is not a scope-token.
1321    ///
1322    /// # Errors
1323    ///
1324    /// [`InvalidScope`], naming the offending scope.
1325    ///
1326    /// # Examples
1327    ///
1328    /// ```
1329    /// use oauth_resource_server::http_layer::RequireScopes;
1330    ///
1331    /// assert!(RequireScopes::try_new(["docs:write"]).is_ok());
1332    /// let err = RequireScopes::try_new(["docs:write", "two words"]).unwrap_err();
1333    /// assert_eq!(err.scope(), "two words");
1334    /// ```
1335    pub fn try_new(
1336        scopes: impl IntoIterator<Item = impl Into<String>>,
1337    ) -> Result<Self, InvalidScope> {
1338        Ok(Self {
1339            scopes: checked_scopes(scopes)?.into(),
1340            static_bypasses: false,
1341        })
1342    }
1343
1344    /// Let a static token through instead of refusing it with 403.
1345    ///
1346    /// # Security
1347    ///
1348    /// A static token then reaches these routes whatever they require —
1349    /// the static token is treated as holding every scope. Use it only where
1350    /// the static token is meant to be a full-access key.
1351    pub fn static_token_bypasses_scopes(mut self) -> Self {
1352        self.static_bypasses = true;
1353        self
1354    }
1355
1356    /// The scopes required, deduplicated, in the order given.
1357    pub fn scopes(&self) -> &[String] {
1358        &self.scopes
1359    }
1360}
1361
1362impl<S> tower_layer::Layer<S> for RequireScopes {
1363    type Service = RequireScopesService<S>;
1364
1365    fn layer(&self, inner: S) -> Self::Service {
1366        RequireScopesService {
1367            require: self.clone(),
1368            inner,
1369        }
1370    }
1371}
1372
1373/// The service a [`RequireScopes`] wraps another in.
1374#[derive(Clone, Debug)]
1375pub struct RequireScopesService<S> {
1376    require: RequireScopes,
1377    inner: S,
1378}
1379
1380impl<S, ReqBody, ResBody> tower_service::Service<Request<ReqBody>> for RequireScopesService<S>
1381where
1382    S: tower_service::Service<Request<ReqBody>, Response = Response<ResBody>>
1383        + Clone
1384        + Send
1385        + 'static,
1386    S::Future: Send + 'static,
1387    ReqBody: Send + 'static,
1388    ResBody: Default + 'static,
1389{
1390    type Response = Response<ResBody>;
1391    type Error = S::Error;
1392    type Future =
1393        Pin<Box<dyn Future<Output = Result<Response<ResBody>, S::Error>> + Send + 'static>>;
1394
1395    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
1396        self.inner.poll_ready(cx)
1397    }
1398
1399    fn call(&mut self, request: Request<ReqBody>) -> Self::Future {
1400        let clone = self.inner.clone();
1401        let mut inner = std::mem::replace(&mut self.inner, clone);
1402        let require = self.require.clone();
1403        Box::pin(async move {
1404            // Decided before the inner call and never held across it, so the
1405            // future is `Send` without `ResBody: Send`.
1406            let (parts, body) = request.into_parts();
1407            let verdict = judge_scopes(&parts, &require.scopes, require.static_bypasses);
1408            let rejection = match verdict {
1409                ScopeVerdict::Pass => return inner.call(Request::from_parts(parts, body)).await,
1410                ScopeVerdict::Refuse(rejection) => rejection,
1411                ScopeVerdict::NoLayer => TokenRejection::Missing,
1412            };
1413            Ok(scope_refusal(
1414                &parts,
1415                &rejection,
1416                &require.scopes,
1417                "RequireScopes",
1418            ))
1419        })
1420    }
1421}
1422
1423/// A `tower::Layer` that authenticates every request to the service it wraps,
1424/// for any `http::Request<ReqBody>` / `http::Response<ResBody>` service; see
1425/// the [module docs](self). Cheap to clone (two `Arc`s).
1426///
1427/// `R` is what builds a refusal's response: [`EmptyRefusal`] unless
1428/// [`HttpAuthLayerBuilder::on_reject`] set a callback.
1429///
1430/// Built once at startup. Nothing in it hot-reloads: a changed static token or
1431/// OAuth config takes effect when a new layer is built, which in practice means
1432/// a restart.
1433pub struct HttpAuthLayer<R = EmptyRefusal> {
1434    mode: Arc<HttpMode>,
1435    on_reject: Arc<R>,
1436}
1437
1438enum HttpMode {
1439    Enforce(Arc<Gate>),
1440    AllowUnauthenticated,
1441}
1442
1443impl<R> Clone for HttpAuthLayer<R> {
1444    fn clone(&self) -> Self {
1445        Self {
1446            mode: Arc::clone(&self.mode),
1447            on_reject: Arc::clone(&self.on_reject),
1448        }
1449    }
1450}
1451
1452/// Hand-written so the static token never reaches a log line through `{:?}`.
1453impl<R> std::fmt::Debug for HttpAuthLayer<R> {
1454    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1455        match &*self.mode {
1456            HttpMode::AllowUnauthenticated => f
1457                .debug_struct("HttpAuthLayer")
1458                .field("allow_unauthenticated", &true)
1459                .finish(),
1460            HttpMode::Enforce(g) => f
1461                .debug_struct("HttpAuthLayer")
1462                .field("static_tokens", &g.static_tokens)
1463                .field("oauth", &g.oauth)
1464                .field("sources", &g.sources)
1465                .field("on_reject", &std::any::type_name::<R>())
1466                .field("static_challenge", &g.static_challenge)
1467                .field("optional", &g.optional)
1468                .finish(),
1469        }
1470    }
1471}
1472
1473impl HttpAuthLayer {
1474    /// Start building an enforcing layer. Give it a static token, an OAuth
1475    /// validator, or both; optionally the credential sources (default:
1476    /// `Authorization: Bearer`), the refusal body
1477    /// ([`on_reject`](HttpAuthLayerBuilder::on_reject)), and
1478    /// [`optional`](HttpAuthLayerBuilder::optional). To honour
1479    /// `accept_static_bearer`, finish with
1480    /// [`build_with_decision`](HttpAuthLayerBuilder::build_with_decision) and a
1481    /// [`crate::static_token_policy`] decision.
1482    ///
1483    /// # Examples
1484    ///
1485    /// ```
1486    /// use http::HeaderName;
1487    /// use oauth_resource_server::http_layer::{AuthLayerError, CredentialSource, HttpAuthLayer};
1488    ///
1489    /// let auth = HttpAuthLayer::builder()
1490    ///     .static_token("example-static-key")
1491    ///     .sources([
1492    ///         CredentialSource::authorization_bearer(),
1493    ///         CredentialSource::Raw(HeaderName::from_static("x-api-key")),
1494    ///     ])
1495    ///     .build()
1496    ///     .unwrap();
1497    /// # let _ = auth;
1498    ///
1499    /// // Fail closed: no credential configured is an error, not a pass-through.
1500    /// assert_eq!(
1501    ///     HttpAuthLayer::builder().build().unwrap_err(),
1502    ///     AuthLayerError::NoCredential
1503    /// );
1504    /// ```
1505    pub fn builder() -> HttpAuthLayerBuilder {
1506        HttpAuthLayerBuilder::default()
1507    }
1508
1509    /// A layer that lets EVERY request through, unauthenticated, and inserts no
1510    /// credential into request extensions. The explicit opt-out; the only
1511    /// other way to get it is a [`StaticTokenDecision::Unauthenticated`] handed
1512    /// to [`HttpAuthLayer::from_decision`] or
1513    /// [`HttpAuthLayerBuilder::build_with_decision`].
1514    ///
1515    /// # Security
1516    ///
1517    /// Every request reaches the wrapped service. Use it only where something
1518    /// else (a trusted network, a proxy that authenticates) stands in front.
1519    /// It still marks every `Authorization` header value sensitive
1520    /// (`http::HeaderValue::set_sensitive`), so a credential a client sends
1521    /// anyway is not printed by a `Debug` of the request downstream.
1522    pub fn allow_unauthenticated() -> Self {
1523        Self {
1524            mode: Arc::new(HttpMode::AllowUnauthenticated),
1525            on_reject: Arc::new(EmptyRefusal),
1526        }
1527    }
1528
1529    /// The layer a [`crate::static_token_policy`] decision calls for, with the
1530    /// default source and refusal body. Shorthand for
1531    /// `HttpAuthLayer::builder().optional_oauth(oauth).build_with_decision(decision)`.
1532    ///
1533    /// # Errors
1534    ///
1535    /// See [`HttpAuthLayerBuilder::build_with_decision`].
1536    pub fn from_decision(
1537        decision: StaticTokenDecision,
1538        oauth: Option<Arc<OAuthValidator>>,
1539    ) -> Result<Self, AuthLayerError> {
1540        Self::builder()
1541            .optional_oauth(oauth)
1542            .build_with_decision(decision)
1543    }
1544}
1545
1546impl<R> HttpAuthLayer<R> {
1547    /// Whether this is the [`HttpAuthLayer::allow_unauthenticated`] pass-through.
1548    pub fn allows_unauthenticated(&self) -> bool {
1549        matches!(*self.mode, HttpMode::AllowUnauthenticated)
1550    }
1551
1552    /// The OAuth validator, when one is configured.
1553    pub fn oauth(&self) -> Option<&Arc<OAuthValidator>> {
1554        match &*self.mode {
1555            HttpMode::Enforce(g) => g.oauth.as_ref(),
1556            HttpMode::AllowUnauthenticated => None,
1557        }
1558    }
1559
1560    /// Authenticate `request`: the request to pass on (with the credential in
1561    /// its extensions), or the refusal to answer with.
1562    async fn check<ReqBody, ResBody>(
1563        &self,
1564        request: Request<ReqBody>,
1565    ) -> Result<Request<ReqBody>, Response<ResBody>>
1566    where
1567        R: RefusalResponse<ResBody> + Send + Sync + 'static,
1568        ResBody: 'static,
1569    {
1570        let gate = match &*self.mode {
1571            HttpMode::AllowUnauthenticated => {
1572                count_request(
1573                    Stage::Layer,
1574                    Outcome::PassedThrough,
1575                    Mechanism::None,
1576                    REASON_NONE,
1577                );
1578                let mut request = request;
1579                mark_authorization_sensitive(request.headers_mut());
1580                self.mark::<ResBody>(request.extensions_mut(), None);
1581                return Ok(request);
1582            }
1583            HttpMode::Enforce(gate) => gate,
1584        };
1585        let (mut parts, body) = request.into_parts();
1586        match gate.admit(&mut parts).await {
1587            Admission::Static => log_static_accepted!(&parts),
1588            Admission::OAuth(token) => log_oauth_accepted!(&parts, &token),
1589            Admission::PassedThrough => log_passed_through!(&parts),
1590            Admission::Refused(rejection, mechanism) => {
1591                log_layer_refusal!(gate, &parts, &rejection, mechanism, Stage::Layer);
1592                let (status, _) = gate.status_and_challenge(&rejection);
1593                let response = self.on_reject.refusal_response(RejectContext {
1594                    rejection: &rejection,
1595                    status,
1596                    request: &parts,
1597                });
1598                return Err(gate.finish(&rejection, response));
1599            }
1600        }
1601        self.mark::<ResBody>(&mut parts.extensions, Some(Arc::clone(gate)));
1602        Ok(Request::from_parts(parts, body))
1603    }
1604
1605    /// Insert the markers a route-level scope check behind this layer reads
1606    /// ([`GateRan`], and this layer's refusal builder for `ResBody`).
1607    fn mark<ResBody>(&self, extensions: &mut http::Extensions, gate: Option<Arc<Gate>>)
1608    where
1609        R: RefusalResponse<ResBody> + Send + Sync + 'static,
1610        ResBody: 'static,
1611    {
1612        // An `allow_unauthenticated` layer inside an enforcing one leaves the
1613        // outer marker in place: the credential a route check behind both
1614        // judges is the outer layer's, so its refusal must be too (a 403
1615        // step-up with the outer challenge, never the open layer's 401).
1616        if gate.is_none() && extensions.get::<GateRan>().is_some() {
1617            return;
1618        }
1619        extensions.insert(GateRan(gate));
1620        extensions.insert(RefusalBody::<ResBody> {
1621            source: Arc::clone(&self.on_reject) as Arc<dyn std::any::Any + Send + Sync>,
1622            build: http_refusal_body::<R, ResBody>,
1623        });
1624    }
1625}
1626
1627/// Builder for an enforcing [`HttpAuthLayer`]; see [`HttpAuthLayer::builder`].
1628pub struct HttpAuthLayerBuilder<R = EmptyRefusal> {
1629    static_token: Option<Zeroizing<String>>,
1630    static_tokens: Option<StaticTokens>,
1631    oauth: Option<Arc<OAuthValidator>>,
1632    sources: Option<Vec<CredentialSource>>,
1633    /// `None`: not set, so [`DEFAULT_STATIC_CHALLENGE`].
1634    static_challenge: Option<Option<HeaderValue>>,
1635    optional: bool,
1636    required_scopes: Vec<String>,
1637    static_bypasses_scopes: bool,
1638    on_reject: R,
1639}
1640
1641impl Default for HttpAuthLayerBuilder {
1642    fn default() -> Self {
1643        Self {
1644            static_token: None,
1645            static_tokens: None,
1646            oauth: None,
1647            sources: None,
1648            static_challenge: None,
1649            optional: false,
1650            required_scopes: Vec::new(),
1651            static_bypasses_scopes: false,
1652            on_reject: EmptyRefusal,
1653        }
1654    }
1655}
1656
1657/// Hand-written so the static token never reaches a log line through `{:?}`.
1658impl<R> std::fmt::Debug for HttpAuthLayerBuilder<R> {
1659    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1660        f.debug_struct("HttpAuthLayerBuilder")
1661            .field(
1662                "static_token",
1663                &self.static_token.as_ref().map(|_| "<redacted>"),
1664            )
1665            .field("static_tokens", &self.static_tokens)
1666            .field("oauth", &self.oauth)
1667            .field("sources", &self.sources)
1668            .field("on_reject", &std::any::type_name::<R>())
1669            .field("static_challenge", &self.static_challenge)
1670            .field("optional", &self.optional)
1671            .field("required_scopes", &self.required_scopes)
1672            .field("static_bypasses_scopes", &self.static_bypasses_scopes)
1673            .finish()
1674    }
1675}
1676
1677impl<R> HttpAuthLayerBuilder<R> {
1678    /// Accept this static token (compared in constant time). An empty string
1679    /// counts as no token.
1680    ///
1681    /// # Security
1682    ///
1683    /// Setting the token here bypasses `accept_static_bearer`, which only
1684    /// [`crate::static_token_policy`] reads; with OAuth configured, prefer
1685    /// [`HttpAuthLayerBuilder::build_with_decision`]. The token's length is not
1686    /// hidden by the comparison, and `Debug` output shows it as `<redacted>`.
1687    pub fn static_token(mut self, token: impl Into<String>) -> Self {
1688        self.static_token = Some(Zeroizing::new(token.into()));
1689        self
1690    }
1691
1692    /// [`HttpAuthLayerBuilder::static_token`] when `Some`.
1693    pub fn optional_static_token(mut self, token: Option<String>) -> Self {
1694        self.static_token = token.map(Zeroizing::new);
1695        self
1696    }
1697
1698    /// Accept every token in `tokens` (each compared in constant time, every
1699    /// entry every time; see [`StaticTokens`]), replacing a set given
1700    /// earlier. On a match the request's extensions get
1701    /// [`Credential::StaticToken`] and the [`StaticTokenMatch`] naming the
1702    /// entry's label.
1703    ///
1704    /// Combines with the other static-token settings exactly as the axum
1705    /// layer's `AuthLayerBuilder::static_tokens` does: with
1706    /// [`static_token`](Self::static_token), both are accepted (a secret in
1707    /// both counts once, under the set's label); with
1708    /// [`build_with_decision`](Self::build_with_decision), see that method.
1709    /// An empty set counts as no static token, so it does not satisfy the
1710    /// fail-closed build on its own.
1711    ///
1712    /// # Security
1713    ///
1714    /// Like [`static_token`](Self::static_token), this bypasses
1715    /// `accept_static_bearer` unless the layer is built with
1716    /// [`build_with_decision`](Self::build_with_decision). `Debug` output
1717    /// shows the count and labels, never a secret.
1718    ///
1719    /// # Examples
1720    ///
1721    /// ```
1722    /// use oauth_resource_server::StaticTokens;
1723    /// use oauth_resource_server::http_layer::HttpAuthLayer;
1724    ///
1725    /// let tokens = StaticTokens::new()
1726    ///     .with(Some("current"), "example-key-old")
1727    ///     .and_then(|t| t.with(Some("next"), "example-key-new"))
1728    ///     .unwrap();
1729    /// let auth = HttpAuthLayer::builder().static_tokens(tokens).build().unwrap();
1730    /// # let _ = auth;
1731    /// ```
1732    pub fn static_tokens(mut self, tokens: StaticTokens) -> Self {
1733        self.static_tokens = Some(tokens);
1734        self
1735    }
1736
1737    /// [`HttpAuthLayerBuilder::static_tokens`] when `Some`; `None` clears a
1738    /// set given earlier.
1739    pub fn optional_static_tokens(mut self, tokens: Option<StaticTokens>) -> Self {
1740        self.static_tokens = tokens;
1741        self
1742    }
1743
1744    /// Accept OAuth access tokens this validator accepts.
1745    pub fn oauth(mut self, validator: Arc<OAuthValidator>) -> Self {
1746        self.oauth = Some(validator);
1747        self
1748    }
1749
1750    /// [`HttpAuthLayerBuilder::oauth`] when `Some`.
1751    pub fn optional_oauth(mut self, validator: Option<Arc<OAuthValidator>>) -> Self {
1752        self.oauth = validator;
1753        self
1754    }
1755
1756    /// Where to read credentials from, replacing the default
1757    /// `[CredentialSource::authorization_bearer()]`. Every source is checked,
1758    /// whatever the others hold.
1759    pub fn sources(mut self, sources: impl IntoIterator<Item = CredentialSource>) -> Self {
1760        self.sources = Some(sources.into_iter().collect());
1761        self
1762    }
1763
1764    /// The `WWW-Authenticate` challenge every 401 carries when NO OAuth
1765    /// validator is configured; with one, the validator's challenges are used
1766    /// and this is ignored. Default: [`crate::DEFAULT_STATIC_CHALLENGE`].
1767    ///
1768    /// `None` sends no challenge at all and leaves any `WWW-Authenticate` an
1769    /// [`on_reject`](Self::on_reject) callback set untouched. That departs from
1770    /// RFC 9110 §15.5.2 (a 401 MUST carry a challenge); use it only to keep an
1771    /// existing API's responses unchanged.
1772    pub fn static_challenge(mut self, challenge: Option<HeaderValue>) -> Self {
1773        self.static_challenge = Some(challenge);
1774        self
1775    }
1776
1777    /// Let a request that presents NO credential through, unauthenticated, with
1778    /// nothing inserted into its extensions; a credential that is presented but
1779    /// refused is refused exactly as without this. "No credential" is decided
1780    /// exactly as by the axum layer's `optional()` (see its documentation): every
1781    /// value of every configured source header absent or blank, where a value
1782    /// that is not visible ASCII, a non-blank later value of a repeated header,
1783    /// a `DPoP`-scheme value and a tab-separated `Bearer` token all count as
1784    /// presented. Any [`Credential`]/[`AuthorizedToken`] an outer layer
1785    /// inserted is removed first.
1786    ///
1787    /// # Security
1788    ///
1789    /// Not a way around the fail-closed build: [`build`](Self::build) still
1790    /// requires a static token or an OAuth validator. Every handler behind an
1791    /// optional layer must treat a request with no [`Credential`] in its
1792    /// extensions as unauthenticated.
1793    pub fn optional(mut self) -> Self {
1794        self.optional = true;
1795        self
1796    }
1797
1798    /// Require every scope in `scopes` (all-of) of every credential this
1799    /// layer accepts, on top of the validator's own `required_scopes` —
1800    /// replacing scopes given earlier. The same validator, so the same key
1801    /// cache: no second validator is needed for routes that need more.
1802    ///
1803    /// Behaves exactly as the axum layer's `AuthLayerBuilder::require_scopes`:
1804    /// an OAuth token missing one is refused with 403 and a challenge naming
1805    /// the validator's required scopes followed by these (see
1806    /// [`crate::refusal_for_scopes`]); a static token — it has no scopes — is
1807    /// refused the same way unless
1808    /// [`static_token_bypasses_scopes`](Self::static_token_bypasses_scopes);
1809    /// an [`optional`](Self::optional) layer still passes a request that
1810    /// presents nothing. A layer that lets everything through
1811    /// ([`HttpAuthLayer::allow_unauthenticated`]) checks nothing, this
1812    /// included, which is why [`build_with_decision`](Self::build_with_decision)
1813    /// refuses an `Unauthenticated` decision on a builder with scopes
1814    /// ([`AuthLayerError::ScopesWithoutAuthentication`]) rather than drop them.
1815    ///
1816    /// For a requirement on some routes only, put a [`RequireScopes`] layer
1817    /// on them instead, behind this one.
1818    ///
1819    /// # Examples
1820    ///
1821    /// ```no_run
1822    /// use std::sync::Arc;
1823    ///
1824    /// use oauth_resource_server::OAuthValidator;
1825    /// use oauth_resource_server::http_layer::HttpAuthLayer;
1826    ///
1827    /// # fn layers(oauth: Arc<OAuthValidator>) {
1828    /// let writes = HttpAuthLayer::builder()
1829    ///     .oauth(oauth)
1830    ///     .require_scopes(["docs:write"])
1831    ///     .build()
1832    ///     .unwrap();
1833    /// # let _ = writes;
1834    /// # }
1835    /// ```
1836    pub fn require_scopes(mut self, scopes: impl IntoIterator<Item = impl Into<String>>) -> Self {
1837        self.required_scopes = scopes.into_iter().map(Into::into).collect();
1838        self
1839    }
1840
1841    /// Let a static token pass [`require_scopes`](Self::require_scopes)
1842    /// instead of refusing it with 403: the static token counts as holding
1843    /// every scope. Without `require_scopes` it changes nothing.
1844    ///
1845    /// # Security
1846    ///
1847    /// Opt in only where the static token is meant to be a full-access key.
1848    pub fn static_token_bypasses_scopes(mut self) -> Self {
1849        self.static_bypasses_scopes = true;
1850        self
1851    }
1852
1853    /// Build a refusal's response (its body and any extra headers, such as
1854    /// `Content-Type`) — for an API whose errors are, say, JSON. Without it a
1855    /// refusal's body is `ResBody::default()`.
1856    ///
1857    /// The callback shapes the response only; it cannot change the outcome.
1858    /// Whatever it returns, the status is set to [`RejectContext::status`] and
1859    /// `WWW-Authenticate` to the layer's challenge, replacing any the callback
1860    /// set (only with `static_challenge(None)` and no OAuth are the callback's
1861    /// headers left as they are). Never put [`TokenRejection::Invalid`]'s
1862    /// reason in the body.
1863    ///
1864    /// # Examples
1865    ///
1866    /// ```
1867    /// use http::{Response, header::CONTENT_TYPE};
1868    /// use oauth_resource_server::http_layer::{HttpAuthLayer, RejectContext};
1869    ///
1870    /// let auth = HttpAuthLayer::builder()
1871    ///     .static_token("example-static-key")
1872    ///     .on_reject(|cx: RejectContext<'_>| {
1873    ///         Response::builder()
1874    ///             .header(CONTENT_TYPE, "application/json")
1875    ///             .body(format!(r#"{{"error":"{}"}}"#, cx.status.as_u16()))
1876    ///             .unwrap()
1877    ///     })
1878    ///     .build()
1879    ///     .unwrap();
1880    /// # let _ = auth;
1881    /// ```
1882    pub fn on_reject<B, F>(self, f: F) -> HttpAuthLayerBuilder<F>
1883    where
1884        F: Fn(RejectContext<'_>) -> Response<B> + Send + Sync + 'static,
1885    {
1886        HttpAuthLayerBuilder {
1887            static_token: self.static_token,
1888            static_tokens: self.static_tokens,
1889            oauth: self.oauth,
1890            sources: self.sources,
1891            static_challenge: self.static_challenge,
1892            optional: self.optional,
1893            required_scopes: self.required_scopes,
1894            static_bypasses_scopes: self.static_bypasses_scopes,
1895            on_reject: f,
1896        }
1897    }
1898
1899    /// Build the layer a [`crate::static_token_policy`] decision calls for,
1900    /// keeping this builder's other settings. The decision's static token (if
1901    /// any) replaces one set on this builder;
1902    /// [`StaticTokenDecision::Unauthenticated`] yields the
1903    /// [`allow_unauthenticated`](HttpAuthLayer::allow_unauthenticated)
1904    /// pass-through.
1905    ///
1906    /// A [`static_tokens`](Self::static_tokens) set follows the decision, as
1907    /// for the axum layer's `AuthLayerBuilder::build_with_decision`: kept, and
1908    /// merged with the decision's token, when the decision carries one
1909    /// (`StaticOnly`/`StaticAndOAuth`); dropped with it on `StaticIgnored`
1910    /// (`accept_static_bearer: false` wins); refused alongside `OAuthOnly` or
1911    /// `Unauthenticated`, which were decided without any static token.
1912    ///
1913    /// # Errors
1914    ///
1915    /// [`AuthLayerError::DecisionNeedsOAuth`] when the decision was made with
1916    /// OAuth on and no validator was given,
1917    /// [`AuthLayerError::DecisionWithoutOAuth`] when it was made with OAuth off
1918    /// (including `Unauthenticated`) and one was given, then
1919    /// [`AuthLayerError::DecisionWithoutStaticToken`] for a non-empty
1920    /// `static_tokens` set with an `OAuthOnly` or `Unauthenticated` decision,
1921    /// then [`AuthLayerError::ScopesWithoutAuthentication`] for
1922    /// [`require_scopes`](Self::require_scopes) with an `Unauthenticated`
1923    /// decision. Otherwise as [`HttpAuthLayerBuilder::build`].
1924    pub fn build_with_decision(
1925        mut self,
1926        decision: StaticTokenDecision,
1927    ) -> Result<HttpAuthLayer<R>, AuthLayerError> {
1928        Gate::check_decision(&decision, self.oauth.is_some())?;
1929        let unauthenticated = decision == StaticTokenDecision::Unauthenticated;
1930        let (token, tokens) = Gate::decision_tokens(decision, self.static_tokens.take())?;
1931        if unauthenticated && !self.required_scopes.is_empty() {
1932            return Err(AuthLayerError::ScopesWithoutAuthentication);
1933        }
1934        if unauthenticated {
1935            return Ok(HttpAuthLayer {
1936                mode: Arc::new(HttpMode::AllowUnauthenticated),
1937                on_reject: Arc::new(self.on_reject),
1938            });
1939        }
1940        self.static_token = token;
1941        self.static_tokens = tokens;
1942        self.build()
1943    }
1944
1945    /// Build the layer.
1946    ///
1947    /// # Errors
1948    ///
1949    /// [`AuthLayerError::NoCredential`] with neither a non-blank static token
1950    /// (from [`static_token`](Self::static_token) or a non-empty
1951    /// [`static_tokens`](Self::static_tokens) set) nor an OAuth validator;
1952    /// [`AuthLayerError::NoSources`] with an empty
1953    /// source list; [`AuthLayerError::InvalidChallenge`] when the validator's
1954    /// challenge is not a valid header value (only reachable from a
1955    /// hand-edited resolved config); [`AuthLayerError::InvalidScope`] when a
1956    /// [`require_scopes`](Self::require_scopes) entry is not a scope-token;
1957    /// [`AuthLayerError::ScopesNeedOAuth`] for `require_scopes` with no OAuth
1958    /// validator and no
1959    /// [`static_token_bypasses_scopes`](Self::static_token_bypasses_scopes).
1960    pub fn build(self) -> Result<HttpAuthLayer<R>, AuthLayerError> {
1961        let gate = Gate::build(
1962            self.static_token,
1963            self.static_tokens,
1964            self.oauth,
1965            self.sources,
1966            self.static_challenge,
1967            self.optional,
1968            self.required_scopes,
1969            self.static_bypasses_scopes,
1970        )?;
1971        Ok(HttpAuthLayer {
1972            mode: Arc::new(HttpMode::Enforce(Arc::new(gate))),
1973            on_reject: Arc::new(self.on_reject),
1974        })
1975    }
1976}
1977
1978impl<S, R> tower_layer::Layer<S> for HttpAuthLayer<R> {
1979    type Service = HttpAuthService<S, R>;
1980
1981    fn layer(&self, inner: S) -> Self::Service {
1982        HttpAuthService {
1983            layer: self.clone(),
1984            inner,
1985        }
1986    }
1987}
1988
1989/// The service an [`HttpAuthLayer`] wraps another in.
1990pub struct HttpAuthService<S, R = EmptyRefusal> {
1991    layer: HttpAuthLayer<R>,
1992    inner: S,
1993}
1994
1995impl<S: Clone, R> Clone for HttpAuthService<S, R> {
1996    fn clone(&self) -> Self {
1997        Self {
1998            layer: self.layer.clone(),
1999            inner: self.inner.clone(),
2000        }
2001    }
2002}
2003
2004impl<S: std::fmt::Debug, R> std::fmt::Debug for HttpAuthService<S, R> {
2005    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2006        f.debug_struct("HttpAuthService")
2007            .field("layer", &self.layer)
2008            .field("inner", &self.inner)
2009            .finish()
2010    }
2011}
2012
2013impl<S, R, ReqBody, ResBody> tower_service::Service<Request<ReqBody>> for HttpAuthService<S, R>
2014where
2015    S: tower_service::Service<Request<ReqBody>, Response = Response<ResBody>>
2016        + Clone
2017        + Send
2018        + 'static,
2019    S::Future: Send + 'static,
2020    R: RefusalResponse<ResBody> + Send + Sync + 'static,
2021    ReqBody: Send + 'static,
2022    ResBody: 'static,
2023{
2024    type Response = Response<ResBody>;
2025    type Error = S::Error;
2026    type Future =
2027        Pin<Box<dyn Future<Output = Result<Response<ResBody>, S::Error>> + Send + 'static>>;
2028
2029    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
2030        self.inner.poll_ready(cx)
2031    }
2032
2033    fn call(&mut self, request: Request<ReqBody>) -> Self::Future {
2034        // Call the instance `poll_ready` was driven on and leave a fresh clone in
2035        // its place (the usual tower pattern for a service moved into a future).
2036        let clone = self.inner.clone();
2037        let mut inner = std::mem::replace(&mut self.inner, clone);
2038        let layer = self.layer.clone();
2039        Box::pin(async move {
2040            // Bound first, so no `ResBody` is held across the inner call and
2041            // the future is `Send` without requiring `ResBody: Send`.
2042            let request = match layer.check(request).await {
2043                Ok(request) => request,
2044                Err(refusal) => return Ok(refusal),
2045            };
2046            inner.call(request).await
2047        })
2048    }
2049}
2050
2051#[cfg(test)]
2052mod tests {
2053    use ::tower::{ServiceExt, service_fn};
2054
2055    use super::*;
2056    use crate::testing;
2057
2058    const STATIC: &str = "secret";
2059
2060    fn validator(jwks_uri: &str) -> Arc<OAuthValidator> {
2061        Arc::new(OAuthValidator::new(&testing::resolved_config(jwks_uri)).unwrap())
2062    }
2063
2064    fn unscoped_token() -> String {
2065        testing::mint(
2066            testing::KEY_A_PEM,
2067            testing::KID_A,
2068            &serde_json::json!({
2069                "iss": testing::ISSUER, "aud": testing::AUDIENCE,
2070                "exp": testing::now() + 3600, "scope": "openid profile",
2071            }),
2072        )
2073    }
2074
2075    fn expired_token() -> String {
2076        testing::mint(
2077            testing::KEY_A_PEM,
2078            testing::KID_A,
2079            &serde_json::json!({
2080                "iss": testing::ISSUER, "aud": testing::AUDIENCE,
2081                "exp": testing::now() - 3600, "scope": "mcp:read",
2082            }),
2083        )
2084    }
2085
2086    /// A plain (non-axum) inner service over `String` bodies: 200 with a
2087    /// description of what the layer inserted, after asserting every
2088    /// credential header reached it marked sensitive.
2089    async fn inner(request: Request<String>) -> Result<Response<String>, std::convert::Infallible> {
2090        for name in ["authorization", "x-api-key"] {
2091            for value in request.headers().get_all(name) {
2092                assert!(
2093                    value.is_sensitive(),
2094                    "{name} must reach the service sensitive"
2095                );
2096            }
2097        }
2098        let token = request.extensions().get::<AuthorizedToken>();
2099        let credential = request.extensions().get::<Credential>();
2100        let body = match (credential, token) {
2101            (Some(Credential::OAuth(_)), Some(t)) => format!("oauth {:?}", t.subject),
2102            (Some(Credential::StaticToken), None) => "static".to_string(),
2103            (None, None) => "anonymous".to_string(),
2104            other => panic!("unexpected extensions: {other:?}"),
2105        };
2106        Ok(Response::new(body))
2107    }
2108
2109    async fn send<R>(layer: &HttpAuthLayer<R>, headers: &[(&str, &str)]) -> Response<String>
2110    where
2111        R: RefusalResponse<String> + Send + Sync + 'static,
2112    {
2113        let mut request = Request::builder().uri("/test");
2114        for (name, value) in headers {
2115            request = request.header(*name, *value);
2116        }
2117        let service = tower_layer::Layer::layer(layer, service_fn(inner));
2118        service
2119            .oneshot(request.body(String::new()).unwrap())
2120            .await
2121            .unwrap()
2122    }
2123
2124    fn challenge<B>(response: &Response<B>) -> Option<&str> {
2125        response
2126            .headers()
2127            .get(WWW_AUTHENTICATE)
2128            .map(|v| v.to_str().unwrap())
2129    }
2130
2131    #[test]
2132    fn the_builder_fails_closed() {
2133        assert_eq!(
2134            HttpAuthLayer::builder().build().unwrap_err(),
2135            AuthLayerError::NoCredential
2136        );
2137        assert_eq!(
2138            HttpAuthLayer::builder()
2139                .static_token("")
2140                .build()
2141                .unwrap_err(),
2142            AuthLayerError::NoCredential
2143        );
2144        assert_eq!(
2145            HttpAuthLayer::builder().optional().build().unwrap_err(),
2146            AuthLayerError::NoCredential
2147        );
2148        assert_eq!(
2149            HttpAuthLayer::builder()
2150                .static_token(STATIC)
2151                .sources([])
2152                .build()
2153                .unwrap_err(),
2154            AuthLayerError::NoSources
2155        );
2156        assert_eq!(
2157            HttpAuthLayer::builder()
2158                .build_with_decision(StaticTokenDecision::OAuthOnly)
2159                .unwrap_err(),
2160            AuthLayerError::DecisionNeedsOAuth
2161        );
2162        let built = HttpAuthLayer::builder()
2163            .static_token(STATIC)
2164            .optional()
2165            .build()
2166            .unwrap();
2167        assert!(!built.allows_unauthenticated() && built.oauth().is_none());
2168        assert!(
2169            HttpAuthLayer::builder()
2170                .build_with_decision(StaticTokenDecision::Unauthenticated)
2171                .unwrap()
2172                .allows_unauthenticated()
2173        );
2174    }
2175
2176    #[test]
2177    fn a_validator_on_its_fallback_challenges_fails_the_build_as_for_axum() {
2178        let mut cfg = testing::resolved_config("http://127.0.0.1:1/jwks");
2179        cfg.resource = "https://api.example.test/v1\r\nX-Injected: 1".into();
2180        let v = Arc::new(OAuthValidator::new(&cfg).unwrap());
2181        assert_eq!(
2182            HttpAuthLayer::builder()
2183                .static_token(STATIC)
2184                .oauth(Arc::clone(&v))
2185                .build()
2186                .unwrap_err(),
2187            AuthLayerError::InvalidChallenge
2188        );
2189        #[cfg(feature = "axum")]
2190        assert_eq!(
2191            crate::axum::AuthLayer::builder()
2192                .oauth(v)
2193                .build()
2194                .unwrap_err(),
2195            AuthLayerError::InvalidChallenge
2196        );
2197    }
2198
2199    #[tokio::test]
2200    async fn only_the_explicit_opt_out_passes_everything() {
2201        // Like the axum layer's pass-through, it reads no header, but it marks
2202        // `Authorization` sensitive: a client may send one anyway.
2203        let layer = HttpAuthLayer::allow_unauthenticated();
2204        let service = tower_layer::Layer::layer(
2205            &layer,
2206            service_fn(|request: Request<String>| async move {
2207                let inserted = request.extensions().get::<Credential>().is_some()
2208                    || request.extensions().get::<AuthorizedToken>().is_some();
2209                assert!(!inserted, "a pass-through inserts nothing");
2210                for value in request.headers().get_all("authorization") {
2211                    assert!(value.is_sensitive(), "Authorization must be sensitive");
2212                }
2213                Ok::<_, std::convert::Infallible>(Response::new(String::from("anonymous")))
2214            }),
2215        );
2216        for authorization in [None, Some("Bearer junk")] {
2217            let mut request = Request::builder().uri("/test");
2218            if let Some(value) = authorization {
2219                request = request.header("authorization", value);
2220            }
2221            let response = service
2222                .clone()
2223                .oneshot(request.body(String::new()).unwrap())
2224                .await
2225                .unwrap();
2226            assert_eq!(response.status(), StatusCode::OK);
2227            assert_eq!(response.body(), "anonymous");
2228        }
2229    }
2230
2231    #[tokio::test]
2232    async fn oauth_accepts_a_valid_token_and_refuses_the_rest_with_the_validators_challenge() {
2233        let jwks = testing::spawn_jwks_server("200 OK", testing::jwks_body()).await;
2234        let v = validator(&jwks.url);
2235        let layer = HttpAuthLayer::builder()
2236            .oauth(Arc::clone(&v))
2237            .build()
2238            .unwrap();
2239
2240        let bearer = format!("Bearer {}", testing::valid_token());
2241        let ok = send(&layer, &[("authorization", &bearer)]).await;
2242        assert_eq!(ok.status(), StatusCode::OK);
2243        assert!(ok.body().starts_with("oauth "), "{}", ok.body());
2244        assert_eq!(challenge(&ok), None);
2245
2246        let cases = [
2247            (None, 401, v.invalid_token_challenge()),
2248            (
2249                Some(format!("Bearer {}", expired_token())),
2250                401,
2251                v.invalid_token_challenge(),
2252            ),
2253            (
2254                Some("Bearer not-a-jwt".to_string()),
2255                401,
2256                v.invalid_token_challenge(),
2257            ),
2258            (
2259                Some(format!("Bearer {}", unscoped_token())),
2260                403,
2261                v.insufficient_scope_challenge(),
2262            ),
2263        ];
2264        for (header, status, expected) in cases {
2265            let headers: Vec<(&str, &str)> = header
2266                .iter()
2267                .map(|h| ("authorization", h.as_str()))
2268                .collect();
2269            let response = send(&layer, &headers).await;
2270            assert_eq!(response.status().as_u16(), status, "{header:?}");
2271            assert_eq!(challenge(&response), Some(expected.as_str()), "{header:?}");
2272            assert_eq!(
2273                response.body(),
2274                "",
2275                "the default body is ResBody::default()"
2276            );
2277        }
2278    }
2279
2280    #[tokio::test]
2281    async fn static_only_sends_the_static_challenge_unless_opted_out() {
2282        let default = HttpAuthLayer::builder()
2283            .static_token(STATIC)
2284            .build()
2285            .unwrap();
2286        let ok = send(&default, &[("authorization", "Bearer secret")]).await;
2287        assert_eq!(
2288            (ok.status(), ok.body().as_str()),
2289            (StatusCode::OK, "static")
2290        );
2291        for headers in [&[][..], &[("authorization", "Bearer wrong")][..]] {
2292            let refused = send(&default, headers).await;
2293            assert_eq!(refused.status(), StatusCode::UNAUTHORIZED);
2294            assert_eq!(challenge(&refused), Some(DEFAULT_STATIC_CHALLENGE));
2295        }
2296
2297        let custom = HttpAuthLayer::builder()
2298            .static_token(STATIC)
2299            .static_challenge(Some(HeaderValue::from_static("ApiKey realm=\"x\"")))
2300            .build()
2301            .unwrap();
2302        assert_eq!(
2303            challenge(&send(&custom, &[]).await),
2304            Some("ApiKey realm=\"x\"")
2305        );
2306
2307        let none = HttpAuthLayer::builder()
2308            .static_token(STATIC)
2309            .static_challenge(None)
2310            .build()
2311            .unwrap();
2312        let refused = send(&none, &[]).await;
2313        assert_eq!(refused.status(), StatusCode::UNAUTHORIZED);
2314        assert_eq!(challenge(&refused), None);
2315    }
2316
2317    #[tokio::test]
2318    async fn every_source_header_is_sensitive_for_the_callback_and_the_service() {
2319        let layer = HttpAuthLayer::builder()
2320            .static_token(STATIC)
2321            .sources([
2322                CredentialSource::authorization_bearer(),
2323                CredentialSource::Raw(HeaderName::from_static("x-api-key")),
2324            ])
2325            .on_reject(|cx: RejectContext<'_>| {
2326                for name in ["authorization", "x-api-key"] {
2327                    assert!(cx.request.headers[name].is_sensitive(), "{name}");
2328                }
2329                assert!(!format!("{cx:?}").contains("wrong"));
2330                Response::new(String::from("refused"))
2331            })
2332            .build()
2333            .unwrap();
2334        // `inner` asserts sensitivity on the accepted path.
2335        let ok = send(
2336            &layer,
2337            &[("authorization", "Bearer wrong"), ("x-api-key", STATIC)],
2338        )
2339        .await;
2340        assert_eq!(ok.status(), StatusCode::OK);
2341        let refused = send(
2342            &layer,
2343            &[("authorization", "Bearer wrong"), ("x-api-key", "wrong")],
2344        )
2345        .await;
2346        assert_eq!(refused.status(), StatusCode::UNAUTHORIZED);
2347        assert_eq!(refused.body(), "refused");
2348    }
2349
2350    #[tokio::test]
2351    async fn on_reject_shapes_the_body_but_not_the_status_or_the_challenge() {
2352        let jwks = testing::spawn_jwks_server("200 OK", testing::jwks_body()).await;
2353        let v = validator(&jwks.url);
2354        let layer = HttpAuthLayer::builder()
2355            .oauth(Arc::clone(&v))
2356            .on_reject(|cx: RejectContext<'_>| {
2357                Response::builder()
2358                    .status(StatusCode::IM_A_TEAPOT)
2359                    .header(WWW_AUTHENTICATE, "Basic realm=\"nope\"")
2360                    .header("content-type", "application/json")
2361                    .body(format!("{{\"status\":{}}}", cx.status.as_u16()))
2362                    .unwrap()
2363            })
2364            .build()
2365            .unwrap();
2366        let bearer = format!("Bearer {}", unscoped_token());
2367        let refused = send(&layer, &[("authorization", &bearer)]).await;
2368        assert_eq!(refused.status(), StatusCode::FORBIDDEN);
2369        let challenges: Vec<_> = refused.headers().get_all(WWW_AUTHENTICATE).iter().collect();
2370        assert_eq!(challenges, [v.insufficient_scope_challenge().as_str()]);
2371        assert_eq!(refused.headers()["content-type"], "application/json");
2372        assert_eq!(refused.body(), "{\"status\":403}");
2373    }
2374
2375    #[tokio::test]
2376    async fn an_optional_layer_passes_only_a_request_presenting_nothing() {
2377        let layer = HttpAuthLayer::builder()
2378            .static_token(STATIC)
2379            .optional()
2380            .build()
2381            .unwrap();
2382        for headers in [
2383            &[][..],
2384            &[("authorization", "Bearer ")][..],
2385            &[("authorization", "Basic x")][..],
2386        ] {
2387            let response = send(&layer, headers).await;
2388            assert_eq!(
2389                (response.status(), response.body().as_str()),
2390                (StatusCode::OK, "anonymous")
2391            );
2392        }
2393        for value in ["Bearer wrong", "DPoP x", "Bearer\tx"] {
2394            let response = send(&layer, &[("authorization", value)]).await;
2395            assert_eq!(response.status(), StatusCode::UNAUTHORIZED, "{value:?}");
2396            assert_eq!(challenge(&response), Some(DEFAULT_STATIC_CHALLENGE));
2397        }
2398    }
2399
2400    #[test]
2401    fn debug_never_prints_the_static_token() {
2402        let builder = HttpAuthLayer::builder().static_token("hunter2");
2403        assert!(!format!("{builder:?}").contains("hunter2"));
2404        let layer = builder.build().unwrap();
2405        let rendered = format!("{layer:?}");
2406        assert!(
2407            !rendered.contains("hunter2") && rendered.contains("<redacted>"),
2408            "{rendered}"
2409        );
2410        let service = tower_layer::Layer::layer(&layer, "inner");
2411        assert!(!format!("{service:?}").contains("hunter2"));
2412    }
2413
2414    // ── static_tokens ────────────────────────────────────────────────────────
2415
2416    fn rotation() -> StaticTokens {
2417        StaticTokens::new()
2418            .with(Some("current"), "key-current")
2419            .and_then(|t| t.with(Some("next"), "key-next"))
2420            .unwrap()
2421    }
2422
2423    /// Status, every header, and a body naming the credential and the static
2424    /// match the layer inserted.
2425    type Seen = (u16, Vec<(String, String)>, String);
2426
2427    async fn seen<R>(layer: &HttpAuthLayer<R>, headers: &[(&str, &str)]) -> Seen
2428    where
2429        R: RefusalResponse<String> + Send + Sync + 'static,
2430    {
2431        let mut request = Request::builder().uri("/test");
2432        for (name, value) in headers {
2433            request = request.header(*name, *value);
2434        }
2435        let service = tower_layer::Layer::layer(
2436            layer,
2437            service_fn(|request: Request<String>| async move {
2438                let body = format!(
2439                    "{:?} {:?}",
2440                    request.extensions().get::<Credential>(),
2441                    request.extensions().get::<StaticTokenMatch>(),
2442                );
2443                Ok::<_, std::convert::Infallible>(Response::new(body))
2444            }),
2445        );
2446        let response = service
2447            .oneshot(request.body(String::new()).unwrap())
2448            .await
2449            .unwrap();
2450        let headers = response
2451            .headers()
2452            .iter()
2453            .map(|(k, v)| (k.to_string(), v.to_str().unwrap().to_string()))
2454            .collect();
2455        (response.status().as_u16(), headers, response.into_body())
2456    }
2457
2458    #[tokio::test]
2459    async fn a_one_entry_set_answers_exactly_like_static_token() {
2460        let jwks = testing::spawn_jwks_server("200 OK", testing::jwks_body()).await;
2461        let v = validator(&jwks.url);
2462        let valid = format!("Bearer {}", testing::valid_token());
2463        let unscoped = format!("Bearer {}", unscoped_token());
2464        let requests: Vec<Vec<(&str, &str)>> = vec![
2465            vec![],
2466            vec![("authorization", "Bearer secret")],
2467            vec![("authorization", "bearer secret")],
2468            vec![("authorization", "Bearer wrong")],
2469            vec![("authorization", "Bearer ")],
2470            vec![("x-api-key", "secret")],
2471            vec![("authorization", "Bearer wrong"), ("x-api-key", "secret")],
2472            vec![("authorization", valid.as_str())],
2473            vec![("authorization", unscoped.as_str())],
2474        ];
2475        for (oauth, optional) in [(None, false), (Some(&v), false), (None, true)] {
2476            let sources = [
2477                CredentialSource::authorization_bearer(),
2478                CredentialSource::Raw(HeaderName::from_static("x-api-key")),
2479            ];
2480            let mut old = HttpAuthLayer::builder()
2481                .static_token(STATIC)
2482                .optional_oauth(oauth.cloned())
2483                .sources(sources.clone());
2484            let mut new = HttpAuthLayer::builder()
2485                .static_tokens(StaticTokens::single(STATIC).unwrap())
2486                .optional_oauth(oauth.cloned())
2487                .sources(sources);
2488            if optional {
2489                old = old.optional();
2490                new = new.optional();
2491            }
2492            let (old, new) = (old.build().unwrap(), new.build().unwrap());
2493            for headers in &requests {
2494                let a = seen(&old, headers).await;
2495                let b = seen(&new, headers).await;
2496                assert_eq!(a, b, "oauth={} {headers:.40?}", oauth.is_some());
2497                if a.2.starts_with("Some(StaticToken)") {
2498                    assert!(
2499                        a.2.ends_with("Some(StaticTokenMatch { label: None })"),
2500                        "{a:?}"
2501                    );
2502                }
2503            }
2504        }
2505    }
2506
2507    #[tokio::test]
2508    async fn every_token_in_a_set_is_accepted_with_its_label() {
2509        let layer = HttpAuthLayer::builder()
2510            .static_tokens(rotation())
2511            .build()
2512            .unwrap();
2513        for (secret, label) in [("key-current", "current"), ("key-next", "next")] {
2514            let (status, _, body) =
2515                seen(&layer, &[("authorization", &format!("Bearer {secret}"))]).await;
2516            assert_eq!(status, 200);
2517            assert_eq!(
2518                body,
2519                format!("Some(StaticToken) Some(StaticTokenMatch {{ label: Some({label:?}) }})")
2520            );
2521        }
2522        let (status, headers, _) = seen(&layer, &[("authorization", "Bearer key-old")]).await;
2523        assert_eq!(status, 401);
2524        assert!(headers.contains(&(
2525            "www-authenticate".to_string(),
2526            DEFAULT_STATIC_CHALLENGE.to_string()
2527        )));
2528    }
2529
2530    #[tokio::test]
2531    async fn static_token_and_static_tokens_are_merged() {
2532        let layer = HttpAuthLayer::builder()
2533            .static_token("key-next")
2534            .static_tokens(rotation())
2535            .build()
2536            .unwrap();
2537        // The secret given both ways counts once, under the set's label.
2538        let (_, _, body) = seen(&layer, &[("authorization", "Bearer key-next")]).await;
2539        assert!(body.ends_with("label: Some(\"next\") })"), "{body}");
2540        let layer = HttpAuthLayer::builder()
2541            .static_token("key-extra")
2542            .static_tokens(rotation())
2543            .build()
2544            .unwrap();
2545        for secret in ["key-current", "key-next", "key-extra"] {
2546            let (status, _, _) =
2547                seen(&layer, &[("authorization", &format!("Bearer {secret}"))]).await;
2548            assert_eq!(status, 200, "{secret}");
2549        }
2550        let (_, _, body) = seen(&layer, &[("authorization", "Bearer key-extra")]).await;
2551        assert!(body.ends_with("label: None })"), "{body}");
2552    }
2553
2554    /// A whitespace-only static token can never match (blank candidates are
2555    /// discarded before any comparison), so it counts as no static token:
2556    /// alone it is `NoCredential` rather than a layer that silently admits
2557    /// nobody; next to OAuth it is dropped.
2558    #[tokio::test]
2559    async fn a_whitespace_static_token_is_no_credential() {
2560        for blank in ["   ", "\t", " \r\n "] {
2561            for builder in [
2562                HttpAuthLayer::builder().static_token(blank),
2563                HttpAuthLayer::builder().static_token(blank).optional(),
2564                HttpAuthLayer::builder()
2565                    .static_token(blank)
2566                    .static_tokens(StaticTokens::new()),
2567            ] {
2568                assert_eq!(
2569                    builder.build().unwrap_err(),
2570                    AuthLayerError::NoCredential,
2571                    "{blank:?}"
2572                );
2573            }
2574            assert_eq!(
2575                HttpAuthLayer::builder()
2576                    .build_with_decision(StaticTokenDecision::StaticOnly(blank.into()))
2577                    .unwrap_err(),
2578                AuthLayerError::NoCredential
2579            );
2580        }
2581        let v = validator("http://127.0.0.1:1/jwks");
2582        let layer = HttpAuthLayer::builder()
2583            .oauth(v)
2584            .static_token("   ")
2585            .build()
2586            .unwrap();
2587        let refused = send(&layer, &[("authorization", "Bearer    ")]).await;
2588        assert_eq!(refused.status(), StatusCode::UNAUTHORIZED);
2589    }
2590
2591    #[test]
2592    fn an_empty_set_is_no_credential() {
2593        for builder in [
2594            HttpAuthLayer::builder().static_tokens(StaticTokens::new()),
2595            HttpAuthLayer::builder()
2596                .static_tokens(StaticTokens::new())
2597                .static_token(""),
2598            HttpAuthLayer::builder()
2599                .static_tokens(StaticTokens::new())
2600                .optional(),
2601            HttpAuthLayer::builder()
2602                .static_tokens(rotation())
2603                .optional_static_tokens(None),
2604        ] {
2605            assert_eq!(builder.build().unwrap_err(), AuthLayerError::NoCredential);
2606        }
2607    }
2608
2609    #[tokio::test]
2610    async fn static_tokens_follow_the_decision() {
2611        let v = validator("http://127.0.0.1:1/jwks");
2612        let accepts = |layer: HttpAuthLayer| async move {
2613            let mut accepted = Vec::new();
2614            for secret in ["key-current", "key-next", "decided"] {
2615                let (status, _, body) =
2616                    seen(&layer, &[("authorization", &format!("Bearer {secret}"))]).await;
2617                if status == 200 {
2618                    accepted.push(format!(
2619                        "{secret}={}",
2620                        body.rsplit("label: ").next().unwrap()
2621                    ));
2622                }
2623            }
2624            accepted
2625        };
2626
2627        // A decision carrying a token keeps the set and merges the token.
2628        for (decision, oauth) in [
2629            (StaticTokenDecision::StaticOnly("decided".into()), None),
2630            (
2631                StaticTokenDecision::StaticAndOAuth("decided".into()),
2632                Some(Arc::clone(&v)),
2633            ),
2634        ] {
2635            let layer = HttpAuthLayer::builder()
2636                .optional_oauth(oauth)
2637                .static_tokens(rotation())
2638                .build_with_decision(decision)
2639                .unwrap();
2640            assert_eq!(
2641                accepts(layer).await,
2642                [
2643                    "key-current=Some(\"current\") })",
2644                    "key-next=Some(\"next\") })",
2645                    "decided=None })"
2646                ]
2647            );
2648        }
2649        // The decision's token already in the set counts once, labeled.
2650        let layer = HttpAuthLayer::builder()
2651            .static_tokens(rotation())
2652            .build_with_decision(StaticTokenDecision::StaticOnly("key-current".into()))
2653            .unwrap();
2654        assert_eq!(
2655            accepts(layer).await,
2656            [
2657                "key-current=Some(\"current\") })",
2658                "key-next=Some(\"next\") })"
2659            ]
2660        );
2661
2662        // `accept_static_bearer: false` drops the set with the token.
2663        let layer = HttpAuthLayer::builder()
2664            .oauth(Arc::clone(&v))
2665            .static_tokens(rotation())
2666            .build_with_decision(StaticTokenDecision::StaticIgnored)
2667            .unwrap();
2668        assert!(accepts(layer).await.is_empty());
2669
2670        // A decision made without any static token cannot speak for a set.
2671        assert_eq!(
2672            HttpAuthLayer::builder()
2673                .oauth(Arc::clone(&v))
2674                .static_tokens(rotation())
2675                .build_with_decision(StaticTokenDecision::OAuthOnly)
2676                .unwrap_err(),
2677            AuthLayerError::DecisionWithoutStaticToken
2678        );
2679        assert_eq!(
2680            HttpAuthLayer::builder()
2681                .static_tokens(rotation())
2682                .build_with_decision(StaticTokenDecision::Unauthenticated)
2683                .unwrap_err(),
2684            AuthLayerError::DecisionWithoutStaticToken
2685        );
2686        // The validator mismatch is reported first.
2687        assert_eq!(
2688            HttpAuthLayer::builder()
2689                .static_tokens(rotation())
2690                .build_with_decision(StaticTokenDecision::OAuthOnly)
2691                .unwrap_err(),
2692            AuthLayerError::DecisionNeedsOAuth
2693        );
2694        // An empty set is no set.
2695        assert!(
2696            HttpAuthLayer::builder()
2697                .static_tokens(StaticTokens::new())
2698                .build_with_decision(StaticTokenDecision::Unauthenticated)
2699                .unwrap()
2700                .allows_unauthenticated()
2701        );
2702        assert!(
2703            HttpAuthLayer::builder()
2704                .oauth(Arc::clone(&v))
2705                .static_tokens(StaticTokens::new())
2706                .build_with_decision(StaticTokenDecision::OAuthOnly)
2707                .is_ok()
2708        );
2709    }
2710
2711    #[tokio::test]
2712    async fn an_optional_layer_with_several_tokens() {
2713        let layer = HttpAuthLayer::builder()
2714            .static_tokens(rotation())
2715            .optional()
2716            .build()
2717            .unwrap();
2718        let (status, _, body) = seen(&layer, &[]).await;
2719        assert_eq!((status, body.as_str()), (200, "None None"));
2720        for (secret, label) in [("key-current", "current"), ("key-next", "next")] {
2721            let (status, _, body) =
2722                seen(&layer, &[("authorization", &format!("Bearer {secret}"))]).await;
2723            assert_eq!(status, 200);
2724            assert!(body.ends_with(&format!("Some({label:?}) }})")), "{body}");
2725        }
2726        let (status, _, _) = seen(&layer, &[("authorization", "Bearer key-old")]).await;
2727        assert_eq!(status, 401);
2728    }
2729
2730    #[test]
2731    fn debug_never_prints_a_token_from_a_set() {
2732        let builder = HttpAuthLayer::builder()
2733            .static_token("hunter2-single")
2734            .static_tokens(
2735                StaticTokens::new()
2736                    .with(Some("current"), "hunter2-current")
2737                    .unwrap(),
2738            );
2739        let rendered = format!("{builder:?}");
2740        assert!(
2741            !rendered.contains("hunter2") && rendered.contains("current"),
2742            "{rendered}"
2743        );
2744        let layer = builder.build().unwrap();
2745        let rendered = format!("{layer:?}");
2746        assert!(
2747            !rendered.contains("hunter2") && rendered.contains("len: 2"),
2748            "{rendered}"
2749        );
2750        let service = tower_layer::Layer::layer(&layer, "inner");
2751        assert!(!format!("{service:?}").contains("hunter2"));
2752    }
2753}