Skip to main content

AuthLayerBuilder

Struct AuthLayerBuilder 

Source
pub struct AuthLayerBuilder { /* private fields */ }
Available on crate feature axum only.
Expand description

Builder for an enforcing AuthLayer; see AuthLayer::builder.

Implementations§

Source§

impl AuthLayerBuilder

Source

pub fn static_token(self, token: impl Into<String>) -> Self

Accept this static token (compared in constant time). An empty string counts as no token.

§Security

Setting the token here bypasses accept_static_bearer, which only crate::static_token_policy reads; with OAuth configured, prefer AuthLayerBuilder::build_with_decision. The token’s length is not hidden by the comparison, and Debug output shows it as <redacted>.

Source

pub fn optional_static_token(self, token: Option<String>) -> Self

AuthLayerBuilder::static_token when Some; for threading through the output of crate::static_token_policy or a secret loader.

Source

pub fn static_tokens(self, tokens: StaticTokens) -> Self

Accept every token in tokens — several static API keys at once, for a zero-downtime key rotation or one key per client — replacing a set given earlier. Each request’s candidates are compared with every entry in constant time (see StaticTokens). On a match the request’s extensions get Credential::StaticToken, exactly as for a single static_token, and the StaticTokenMatch naming the entry’s label, which a handler reads with the StaticTokenMatch extractor.

How it combines with the other static-token settings:

  • With static_token: both are accepted. A secret given both ways counts once, under the set’s label.
  • With build_with_decision (or AuthLayer::from_decision, which takes no set): the set follows the decision — see that method.
  • An empty set counts as no static token, as an empty static_token does: it never satisfies the fail-closed build on its own.

A one-entry set behaves exactly like static_token with that secret (same statuses, challenges, bodies and Credential); only the StaticTokenMatch it inserts, unlabeled or not, is new — and a layer built with static_token inserts an unlabeled one too.

§Security

Setting tokens here bypasses accept_static_bearer unless the layer is built with build_with_decision. Debug output shows the count and labels, never a secret.

§Examples
use axum::{Router, routing::get};
use oauth_resource_server::StaticTokenMatch;
use oauth_resource_server::StaticTokens;
use oauth_resource_server::axum::AuthLayer;

async fn which_key(matched: Option<StaticTokenMatch>) -> String {
    match matched {
        Some(m) => format!("key {}", m.label().unwrap_or("(unlabeled)")),
        None => "not a static key".to_string(),
    }
}

let tokens = StaticTokens::new()
    .with(Some("current"), "example-key-old")
    .and_then(|t| t.with(Some("next"), "example-key-new"))
    .unwrap();
let auth = AuthLayer::builder().static_tokens(tokens).build().unwrap();
let app: Router = Router::new().route("/", get(which_key)).route_layer(auth);
Source

pub fn optional_static_tokens(self, tokens: Option<StaticTokens>) -> Self

AuthLayerBuilder::static_tokens when Some — for threading through the output of the env feature’s static_tokens_from_env; None clears a set given earlier.

Source

pub fn oauth(self, validator: Arc<OAuthValidator>) -> Self

Accept OAuth access tokens this validator accepts.

Source

pub fn optional_oauth(self, validator: Option<Arc<OAuthValidator>>) -> Self

Source

pub fn sources( self, sources: impl IntoIterator<Item = CredentialSource>, ) -> Self

Where to read credentials from, replacing the default [CredentialSource::authorization_bearer()]. Every source is checked, whatever the others hold.

Source

pub fn static_challenge(self, challenge: Option<HeaderValue>) -> Self

The WWW-Authenticate challenge every 401 carries when NO OAuth validator is configured; with one, the validator’s challenges are used and this is ignored. Default: DEFAULT_STATIC_CHALLENGE.

Some(value) sends value instead — Bearer realm="my-api", say, or your own scheme for an API-key header. None sends no challenge at all and leaves any WWW-Authenticate an on_reject callback set untouched. That departs from RFC 9110 §15.5.2 (a 401 MUST carry a challenge); use it only to keep an existing API’s responses unchanged.

use axum::http::HeaderValue;
use oauth_resource_server::axum::AuthLayer;

