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