pub struct AuthorizationServer<S: Storage, C: Clock = SystemClock> { /* private fields */ }Expand description
Implementations§
Source§impl<S: Storage, C: Clock> AuthorizationServer<S, C>
impl<S: Storage, C: Clock> AuthorizationServer<S, C>
Available on crate feature par and (crate features jar or par) only.
par and (crate features jar or par) only.RFC 9126 section 2: the pushed authorization request endpoint.
parameters is the form body exactly as it arrived, so that this can apply section 2.1
step 2 (a pushed request carrying request_uri is refused) and section 3 (a request
parameter is a signed request object) rather than making the host decide either. Parameters
this server does not know are ignored, per RFC 6749 section 3.1.
The client authenticates exactly as at the token endpoint (section 2.1 step 1), and the pushed request is fully validated here (step 3): client, exact-match redirect URI, PKCE S256, scope inside the registration, RFC 8707 resource indicators. That is the whole point of the back channel. A client learns its request is malformed from a JSON error it can read rather than from a browser redirect it cannot.
Errors follow RFC 9126 section 2.3: the token endpoint’s RFC 6749 section 5.2 shape, with
invalid_request standing in for the authorization errors section 4.1.2.1 refuses to
redirect (a missing or mismatching redirect URI above all).
Available on crate feature par and (crate features jar or par) only.
par and (crate features jar or par) only.AuthorizationServer::pushed_authorization_request for a client presenting any credential
this server accepts at the token endpoint, not just a shared secret.
RFC 9126 section 2.1 step 1 says the client authenticates here “in the same way as at the
token endpoint”, so an RFC 7523 private_key_jwt client that the token endpoint accepts
must be accepted here too; a PAR endpoint that only understood client_secret_basic would
lock exactly the deployments that most want PAR (FAPI 2.0 requires both) out of it. This
mirrors device_authorization_with_credential and
introspection_response_with_credential, for the same reason and with the same shape.
Available on crate feature par and (crate features jar or par) only.
par and (crate features jar or par) only.The authorization endpoint for a request that arrived as client_id plus request_uri
(RFC 9126 section 4).
Every OTHER query parameter is ignored, and this signature is how that is enforced: they
are not accepted, so there is no code path in which one of them could win. RFC 9101
section 6.3, which RFC 9126 section 4 builds on, is explicit that the server MUST only use
the parameters from the reference “even if the same parameter is provided in the query
parameter”. A client that duplicates scope in the query gets the pushed scope; an
attacker who appends one gets the same.
The handle is consumed atomically, so a second use of it fails however many requests are in flight (RFC 9126 section 4 and section 7.3).
Available on crate feature jar and (crate features jar or par) only.
jar and (crate features jar or par) only.The authorization endpoint for a request that arrived as client_id plus a signed
request object (RFC 9101 sections 5.1 and 6).
As with AuthorizationServer::validate_pushed_authorization_request, the query
parameters that a client may have duplicated alongside the object are not accepted here at
all: RFC 9101 section 6.3 requires the server to use only the object’s own parameters, and
the surest way to honour that is to have no other parameters in hand.
Source§impl<S: Storage, C: Clock> AuthorizationServer<S, C>
impl<S: Storage, C: Clock> AuthorizationServer<S, C>
Sourcepub async fn register_dynamic_client(
&self,
metadata: &ClientMetadata,
initial_access_token: Option<&str>,
) -> Result<ClientInformation, RegistrationFailure>
pub async fn register_dynamic_client( &self, metadata: &ClientMetadata, initial_access_token: Option<&str>, ) -> Result<ClientInformation, RegistrationFailure>
RFC 7591 section 3.1: register a client.
initial_access_token is whatever the request presented as an RFC 6750 bearer token, and
is passed straight to the host’s RegistrationPolicy; this crate does not interpret it.
On success the returned ClientInformation carries the ONLY copy of the client secret
(for a confidential registration) and of the RFC 7592 registration access token. Neither is
recoverable afterwards, by the client or by the host: see the module docs.
§THIS FUTURE IS NOT CANCELLATION SAFE, and what a drop costs
A dropped future stops at whatever await it was suspended in and never resumes, and this
crate cannot make it finish: there is no destructor that can run an async store call. The
token plane states this contract on AuthorizationServer::token and
AuthorizationServer::revoke. The management plane pays something different for a drop,
and until 0.9.2 said nothing at all about it.
What it costs here follows from the once-only rule above: the credentials are minted
BEFORE crate::store::Storage::put_client and handed back only in the return value after
it. A drop at or after that write leaves a row this server will honour whose registration
access token — and, for a confidential registration, whose client secret — existed only in
the dropped frame. Nothing recovers either one: this server kept verifiers, not
credentials, so there is nothing to re-send; RFC 7592
management of that registration needs the access token that is gone, so section 2.3 cannot
delete it either; and a crate::client::Client has no expiry and is never reclaimed by
crate::store::Storage::sweep_expired. The caller sees a request that did not answer and
retries, which registers a SECOND client. The first is permanent litter that only a host
deleting it out of band removes.
The order is not the defect and reversing it would be worse: returning the credentials before the write would hand a caller a live-looking secret for a registration that then failed to persist.
WHAT A HOST MUST DO, exactly as on the token plane: drive this from a task the connection
cannot cancel — spawn the call and await the join handle — so a disconnecting client aborts
the response and not the work. This crate’s axum adapter already does that for every route,
this one included, because it spawns inside a single fallback; see crate::http. A
host that mounts crate::http::AuthorizationService::handle itself, or calls this method
directly, owns it.
Sourcepub async fn read_registration(
&self,
client_id: &ClientId,
registration_access_token: &str,
) -> Result<ClientInformation, RegistrationFailure>
pub async fn read_registration( &self, client_id: &ClientId, registration_access_token: &str, ) -> Result<ClientInformation, RegistrationFailure>
RFC 7592 section 2.1: read a registration.
The response carries no client_secret and no registration_access_token, because this
server stores neither: see the module docs.
Sourcepub async fn update_registration(
&self,
client_id: &ClientId,
registration_access_token: &str,
metadata: &ClientMetadata,
) -> Result<ClientInformation, RegistrationFailure>
pub async fn update_registration( &self, client_id: &ClientId, registration_access_token: &str, metadata: &ClientMetadata, ) -> Result<ClientInformation, RegistrationFailure>
RFC 7592 section 2.2: replace a registration’s metadata.
The whole document is replaced, not merged: section 2.2 says the client sends its full metadata and that any omitted member is treated as absent. Merging would make a client that dropped a redirect URI keep it, which is precisely backwards for the one member that decides where a code may be delivered.
client_id cannot be changed (section 2.2), and the grant and scope ceilings of
RegistrationConfig apply again, so an update cannot reach anything a fresh registration
could not.
The response carries a client_secret in exactly one case: an update that moves the client
from token_endpoint_auth_method: none to a method that needs one MINTS a secret, and this
is the only response other than the original registration that ever carries a live
credential. It is never an ECHO of an existing secret, which this server does not hold; see
the module docs.
§THIS FUTURE IS NOT CANCELLATION SAFE, and this is the drop a client cannot retry around
See AuthorizationServer::register_dynamic_client for why a dropped future cannot be
finished by this crate. The expensive drop point here is the one case above: the mint.
When this call mints a secret it writes the crate::client::SecretHash of it through
crate::store::Storage::compare_and_swap_client, and returns the secret itself only in
the value at the end of this function. A drop after that swap RESOLVES and before the
response reaches the client leaves the store holding a verifier for a string that exists
nowhere. The client’s retry does not repair it and cannot: the stored registration is now
confidential, so on the second pass had_secret is true and the arm that KEEPS THE
EXISTING VERIFIER is taken rather than the mint. That arm is right for what it was written
for — a metadata edit must not log a client out of the token endpoint — and nothing on the
wire distinguishes that case from this one. The registration can never authenticate at the
token endpoint again.
The way out is RFC 7592 section 2.3, and it is the only one: this call never rotates the
registration access token, so the client still holds it and can DELETE the registration and
register afresh. A host whose RegistrationPolicy admits an initial access token only
once has to provision the client again itself.
Reversing the order would be worse rather than better: handing the secret back before the swap would give a client a credential for a write that a concurrent section 2.3 delete is entitled to refuse — which is exactly what the compare-and-swap exists to allow. So the order stands and the contract is stated instead.
The cheaper drop points, for completeness: anywhere before the swap costs nothing, because
nothing has been written; between the swap and the return, an update with no mint loses
only the crate::events::Event::ClientRegistrationUpdated, so an audit trail can miss an
update that did happen.
WHAT A HOST MUST DO is what AuthorizationServer::register_dynamic_client says: spawn
this and await the join handle. The axum adapter does. A host driving this future from the
connection is choosing the cost above, at whatever rate its clients disconnect.
Sourcepub async fn delete_registration(
&self,
client_id: &ClientId,
registration_access_token: &str,
) -> Result<(), RegistrationFailure>
pub async fn delete_registration( &self, client_id: &ClientId, registration_access_token: &str, ) -> Result<(), RegistrationFailure>
RFC 7592 section 2.3: delete a registration.
Deletion takes everything the registration was issued with it, through
Storage::delete_client: a client that no longer exists must not still have live access
tokens, refresh chains or outstanding authorization codes. Section 2.3 requires exactly
that, and it is the half of deletion that is easy to skip and impossible to notice.
§THIS FUTURE IS NOT CANCELLATION SAFE, and what a drop costs
This is the cheap one of the three; see AuthorizationServer::register_dynamic_client
for the contract and AuthorizationServer::update_registration for the expensive one. A
drop before crate::store::Storage::delete_client leaves the registration standing, and
the client still holds the registration access token, so the request is simply repeatable.
A drop after it loses the crate::events::Event::ClientRegistrationDeleted, so a host
can find a registration gone with no audit record of who removed it. A retry after that
answers Unauthorized, because the token now names a registration that does not exist —
and pays what an unknown id pays: a
crate::events::Event::ClientRegistrationAuthenticationFailed carrying
crate::events::ClientAuthFailure::UnknownClient, charged to the limiter as a failure.
So a client retrying a deletion that in fact completed reads, to an operator, exactly like
somebody guessing at a registration access token. That is the audit cost of a drop here,
and it is the whole of it: nothing is left half-written.
Source§impl<S: Storage> AuthorizationServer<S, SystemClock>
impl<S: Storage> AuthorizationServer<S, SystemClock>
Sourcepub fn new(config: ServerConfig, store: S) -> Self
pub fn new(config: ServerConfig, store: S) -> Self
Construct with the real clock. This is the crate’s allocation entry point: call it when (and only when) host config enables the AS.
Source§impl<S: Storage, C: Clock> AuthorizationServer<S, C>
impl<S: Storage, C: Clock> AuthorizationServer<S, C>
Sourcepub fn with_clock(config: ServerConfig, store: S, clock: C) -> Self
pub fn with_clock(config: ServerConfig, store: S, clock: C) -> Self
Construct with an injected clock (tests).
Sourcepub fn with_event_sink(self, sink: Box<dyn EventSink>) -> Self
pub fn with_event_sink(self, sink: Box<dyn EventSink>) -> Self
Install the audit sink (RFC-agnostic; see crate::events). Builder-style so a host wires
it at construction: AuthorizationServer::new(cfg, store).with_event_sink(Box::new(sink)).
This crate logs nothing by itself. Without a sink, the two events that are evidence of compromise (authorization code replay, refresh token reuse) revoke silently, which means an operator learns about a stolen grant from a support ticket rather than from a log line.
Sourcepub fn with_rate_limiter(self, limiter: Box<dyn RateLimiter>) -> Self
pub fn with_rate_limiter(self, limiter: Box<dyn RateLimiter>) -> Self
Install the rate limiter. THIS LIBRARY DOES NOT RATE LIMIT: RFC 8628 section 5.1 makes user
code entropy adequate only IN COMBINATION WITH rate limiting of code entry, and only the
host has a caller, an IP or a session to count against. See
AuthorizationServer::approve_device.
Sourcepub fn with_secret_verifier(self, verifier: Box<dyn SecretVerifier>) -> Self
pub fn with_secret_verifier(self, verifier: Box<dyn SecretVerifier>) -> Self
Install the client secret verifier, for crate::client::SecretHash schemes this crate
does not implement (argon2id, scrypt, an HSM). The built-in scheme needs no verifier and is
never delegated to one.
Sourcepub fn with_registration_policy(
self,
policy: Box<dyn RegistrationPolicy>,
) -> Self
pub fn with_registration_policy( self, policy: Box<dyn RegistrationPolicy>, ) -> Self
Install the RFC 7591 registration policy: who may create a client here.
Required, not optional, for any host that sets ServerConfig::registration: with no
policy installed every registration is refused, because an endpoint that mints clients and
has been told nothing about who may use it is the abuse vector RFC 7591 section 5
describes. See crate::registration::RegistrationPolicy.
Sourcepub fn with_request_object_keys(self, keys: Box<dyn RequestObjectKeys>) -> Self
Available on crate feature jar only.
pub fn with_request_object_keys(self, keys: Box<dyn RequestObjectKeys>) -> Self
jar only.Install the RFC 9101 request object verification keys: which public key, under which algorithm, each client registered for signing request objects.
Required, not optional, for a host that sets ServerConfig::jar: with no key source
installed every request parameter is refused, because a server that cannot check a
signature must not act on the claims under it. See crate::par::RequestObjectKeys.
Sourcepub fn with_es256_verifier(self, verifier: Arc<dyn Es256Verifier>) -> Self
Available on crate feature jwt only.
pub fn with_es256_verifier(self, verifier: Arc<dyn Es256Verifier>) -> Self
jwt only.Install the ES256 backend this server VERIFIES signatures with: RFC 9449 DPoP proofs, RFC 9101 request objects, RFC 7523 client assertions.
Required unless jwt-p256 is compiled in, which installs crate::jwt::P256Verifier as
the default. With neither, every signed credential is REFUSED, exactly as an absent
crate::par::RequestObjectKeys or an absent registration policy refuses: a server that
cannot check a signature must never behave as though it had checked one.
A verifier installed here WINS over the built-in one, because it was installed. That is the
whole of the precedence rule, and it is why nothing in this crate’s feature set is mutually
exclusive: a dependency graph that unifies jwt-p256 on cannot take a host’s choice away.
Run crate::signer_conformance against whatever you install here before you deploy it.
Sourcepub fn hooks(&self) -> &Hooks
pub fn hooks(&self) -> &Hooks
The installed host seams, for a host that wants to emit its own events onto the same channel (a consent decision, say) or to consult its own limiter.
Sourcepub fn jwks(&self) -> Option<Jwks>
Available on crate feature jwt only.
pub fn jwks(&self) -> Option<Jwks>
jwt only.The RFC 7517 key set to serve at jwks_uri, or None when tokens are opaque. PUBLIC key
parameters only.
Sourcepub fn jwks_uri(&self) -> Option<&str>
Available on crate feature jwt only.
pub fn jwks_uri(&self) -> Option<&str>
jwt only.The configured jwks_uri, or None. An RFC 8414 metadata document must advertise
jwks_uri exactly when this is Some: advertising a key set for an AS that signs nothing
is a lie, and signing without advertising leaves resource servers unable to verify.
Sourcepub fn config(&self) -> &ServerConfig
pub fn config(&self) -> &ServerConfig
The configuration.
Sourcepub fn metadata(&self) -> AuthorizationServerMetadata
pub fn metadata(&self) -> AuthorizationServerMetadata
The RFC 8414 document THIS server would publish, which is the one a host should serve.
Different from crate::metadata::AuthorizationServerMetadata::from_config, and the
difference is the point: from_config sees the configuration and nothing else, while some
of what the document promises depends on a seam the host INSTALLED on the server. RFC 7523
private_key_jwt is the case that forced this. It is ES256, so it is honest exactly when
this server can check an ES256 signature, and that is a property of
AuthorizationServer::with_es256_verifier plus the jwt-p256 feature, neither of which
a &ServerConfig can see. A method the document names and the token endpoint refuses
every time is not a defect a client can work around: it did what it was told.
from_config therefore advertises only what the CONFIGURATION alone establishes, and this
adds back exactly what the installed seams establish. It is the direction that fails safe:
a host that ignores this method under-advertises rather than inviting clients to use a
method that cannot work. crate::http::ServiceBuilder::build uses this one.
Sourcepub fn store(&self) -> &S
pub fn store(&self) -> &S
The storage seam, so the host can administer its own store.
The one administrative operation this crate REQUIRES of the host is eviction:
Storage::sweep_expired must be called on some host-chosen schedule, because nothing in
this crate ever evicts anything on its own. There is no background task here and there will
not be one (see the crate docs on zero cost until enabled), so a host that never sweeps has
a store that only grows: consumed authorization codes and spent refresh records are
retained ON PURPOSE until their expiry (that retention is what makes replay and reuse
detectable), and expired access tokens and abandoned device grants are simply never looked
at again. Anything else the host wants to do here, such as listing, is its own store’s
business and not this trait’s.
Sourcepub async fn register_client(&self, client: Client) -> Result<(), StorageError>
pub async fn register_client(&self, client: Client) -> Result<(), StorageError>
Register (or replace) a client the HOST provisioned: no policy is consulted, no credential is minted, and whatever is handed in is what the store holds.
This is the out-of-band half of registration. RFC 7591 dynamic client registration is the
other half and it is built:
AuthorizationServer::register_dynamic_client layers on this one, adding the
crate::registration::RegistrationPolicy check, the minted client_id and secret, and
the RFC 7592 management credential. A host calling THIS method is asserting that the
registration was authorised somewhere it can point to.
§What IS checked, despite “no policy is consulted”
Every redirect_uris entry, against the same rule RFC 7591 registration applies (RFC 6749
section 3.1.2: an absolute URI with no fragment, and nothing outside printable ASCII, which
RFC 3986 requires of a URI anyway). This is not policy: it is whether the value can work at
all, and the two ways of creating a client used to disagree about it, with the DIRECT one —
the one a default build has, since http and dynamic registration are optional — being the
permissive half.
The reason it is worth a refusal here rather than being left to fail later is WHERE it fails
later, which is three layers away from its cause. A redirect URI containing a space passes
the authorization endpoint’s exact-string match, the host’s resolver approves, an
authorization code is MINTED AND PERSISTED, and only then does building the Location
header fail: the user sees a 500, the client is never reached, and the code sits in storage
until it expires while every retry does it again. Refusing at registration turns that into
one error, at startup, in front of the person who wrote the value.
A StorageError rather than a new error type, and it is the honest reading rather than a
convenience: the answer is that this registration cannot be stored as given. The message
names the offending URI, because the caller is the operator who just supplied it.
RFC 8628 section 3.1/3.2: start a device authorization.
RFC 8628 section 3.1/3.2 for a client authenticating with any credential this server accepts, including an RFC 7523 assertion.
Added ALONGSIDE AuthorizationServer::device_authorization rather than replacing it: the
three-argument form is what every existing host already calls and a shared secret remains
the commonest credential. Both go through the same authenticate_client, so there is one
authentication path and not two.
Sourcepub async fn approve_device(
&self,
entered_user_code: &str,
subject: impl Into<String>,
) -> Result<(), DeviceApprovalError>
pub async fn approve_device( &self, entered_user_code: &str, subject: impl Into<String>, ) -> Result<(), DeviceApprovalError>
The host’s verification UI approves a grant for subject (the authenticated user).
§The host MUST rate limit calls to this
RFC 8628 section 5.1 is explicit that the user code’s entropy is sufficient only IN COMBINATION WITH rate limiting: the code is short because a human types it, and an unthrottled verification endpoint turns “short enough to type” into “short enough to enumerate”.
This crate CAN throttle this call, and does: the first thing this method does is
pending_grant_by_user_code, which asks the installed RateLimiter about an
crate::events::Attempt::DeviceUserCodeEntry BEFORE the code is looked up, and reports
the outcome back afterwards. DeviceApprovalError::RateLimited is what a refusal looks
like from here. A limiter is shipped (crate::rate_limit::FixedWindowRateLimiter), and
the crate’s own http service installs nothing by default, so a host that installs none
gets none. This paragraph said the opposite through 0.9.1 — “performs NO rate limiting and
cannot” — which was a promise of absence beside code that consults the throttle on its
first statement, and is the kind of doc that gets a host to build a second throttle or,
worse, to conclude the risk is unavoidable.
What remains the HOST’s, and it is the important half: this crate has no notion of a
caller, an IP, a session or a user, so it can only key the throttle on what it is given.
An attacker who spreads guesses across the whole code space from many sources is visible
to the host and not to this crate. Without a limiter installed, MIN_USER_CODE_LENGTH
symbols is a guessing exercise, not a credential.
Sourcepub async fn deny_device(
&self,
entered_user_code: &str,
) -> Result<(), DeviceApprovalError>
pub async fn deny_device( &self, entered_user_code: &str, ) -> Result<(), DeviceApprovalError>
The host’s verification UI records the user’s refusal.
The same RFC 8628 section 5.1 obligation as AuthorizationServer::approve_device applies:
this path also tells a caller whether a code exists, so the HOST must rate limit it too. An
attacker enumerating codes does not care which of the two endpoints answers.
Sourcepub fn token(
&self,
request: TokenRequest,
) -> impl Future<Output = Result<TokenResponse, ErrorResponse>> + '_
pub fn token( &self, request: TokenRequest, ) -> impl Future<Output = Result<TokenResponse, ErrorResponse>> + '_
The token endpoint (RFC 6749 section 3.2; device grant per RFC 8628 section 3.4/3.5), for a request that names no RFC 8707 resource indicator.
Equivalent to AuthorizationServer::token_with_resources with an empty list, which is
what a token request carrying no resource parameter means: no NARROWING is asked for, so
the issued token inherits whatever the grant already carries.
§THIS FUTURE IS NOT CANCELLATION SAFE, and what a drop costs
Applies equally to AuthorizationServer::token_with_resources and
AuthorizationServer::token_with_context, which are the same future.
A Rust future stops at whatever await it is suspended in when it is dropped, and it never
resumes. Two grants reached through here are TAKE-THEN-WRITE sequences, meaning they remove
a single-use credential from the store and then persist what that credential became. A drop
between the two leaves the first half done and nothing to finish it, and the crate cannot
make a dropped future complete: there is no destructor that can run an async store call.
So the CONTRACT is stated here rather than silently relied on, because until 0.9.1 a host
had no way to learn it.
Named exactly, because the cost differs:
authorization_code. The code is TAKEN (RFC 6749 s4.1.2’s one-time use), then a CONSUMED record is written, then the tokens are issued, then that record is updated with what they were. A drop between the take and the consumed write is the most expensive one in the crate: the code is gone, so it cannot be redeemed twice, but RFC 9700 s4.1.1 replay DETECTION works by recognising a code that was already redeemed, and this leaves no record to recognise. A later replay of a code that leaked into a log, aRefererheader or browser history reads as an unknown string, permanently and silently, for that grant. The write order was chosen to make a store FAILURE fall this way round rather than the other, and a drop lands in the same window that ordering shrank; it cannot close it.refresh_token. The presented token is TAKEN, then a SPENT record is written, then the rotated chain is issued. A drop between the take and the spent write destroys the client’s chain (the string it holds is gone and no replacement was persisted, so the user authenticates again) and loses the RFC 9700 s4.14.2 reuse marker for it, which is the same loss as above: a later presentation of that token is an unknown string rather than evidence of compromise, so it revokes no family.- The device grant’s successful poll takes its grant record before issuing, with the same shape and the same cost as the code path.
WHAT A HOST MUST DO. Drive this from a task the connection cannot cancel, and await THAT:
spawn the call and await the join handle, so a disconnecting client aborts the response and
not the work. This crate’s own axum adapter does exactly that; see crate::http. A host
that instead selects this future against a timeout, a shutdown signal, or a connection
watcher is choosing every cost listed above, and choosing it at whatever rate its clients
disconnect.
Sourcepub fn token_with_resources<'a>(
&'a self,
request: TokenRequest,
resources: &'a [String],
) -> impl Future<Output = Result<TokenResponse, ErrorResponse>> + 'a
pub fn token_with_resources<'a>( &'a self, request: TokenRequest, resources: &'a [String], ) -> impl Future<Output = Result<TokenResponse, ErrorResponse>> + 'a
The token endpoint with the RFC 8707 resource parameter.
resources is the (possibly repeated) resource parameter from the token request, in wire
order. It is a separate argument rather than a field on every TokenRequest variant on
purpose: RFC 8707 section 2 defines resource as a parameter of the token REQUEST,
independent of grant_type, so putting it on each variant would state the same thing four
times, grow the enum every host copies around, and make every future grant type repeat it
again.
What it does depends on the grant, and section 2 is what decides:
authorization_codeandrefresh_tokenmay NARROW to a subset of what the authorization request obtained, and never widen it;client_credentialshas no prior authorization request, so its resources are simply validated and used;urn:ietf:params:oauth:grant-type:device_coderefuses any resource withinvalid_target. The device authorization request (RFC 8628 section 3.1) does not acceptresourcein this crate yet, so there is nothing granted for a poll to narrow to, and inventing an audience at the token endpoint that the user never approved is exactly what the narrowing rule exists to prevent.
Sourcepub fn token_with_context<'a>(
&'a self,
request: TokenRequest,
context: TokenRequestContext<'a>,
) -> impl Future<Output = Result<TokenResponse, ErrorResponse>> + 'a
pub fn token_with_context<'a>( &'a self, request: TokenRequest, context: TokenRequestContext<'a>, ) -> impl Future<Output = Result<TokenResponse, ErrorResponse>> + 'a
The token endpoint with everything about the request that does not belong inside
TokenRequest: the RFC 8707 resource indicators, the RFC 7523 client assertion, and the
RFC 9449 DPoP proof.
AuthorizationServer::token and AuthorizationServer::token_with_resources are this
with an emptier context, so there is one implementation of the token endpoint and not three.
Both of them are plain functions returning THIS future rather than async fns that await
it, and that is a measurement rather than a style: an async fn wrapper is a second
generator frame holding its own copy of the 120-byte TokenRequest while the inner future
holds another, and adding one pushed the token future over tokio’s 2048-byte debug boxing
threshold. tests/allocation.rs caught it.
§Why THIS one is a plain function too, and not an async fn
The same measurement, one level down, and it is the largest single saving on this path.
An async fn stores its parameters TWICE: once as the coroutine’s upvars, which is where
they live before the first poll, and again as the locals they are moved into on that first
poll. rustc does not overlay the two, so request (120 bytes) and context (104 bytes)
were each counted twice for the whole life of the future. A plain function returning an
async move block captures each ONCE, as an upvar the body reads directly.
Measured on the RFC 6749 s4.1.3 arm, which is the widest: 2056 bytes as an async fn
against 1824 as a block, both --all-features. The first of those is past tokio’s
threshold and costs a 2 KB heap allocation on every single token request.
The client secret may be presented EITHER on the TokenRequest variant (where it has
always lived) or on TokenRequestContext::credential; the context wins when both are set,
and neither is silently dropped.
Validate an authorization request (RFC 6749 section 4.1.1) before any user interaction.
The order of checks is dictated by RFC 6749 section 4.1.2.1 and is a security boundary, not a style choice: the client and the redirect URI are validated FIRST, because until they are, there is no address the server may safely send an error to. Everything checked afterwards is reported by redirecting to the (now validated) URI.
On success the host shows its consent UI and then calls
AuthorizationServer::issue_authorization_code, or reports
ValidatedAuthorizationRequest::denied if the user refuses.
Mint an authorization code for a request the user has approved (RFC 6749 section 4.1.2).
RFC 6749 SECTION 10.12 IS THE REASON THIS TAKES A UserApproval AND NOT A SUBJECT.
Knowing WHO the user is does not establish that they agreed to anything. An authorization
endpoint that mints a code as soon as it can name the logged-in user issues one on any
cross-site top-level navigation that user’s browser can be made to follow, which is exactly
the cross-site request forgery section 10.12 describes: the attacker’s client, the victim’s
session, a code delivered to the attacker’s registered redirect URI. Nothing in this crate
can see a user, so nothing here can detect that; the only defence a library has is to
require the host to SAY that a resource owner approved this request, and to be unbuildable
without it.
Taking a ValidatedAuthorizationRequest (through the approval) rather than a raw request
is deliberate for the same reason one level down: an unvalidated request cannot reach code
issuance, because it cannot be spelled.
Available on crate feature consent only.
consent only.Mint an authorization code for a request the user has approved, holding the request’s RFC 9470 step-up requirement to the authentication the HOST reports it performed.
This is the enforcement half of RFC 9470, and it is a library job rather than a host job on
purpose: a max_age the host is trusted to check for itself is a max_age that gets checked
in whichever code path somebody remembered. The host still owns the authentication itself,
and authentication is its REPORT of one; this crate cannot verify that report and does not
pretend to. See the crate::consent module docs for the whole boundary.
A requirement the report does not satisfy is refused with RFC 9470 section 3’s
insufficient_user_authentication, delivered as a REDIRECT (RFC 6749 section 4.1.2.1):
by this point the redirect URI has been validated, and the client is both the party that
asked the question and the party that has to decide whether to send the user back to log in.
Nothing is minted and no consent is touched.
The approval means the same thing here as it does on
AuthorizationServer::issue_authorization_code, and is required for the same RFC 6749
section 10.12 reason: a satisfied acr_values says the user authenticated STRONGLY, never
that they agreed.
Sourcepub async fn introspect(
&self,
access_token: &str,
) -> Result<Option<Arc<IssuedToken>>, StorageError>
pub async fn introspect( &self, access_token: &str, ) -> Result<Option<Arc<IssuedToken>>, StorageError>
Opaque-token introspection: Ok(Some(_)) only for a known, unexpired token.
This is the host-facing form, which hands back the whole record. The RFC 7662 WIRE form is
AuthorizationServer::introspection_response, which answers the reduced, caller-scoped
document the RFC defines.
Sourcepub async fn introspection_response(
&self,
client_id: &ClientId,
client_secret: Option<&str>,
token: &str,
) -> Result<IntrospectionResponse, ErrorResponse>
pub async fn introspection_response( &self, client_id: &ClientId, client_secret: Option<&str>, token: &str, ) -> Result<IntrospectionResponse, ErrorResponse>
RFC 7662 token introspection, as the protected endpoint the RFC describes.
The caller must authenticate (section 2.1), and a token the caller has no relationship to
reads as inactive rather than as a description of somebody else’s grant: section 2.2 says
the response for an invalid token is simply active: false, and section 4 warns that this
endpoint otherwise becomes an oracle for probing tokens a caller does not hold.
§The two callers
Section 1 names a protected resource as the primary consumer, and section 2.1 permits a client to introspect its own token. Both are served:
- the token’s OWN CLIENT, which sees the whole record; and
- a RESOURCE SERVER declared in
ServerConfig::resource_servers, which sees a token only when the token’s RFC 8707IssuedToken::resourceset names one of the identifiers that resource server is registered for.
Everything else is {"active": false}, including a live token belonging to another client
and addressed to another resource server. A deployment that registers no resource servers
answers the token’s own client and nobody else, which is what this server did through
0.9.1.
The resource server is not a new kind of principal and gets no new credential: it registers
as an ordinary confidential client and authenticates here exactly as any client does. See
ServerConfig::resource_servers for why the authorization half is not optional – and
for what a resource server’s traffic costs the client-authentication rate limit, which is
the one thing about this endpoint that a host has to size rather than accept. A resource
server calls it once per request at the protected resource, and the default budget was
derived from a client’s token traffic.
§What a resource server is not told
RFC 7662 section 5: “omitting privacy-sensitive information from an introspection response is the simplest way of minimizing privacy issues”. The sensitive thing here is not only the user’s identity, which the resource server needs and gets. It is the SHAPE OF THE GRANT: which OTHER services this user’s token is good at. Two members carry that fact and both are narrowed to the asking resource server:
aud, to the RFC 8707 identifiers that resource server is registered for; andauthorization_details, to the RFC 9396 section 2.2 elements whoselocationsname one of those identifiers, or that carry nolocationsat all. A kept element has its ownlocationsnarrowed too, so an element addressed to two resource servers does not smuggle the second one’s URI past the filter. Section 9.2 asks for precisely this (“filtered and extended for the RS making the introspection request”), and section 9.1 says the same of the JWT form.
An earlier 0.9.2 draft narrowed aud and shipped authorization_details whole, which meant
the disclosure
the first refused was re-made verbatim by the second, with the actions and privileges
granted elsewhere attached. That is fixed rather than accepted, and the alternative
resolution – STOP NARROWING aud, and treat a registered resource server as a semi-trusted
party that sees the grant as granted – was rejected. A resource server is registered for
the identifiers it answers for and nothing wider; a deployment adding a second protected
resource would otherwise be silently telling the first one about it, and the party who pays
for that is the user, who is not present and cannot be asked. Consistency in the other
direction is cheaper to buy and costs somebody else.
Two members are NOT narrowed, and the omission is a decision rather than an oversight:
scopeis the whole grant’s scope set. Nothing in this crate maps a scope to a resource server – there is no per-resource catalogue to filter against – so any narrowing would be a guess at which strings “belong” to the asker, and a resource server that silently loses a scope refuses access the resource owner granted. A scope is also a token in the deployment’s own vocabulary; unlike alocationsURI it does not NAME another service.act(RFC 8693 section 4.1) describes who is acting in the call this resource server is handling, not where else the grant reaches.
The cost of the narrowing, stated plainly: a resource server given a filtered
authorization_details cannot distinguish “not granted” from “not for you”. That is the
same indistinguishability the aud narrowing already imposes, and it is the harmless
direction – both readings oblige the resource server to refuse, because an element it is
not named in is one it must not act on either way. The disclosure direction has no such
symmetry. A caller that needs the unfiltered record is the token’s OWN CLIENT, and it still
gets it; so does a host, through AuthorizationServer::introspect.
Sourcepub async fn introspection_response_with_credential(
&self,
client_id: &ClientId,
cred: &ClientCredential<'_>,
token: &str,
) -> Result<IntrospectionResponse, ErrorResponse>
pub async fn introspection_response_with_credential( &self, client_id: &ClientId, cred: &ClientCredential<'_>, token: &str, ) -> Result<IntrospectionResponse, ErrorResponse>
RFC 7662 introspection for a caller authenticating with any credential this server accepts,
including an RFC 7523 assertion. See
AuthorizationServer::device_authorization_with_credential on why this is an addition
rather than a replacement.
Sourcepub async fn record_consent(
&self,
client_id: &ClientId,
subject: &str,
scope: &ScopeSet,
resource: &[String],
authentication: Option<Authentication>,
) -> Result<ConsentRecord, StorageError>
Available on crate feature consent only.
pub async fn record_consent( &self, client_id: &ClientId, subject: &str, scope: &ScopeSet, resource: &[String], authentication: Option<Authentication>, ) -> Result<ConsentRecord, StorageError>
consent only.Record that a resource owner has consented to a client acting for them.
One live consent per (client, subject) pair: an existing record is WIDENED in place, keeping
its identifier and its original granted_at, so a user who approves one more scope next
month still sees one entry rather than two and withdrawing it withdraws the whole
relationship. See crate::consent::ConsentRecord::extend.
The library NEVER calls this for itself. Recording consent is a statement that a user agreed to something, and this crate has no way to know that: it never sees a user. The host calls it once its own consent step has actually been answered.
§Concurrency, and what this used to concede
This is a read-modify-write: it looks for an existing record, widens it, and writes it
back. It is now a COMPARE-AND-SWAP against what it read
(Storage::compare_and_swap_consent), retried once, and both halves of that matter.
The half this doc used to argue away was two overlapping FIRST approvals each finding nothing and each creating a record, leaving the pair with two. The argument was that the records are additive so the damage is a possible re-prompt. That much was true, and it is no longer relevant: the create is conditional on the pair still being empty, so the loser sees the winner’s record and widens it instead.
The half this doc never considered is the one that was not benign, and it is the reason
this changed. The second writer it reasoned about was another record_consent. The writer
that actually mattered was AuthorizationServer::withdraw_consent: a widen that read a
record, and wrote it back after the user clicked withdraw, RESURRECTED a consent the user
had destroyed, and every later authorization request was answered from it. “Benign in the
direction that matters” was a statement about the wrong direction. See the resurrection
rule in the crate::store module docs.
A host no longer owes this path a lock of its own.
Sourcepub async fn remembered_consent(
&self,
client_id: &ClientId,
subject: &str,
) -> Result<Option<Arc<ConsentRecord>>, StorageError>
Available on crate feature consent only.
pub async fn remembered_consent( &self, client_id: &ClientId, subject: &str, ) -> Result<Option<Arc<ConsentRecord>>, StorageError>
consent only.The consent this user has already given this client, if any.
This ANSWERS a question; it does not make a decision, and nothing in this crate approves an
authorization request on the strength of it. See the http feature’s
ServiceBuilder::with_approval_resolver: the library reports what it remembers
and the host decides what that is worth, because “the user agreed to this once” and “the
user agrees to this now” are different sentences and only the host can tell them apart.
Sourcepub async fn consents_for_subject(
&self,
subject: &str,
) -> Result<Vec<Arc<ConsentRecord>>, StorageError>
Available on crate feature consent only.
pub async fn consents_for_subject( &self, subject: &str, ) -> Result<Vec<Arc<ConsentRecord>>, StorageError>
consent only.Everything one resource owner has consented to, so a host can show a user what they have granted. Without this a user cannot SEE what they gave away, which is half of why this feature exists at all.
Sourcepub async fn withdraw_consent(
&self,
consent_id: &str,
) -> Result<u64, StorageError>
Available on crate feature consent only.
pub async fn withdraw_consent( &self, consent_id: &str, ) -> Result<u64, StorageError>
consent only.WITHDRAW a consent, revoking everything issued under it. Returns how many records the cascade removed.
This is the point of the whole feature. A withdrawal that left tokens alive would be worse
than no withdrawal at all, because the user would believe they had stopped something they
had not, so the cascade is one storage operation
(crate::store::Storage::revoke_consent) and it reaches every family the consent ever
produced, plus the authorization codes and approved-but-unpolled device grants that would
otherwise mint tokens seconds later.
Withdrawing a consent that is already gone is Ok(0), not an error.
Sourcepub async fn revoke(
&self,
client_id: &ClientId,
client_secret: Option<&str>,
token: &str,
token_type_hint: Option<TokenTypeHint>,
) -> Result<(), ErrorResponse>
pub async fn revoke( &self, client_id: &ClientId, client_secret: Option<&str>, token: &str, token_type_hint: Option<TokenTypeHint>, ) -> Result<(), ErrorResponse>
RFC 7009 token revocation.
Returns Ok(()) when the token is gone, INCLUDING when it never existed: section 2.2
requires a 200 for an unknown token, because distinguishing “revoked” from “never heard of
it” would let an unauthenticated caller test whether a token string is real.
token_type_hint (section 2.1) is an optimisation, not a constraint: the RFC requires the
server to keep looking if the hint is wrong, so a wrong hint costs a second lookup and
nothing else.
PUBLIC CLIENTS MAY REVOKE THEIR OWN TOKENS here, presenting a client_id and no secret,
which is section 2.1’s own rule (“in case of a confidential client” scopes the credential
check) and section 5’s (“a valid client_id, in the case of a public client”). What stops
a caller who merely knows a public client’s id is the OWNERSHIP check, made against the
stored record: another client’s token is untouched, and answered Ok(()) all the same.
This is deliberately NOT what
introspection_response does; see the
comment inside revoke_with_credential for
why the two RFCs differ.
§THIS FUTURE IS NOT CANCELLATION SAFE, and what a drop costs
Applies equally to
revoke_with_credential, which is the same
future. A dropped future stops at whatever await it was suspended in and never resumes,
and this crate cannot make it finish: there is no destructor that can run an async store
call. So the contract is stated rather than left to be discovered.
Revoking a REFRESH token is a two-write sequence: the RFC 7009 s2.1 cascade over the
grant’s family, and the removal of the presented string. A drop between them leaves the
family revoked, with a barrier recorded, and one live-LOOKING refresh string that names a
family nothing will honour. That is fail-closed on purpose, and it is why the cascade runs
first; the opposite order was worse than an incomplete revocation, because the client’s
RETRY found the presented string already gone and answered 200 without cascading at all,
leaving every access token of a grant the user had logged out of live for its whole TTL.
A retry after a drop now still reaches the cascade, and the cascade is idempotent
(crate::store::Storage::revoke_token_family). tests/revocation_cancellation.rs pins
it.
WHAT IS STILL LOST to a drop, stated rather than implied: the crate::events::Event the
completed call would have emitted, so an audit trail can miss a revocation that partly
happened. A host that needs the sequence to complete must drive it from a task the
connection cannot cancel, spawning the call and awaiting the join handle, which is what this
crate’s own axum adapter does; see crate::http.
Sourcepub async fn revoke_with_credential(
&self,
client_id: &ClientId,
cred: &ClientCredential<'_>,
token: &str,
token_type_hint: Option<TokenTypeHint>,
) -> Result<(), ErrorResponse>
pub async fn revoke_with_credential( &self, client_id: &ClientId, cred: &ClientCredential<'_>, token: &str, token_type_hint: Option<TokenTypeHint>, ) -> Result<(), ErrorResponse>
RFC 7009 revocation for a caller authenticating with any credential this server accepts,
including an RFC 7523 assertion. See
AuthorizationServer::device_authorization_with_credential on why this is an addition
rather than a replacement.
Trait Implementations§
Source§impl<S: Storage, C: Clock> TokenExchange for AuthorizationServer<S, C>
Available on crate feature token-exchange only.
impl<S: Storage, C: Clock> TokenExchange for AuthorizationServer<S, C>
token-exchange only.Source§async fn exchange_token(
&self,
request: &TokenExchangeRequest<'_>,
) -> Result<ExchangedToken, ErrorResponse>
async fn exchange_token( &self, request: &TokenExchangeRequest<'_>, ) -> Result<ExchangedToken, ErrorResponse>
subject_token for a new token, per RFC 8693 section 2. Read more