let auth = AuthLayer::builder()
    .static_token("example-static-key")
    .static_challenge(Some(HeaderValue::from_static("Bearer realm=\"my-api\"")))
    .build()
    .unwrap();
Source

pub fn optional(self) -> Self

Let a request that presents NO credential through, unauthenticated, with nothing inserted into its extensions; everything else is decided exactly as without this. For routes that serve everyone but personalize for an authenticated caller, or read-open/write-authenticated APIs.

  • A credential that is accepted is inserted as usual.
  • A credential that is presented but refused — invalid, expired, signed by an unknown key, not the static token, or a valid token without the required scopes — gets the same 401 or 403, with the same WWW-Authenticate challenge and on_reject body, as without optional(). A client with a bad token learns so, rather than being served silently as anonymous.
  • A request presents no credential when EVERY value of EVERY configured CredentialSource header is absent or blank: empty or whitespace for a CredentialSource::Raw header; for a CredentialSource::Bearer header, empty or whitespace after Bearer, or a value using some other scheme (which carries no bearer credential to check). This is the same classification crate::authenticate() reports as TokenRejection::Missing, with two stricter edges: a header value that is not visible ASCII counts as a presented credential, not as a blank one, and so does a non-blank LATER value of a repeated header (only the first is ever authenticated). Both are refused with the layer’s 401. So are a Bearer value that uses a tab instead of a space before a non-blank token, and any DPoP-scheme value (RFC 9449; a sender-constrained token this crate cannot verify must be refused, not served as anonymous). The strict parsing of those values is unchanged: they get exactly the refusal a non-optional layer sends.
  • Any Credential/AuthorizedToken an outer layer inserted is removed first, so a pass-through always extracts as None (see nested layers).

Handlers read the outcome with Option<Credential> or Option<AuthorizedToken> (see the module docs); a handler that takes a plain Credential or AuthorizedToken refuses a passed-through request with the layer’s own 401 and challenge.

§Security

This is not a way around the fail-closed build: build still requires a static token or an OAuth validator, and a credential the layer cannot accept is still refused. What passes through is only what any caller could send by leaving the credential headers off, so every handler behind an optional layer must treat None as unauthenticated. The pass-through is logged at debug.

§Examples
use axum::{Router, routing::get};
use oauth_resource_server::Credential;
use oauth_resource_server::axum::AuthLayer;

async fn greeting(credential: Option<Credential>) -> &'static str {
    match credential {
        Some(_) => "hello, authenticated caller",
        None => "hello, anonymous caller",
    }
}

let auth = AuthLayer::builder()
    .static_token("example-static-key")
    .optional()
    .build()
    .unwrap();
let app: Router = Router::new().route("/", get(greeting)).route_layer(auth);
Source

pub fn require_scopes( self, scopes: impl IntoIterator<Item = impl Into<String>>, ) -> Self

Require every scope in scopes (all-of) of every credential this layer accepts, on top of the validator’s own required_scopes — replacing scopes given earlier. It runs after validation, on the same validator and so the same key cache: a layer for the routes that write needs no second validator, only a second layer built from the same Arc<OAuthValidator> (or, simpler, a RequireScopes route layer, or the Scoped extractor, behind this one).

  • An OAuth token missing one is refused with 403 and a challenge naming the validator’s required scopes followed by these — the scopes this request needs, which is what a client re-authorizes for (crate::refusal_for_scopes sends the same bytes).
  • A static token has no scopes, so it is refused the same way, with 403, unless static_token_bypasses_scopes. Fail-safe by default: in dual mode a static token was never subject to the validator’s scopes, but a route that asks for scopes by name asks for them of every credential.
  • An optional layer still passes a request that presents no credential (its handlers must treat that as unauthenticated); a presented one is checked.
  • A layer that lets everything through (AuthLayer::allow_unauthenticated) checks nothing, this included, so build_with_decision refuses an Unauthenticated decision on a builder with scopes (AuthLayerError::ScopesWithoutAuthentication) rather than silently drop them.

build refuses an entry that is not an RFC 6749 §3.3 scope-token (AuthLayerError::InvalidScope), and scopes with no OAuth validator unless static tokens bypass them (AuthLayerError::ScopesNeedOAuth).

§Examples
use std::sync::Arc;

