tower only.Expand description
A tower authentication layer for any HTTP stack built on the http crate’s
types — hyper, tonic, or a tower service of your own — whatever its body
types: HttpAuthLayer (built with HttpAuthLayerBuilder) wraps a
Service<http::Request<ReqBody>, Response = http::Response<ResBody>> for any
ReqBody and any ResBody.
use http::{Request, Response};
use oauth_resource_server::http_layer::HttpAuthLayer;
use tower::{ServiceBuilder, service_fn};
let auth = HttpAuthLayer::builder()
.static_token("example-static-key")
.build()
.unwrap();
let service = ServiceBuilder::new().layer(auth).service(service_fn(
|_request: Request<String>| async { Ok::<_, std::convert::Infallible>(Response::new(String::from("ok"))) },
));On an axum app use crate::axum::AuthLayer instead (feature axum, which
turns this feature on): it is the same check, with axum’s Response, the
require_auth middleware form, and the axum extractors. Behind an
HttpAuthLayer alone those extractors return the value it inserted
(AuthorizedToken, Credential, StaticTokenMatch, and their Option
forms), but answer 500 when there is none — even Option<..> behind an
optional() pass-through, never None — because they cannot tell it ran.
§Behavior
Identical to the axum layer, because both run the same code: every
configured CredentialSource contributes one candidate to
crate::authenticate(); an accepted request gets the Credential (and,
for an OAuth token, the AuthorizedToken; for a static token, the
StaticTokenMatch naming which one) inserted into its extensions;
a refused one gets the status and WWW-Authenticate challenge
crate::refusal() describes — the validator’s challenge on every 401 and
403 when OAuth is configured, otherwise the
static_challenge
(crate::DEFAULT_STATIC_CHALLENGE unless set). The refusal’s body is
ResBody::default() (empty, for the usual body types) unless
on_reject builds one; its status and
challenge are set after that callback runs, so it cannot drop or contradict
them. optional passes a request that
presents no credential through, exactly as the axum layer’s does.
Fail-closed by construction. HttpAuthLayerBuilder::build refuses to
build without a static token or an OAuth validator
(AuthLayerError::NoCredential); the only layer that lets every request
through is HttpAuthLayer::allow_unauthenticated, asked for by name (or a
StaticTokenDecision::Unauthenticated handed to
HttpAuthLayerBuilder::build_with_decision). Every configured source
header is marked sensitive (http::HeaderValue::set_sensitive) on the
request before the callback and the inner service see it (an
allow_unauthenticated layer, which has no sources, marks Authorization),
and the layer’s
Debug never prints a static token (the builder’s single one shows as
<redacted>, a StaticTokens set as its count and labels).
§Per-route scopes
HttpAuthLayerBuilder::require_scopes requires more scopes of every
credential the layer accepts, and RequireScopes (placed behind either
layer) of the routes it wraps, on top of the validator’s own. Both layers
mark every request they pass (a private marker holding the layer’s
challenges and refusal builder), so RequireScopes — and the mcp
feature’s McpToolScopes — refuse with that layer’s own status,
challenge and on_reject body, with a 403 challenge naming the scopes
the request needed; without a layer in front they answer 500.
§Logging
The same outcomes at the same levels as the axum layer, with target
oauth_resource_server::http_layer: an accepted OAuth token at debug
(principal, subject, scopes — never the token); an accepted static token
at debug (its label, never the token); a request with no credential
at debug when OAuth is configured; a request with no credential passed
through by an optional layer at debug;
any other refusal at warn, with the reason when OAuth is configured. The
reason goes to the log only, never to the caller. A route-level scope
refusal (RequireScopes, McpToolScopes) is logged at info with the
required and present scopes; a wiring no request can satisfy at error.
Every one of these events (the route-level refusals included) also
carries the stable, low-cardinality fields auth.outcome (accepted,
rejected, passed_through), auth.mechanism (static, oauth,
none) and, on a refusal, auth.reason (an InvalidTokenKind label,
missing, insufficient_scope or misconfigured) and auth.status
(401, 403 or 500); an accepted labeled static token adds
auth.static_label. Unlike the message text, their names and values are
covered by semver — the README’s “Observability” section lists them all,
with the spans and the metrics feature’s counters.
§Naming
HttpAuthLayer, not AuthLayer: with the axum feature on, both layers
are in scope in one application, and two AuthLayers would read as the
same type under two paths. The Http prefix names what it is generic over
— http::Request<B> for any B. The module is http_layer, not tower
(the feature is still tower): a crate-root module named tower would
make tower ambiguous in a downstream module that glob-imports this
crate’s root and also uses the tower crate.
CredentialSource, RejectContext and AuthLayerError are shared by
both layers and are also reachable under oauth_resource_server::axum.
Structs§
- Empty
Refusal - The default refusal:
B::default()as the body (empty for the usual body types), no extra headers. - Http
Auth Layer - A
tower::Layerthat authenticates every request to the service it wraps, for anyhttp::Request<ReqBody>/http::Response<ResBody>service; see the module docs. Cheap to clone (twoArcs). - Http
Auth Layer Builder - Builder for an enforcing
HttpAuthLayer; seeHttpAuthLayer::builder. - Http
Auth Service - The service an
HttpAuthLayerwraps another in. - Invalid
Scope - A scope given to a route-level requirement (
RequireScopes::try_new, themcpfeature’sMcpToolScopes::try_default/try_tool) that is not an RFC 6749 §3.3 scope-token: empty, or holding a space,",\, a control or non-ASCII character. No token can carry such a scope, so the requirement could never be met. - Reject
Context - What an
on_rejectcallback (HttpAuthLayerBuilder::on_reject, or the axum layer’sAuthLayerBuilder::on_reject) is told about a refusal. - Require
Scopes - A route-level scope requirement: a
tower::Layerfor the routes (or services) that need more than the authentication layer in front of them requires — say, a write scope on the routes that write. It adds no key cache and no validation of its own: it reads the credential that layer accepted from the request’s extensions and checks it against its scopes (all-of, the same matching as the validator’s own check,AuthorizedToken::require_scopes). - Require
Scopes Service - The service a
RequireScopeswraps another in.
Enums§
- Auth
Layer Error - Why a layer’s builder refused to build (
HttpAuthLayerBuilder, or the axum layer’sAuthLayerBuilder, which reaches this type asoauth_resource_server::axum::AuthLayerError). - Credential
Source - Where a request may carry a credential. Each configured source contributes
at most one candidate — the header’s FIRST value; a request that repeats the
header has the later values ignored, not refused — and every candidate is
checked independently (see
crate::authenticate()): a bad credential in one source never masks a good one in another.
Traits§
- Refusal
Response - Builds the response for a refusal (its body and any extra headers); see
HttpAuthLayerBuilder::on_reject. Implemented forEmptyRefusal(the default) and for everyFn(RejectContext<'_>) -> http::Response<B>.