Skip to main content

Module http_layer

Module http_layer 

Source
Available on crate feature 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§

EmptyRefusal
The default refusal: B::default() as the body (empty for the usual body types), no extra headers.
HttpAuthLayer
A tower::Layer that authenticates every request to the service it wraps, for any http::Request<ReqBody> / http::Response<ResBody> service; see the module docs. Cheap to clone (two Arcs).
HttpAuthLayerBuilder
Builder for an enforcing HttpAuthLayer; see HttpAuthLayer::builder.
HttpAuthService
The service an HttpAuthLayer wraps another in.
InvalidScope
A scope given to a route-level requirement (RequireScopes::try_new, the mcp feature’s McpToolScopes::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.
RejectContext
What an on_reject callback (HttpAuthLayerBuilder::on_reject, or the axum layer’s AuthLayerBuilder::on_reject) is told about a refusal.
RequireScopes
A route-level scope requirement: a tower::Layer for 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).
RequireScopesService
The service a RequireScopes wraps another in.

Enums§

AuthLayerError
Why a layer’s builder refused to build (HttpAuthLayerBuilder, or the axum layer’s AuthLayerBuilder, which reaches this type as oauth_resource_server::axum::AuthLayerError).
CredentialSource
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§

RefusalResponse
Builds the response for a refusal (its body and any extra headers); see HttpAuthLayerBuilder::on_reject. Implemented for EmptyRefusal (the default) and for every Fn(RejectContext<'_>) -> http::Response<B>.