use axum::{Router, routing::{get, post}};
use oauth_resource_server::OAuthValidator;
use oauth_resource_server::axum::AuthLayer;

let reads = AuthLayer::builder().oauth(Arc::clone(&oauth)).build().unwrap();
// The same validator: one key cache for both.
let writes = AuthLayer::builder()
    .oauth(oauth)
    .require_scopes(["docs:write"])
    .build()
    .unwrap();
Router::new()
    .merge(Router::new().route("/docs", get(|| async { "read" })).route_layer(reads))
    .merge(Router::new().route("/docs/new", post(|| async { "written" })).route_layer(writes))
Source

pub fn static_token_bypasses_scopes(self) -> Self

Let a static token pass require_scopes instead of refusing it with 403: the static token counts as holding every scope, as it did before per-route scopes existed. Without require_scopes it changes nothing.

§Security

Opt in only where the static token is meant to be a full-access key.

Source

pub fn on_reject( self, f: impl Fn(RejectContext<'_>) -> Response + Send + Sync + 'static, ) -> Self

Build the body and any extra headers of a refusal — for an API whose errors are, say, JSON. Without it a refusal has an empty body. The RejectContext carries the rejection, the status and the request’s parts (method, URI, headers), so the shape can depend on, say, Accept.

The callback shapes the response only; it cannot change the outcome. Whatever it returns, the status is set to RejectContext::status (401 for a missing or invalid credential, 403 for insufficient scope), and WWW-Authenticate is set to the layer’s challenge, replacing any the callback set: the validator’s when OAuth is configured — every 401/403 must carry it, or claude.ai (among others) never starts the authorization flow — and otherwise the static_challenge. Only with static_challenge(None) and no OAuth are the callback’s headers left as they are.

Never put TokenRejection::Invalid’s reason in the body: telling an unauthenticated caller exactly which check failed is a free oracle. The credential headers in RejectContext::request are marked sensitive, and RejectContext’s Debug prints no header values.

Source

pub fn build_with_decision( self, decision: StaticTokenDecision, ) -> Result<AuthLayer, AuthLayerError>

Build the layer a crate::static_token_policy decision calls for, keeping this builder’s sources and on_reject.

The decision’s static token (if any) replaces one set on this builder. StaticTokenDecision::Unauthenticated yields AuthLayer::allow_unauthenticated — the decision is itself the application’s explicit opt-out, since static_token_policy returns it only when asked to allow unauthenticated access. Every other decision builds an enforcing layer exactly as AuthLayerBuilder::build does.

A static_tokens set follows the decision, which speaks about one token (pass the current one to static_token_policy):

DecisionThe builder’s static_tokens set
StaticOnly(t), StaticAndOAuth(t)kept, and merged with t (a t already in the set counts once, under its label)
StaticIgnoreddropped, with t: accept_static_bearer: false wins over every static token
OAuthOnly, UnauthenticatedAuthLayerError::DecisionWithoutStaticToken — the policy was never told a static token exists, so the decision cannot speak for the set, and honouring it would silently drop the keys (or open the routes despite them)

An empty set is no set: it never causes that error.

§Errors

The decision must agree with the validator given via AuthLayerBuilder::oauth: AuthLayerError::DecisionNeedsOAuth when it was made with OAuth on and no validator was given, AuthLayerError::DecisionWithoutOAuth when it was made with OAuth off (including Unauthenticated) and one was given. Then AuthLayerError::DecisionWithoutStaticToken as above, then AuthLayerError::ScopesWithoutAuthentication for require_scopes with an Unauthenticated decision. Otherwise as AuthLayerBuilder::build.

Source

pub fn build(self) -> Result<AuthLayer, AuthLayerError>

Build the layer.

§Errors

AuthLayerError::NoCredential with neither a non-blank static token (from static_token or a non-empty static_tokens set) nor an OAuth validator; AuthLayerError::NoSources with an empty source list; AuthLayerError::InvalidChallenge when the validator’s challenge is not a valid header value (only reachable from a hand-edited resolved config); AuthLayerError::InvalidScope and AuthLayerError::ScopesNeedOAuth as require_scopes says.

Trait Implementations§

Source§

impl Debug for AuthLayerBuilder

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for AuthLayerBuilder

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more