Skip to main content

AuthorizationServer

Struct AuthorizationServer 

Source
pub struct AuthorizationServer<S: Storage, C: Clock = SystemClock> { /* private fields */ }
Expand description

The authorization server. Generic over the host’s Storage and (for tests) the Clock.

Implementations§

Source§

impl<S: Storage, C: Clock> AuthorizationServer<S, C>

Source

pub async fn pushed_authorization_request( &self, client_id: &ClientId, client_secret: Option<&str>, parameters: &[(&str, &str)], ) -> Result<PushedAuthorizationResponse, ErrorResponse>

Available on crate feature 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).

Source

pub async fn pushed_authorization_request_with_credential( &self, client_id: &ClientId, credential: &ClientCredential<'_>, parameters: &[(&str, &str)], ) -> Result<PushedAuthorizationResponse, ErrorResponse>

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

Source

pub async fn validate_pushed_authorization_request( &self, client_id: &str, request_uri: &str, ) -> Result<ValidatedAuthorizationRequest, AuthorizationError>

Available on crate feature 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).

Source

pub async fn validate_signed_authorization_request( &self, client_id: &str, request_object: &str, ) -> Result<ValidatedAuthorizationRequest, AuthorizationError>

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

Source

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.

Source

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.

Source

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.

Source

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>

Source

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>

Source

pub fn with_clock(config: ServerConfig, store: S, clock: C) -> Self

Construct with an injected clock (tests).

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn with_request_object_keys(self, keys: Box<dyn RequestObjectKeys>) -> Self

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

Source

pub fn with_es256_verifier(self, verifier: Arc<dyn Es256Verifier>) -> Self

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

Source

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.

Source

pub fn jwks(&self) -> Option<Jwks>

Available on crate feature jwt only.

The RFC 7517 key set to serve at jwks_uri, or None when tokens are opaque. PUBLIC key parameters only.

Source

pub fn jwks_uri(&self) -> Option<&str>

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

Source

pub fn config(&self) -> &ServerConfig

The configuration.

Source

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.

Source

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.

Source

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.

Source

pub async fn device_authorization( &self, client_id: &ClientId, client_secret: Option<&str>, requested_scope: Option<&ScopeSet>, ) -> Result<DeviceAuthorizationResponse, ErrorResponse>

RFC 8628 section 3.1/3.2: start a device authorization.

Source

pub async fn device_authorization_with_credential( &self, client_id: &ClientId, cred: &ClientCredential<'_>, requested_scope: Option<&ScopeSet>, ) -> Result<DeviceAuthorizationResponse, ErrorResponse>

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.

Source

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.

Source

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.

Source

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, a Referer header 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.

Source

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_code and refresh_token may NARROW to a subset of what the authorization request obtained, and never widen it;
  • client_credentials has no prior authorization request, so its resources are simply validated and used;
  • urn:ietf:params:oauth:grant-type:device_code refuses any resource with invalid_target. The device authorization request (RFC 8628 section 3.1) does not accept resource in 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.
Source

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.

Source

pub async fn validate_authorization_request( &self, request: &AuthorizationRequest<'_>, ) -> Result<ValidatedAuthorizationRequest, AuthorizationError>

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.

Source

pub async fn issue_authorization_code( &self, approval: UserApproval<'_>, ) -> Result<AuthorizationResponse, AuthorizationError>

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.

Source

pub async fn issue_authorization_code_with_authentication( &self, approval: UserApproval<'_>, requirement: &AuthenticationRequirement, authentication: Option<&Authentication>, ) -> Result<AuthorizationResponse, AuthorizationError>

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

Source

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.

Source

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 8707 IssuedToken::resource set 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; and
  • authorization_details, to the RFC 9396 section 2.2 elements whose locations name one of those identifiers, or that carry no locations at all. A kept element has its own locations narrowed 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:

  • scope is 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 a locations URI 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.

Source

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.

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

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

Source

pub async fn consents_for_subject( &self, subject: &str, ) -> Result<Vec<Arc<ConsentRecord>>, StorageError>

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

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

Source

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.

Source

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.
Source§

async fn exchange_token( &self, request: &TokenExchangeRequest<'_>, ) -> Result<ExchangedToken, ErrorResponse>

Exchange subject_token for a new token, per RFC 8693 section 2. Read more

Auto Trait Implementations§

§

impl<S, C = SystemClock> !RefUnwindSafe for AuthorizationServer<S, C>

§

impl<S, C = SystemClock> !UnwindSafe for AuthorizationServer<S, C>

§

impl<S, C> Freeze for AuthorizationServer<S, C>
where S: Freeze, C: Freeze,

§

impl<S, C> Send for AuthorizationServer<S, C>

§

impl<S, C> Sync for AuthorizationServer<S, C>

§

impl<S, C> Unpin for AuthorizationServer<S, C>
where S: Unpin, C: Unpin,

§

impl<S, C> UnsafeUnpin for AuthorizationServer<S, C>
where S: UnsafeUnpin, C: UnsafeUnpin,

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<A, B, T> HttpServerConnExec<A, B> for T
where B: Body,

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> Same for T

Source§

type Output = T

Should always be Self
Source§

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

Source§

type Error = Infallible

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

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

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.