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