Skip to main content

AuthorizationService

Struct AuthorizationService 

Source
pub struct AuthorizationService<S: Storage, C: Clock> { /* private fields */ }
Available on crate feature http only.
Expand description

An RFC-shaped authorization server as an HTTP service.

Built by ServiceBuilder. Cheap to clone (one refcount bump) and safe to share, so a host clones one per connection or per task without duplicating any of the state or re-serializing anything.

§Mounting it

With the axum feature, axum::Router::from(service) is the whole wiring. Without it, call handle from whatever the host’s own server hands it: it takes an http::Request over any http_body::Body and answers with an http::Response<Body>.

Implementations§

Source§

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

Source

pub async fn handle<B>(&self, request: Request<B>) -> Response
where B: Body,

Answer one request.

Generic over the request body so that a host on any HTTP server can call it: hyper, axum, a test harness holding a String. The body is read whole, up to 64 KiB, before it is parsed, which is what these endpoints require (client authentication for client_secret_post is IN the body, so nothing can be checked before it has all arrived).

Trait Implementations§

Source§

impl<S: Storage, C: Clock> Clone for AuthorizationService<S, C>

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl<S: Storage, C: Clock> Debug for AuthorizationService<S, C>

Source§

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

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

impl<S, C> From<AuthorizationService<S, C>> for Router
where S: Storage + Send + Sync + 'static, C: Clock + Send + Sync + 'static,

Mount the service on axum.

This is the ENTIRE axum surface of this crate, and it is one function on purpose. axum is a 0.x crate: its major has moved before and will move again, and every earlier version of this module put axum::Router in the return type of the only way to use the http feature, which meant a host on a different axum major could not enable the feature at all. Confining axum to an adapter behind its own feature makes that a per-host decision instead of this crate’s.

A fallback rather than a route per endpoint: the route table is DERIVED from the metadata document at build time, so re-declaring it here in axum’s syntax would create a second table that could disagree with the first. A 404 from AuthorizationService::handle is a path this server does not serve, which is exactly what a fallback means.

§Why the request is answered on a spawned task

BECAUSE A CLIENT THAT HANGS UP MUST NOT BE ABLE TO STOP THE SERVER MID-SEQUENCE. hyper drops the service future when the connection closes, and a dropped future does not fail: the code after the .await it was suspended on simply never runs. crate::server has no transactions (Storage deliberately offers none), so several of its sequences are an atomic TAKE followed by a write, and every one of those arguments about which way the pair fails assumes that a failure HAPPENS. The refresh rotation is the sharp case: Storage::take_refresh_token removes the chain and the spent marker that arms RFC 9700 s4.14.2 reuse detection is written after it, so a drop in between leaves the chain gone with no marker, which is the exact state that ordering exists to prevent. The authorization code path has the same shape with its consumed record. Neither is a race an attacker has to win by timing: whoever presents the credential is whoever decides when to close the socket.

Awaiting a JoinHandle moves the cancellation to the RIGHT place. The client’s disconnect cancels this adapter’s await on the handle; the spawned task keeps its own place in the runtime and runs the store sequence to the end. Nothing else in this crate can do this, because the http feature deliberately pulls in no runtime; the axum feature is the one place a runtime is already present (axum = ["http", "dep:axum", "dep:tokio"]), so it is the one place this can be contained. A HOST MOUNTING AuthorizationService::handle ITSELF OWNS THIS, and should spawn for the same reason.

§What a host may notice

IN-FLIGHT WORK IS NO LONGER BOUNDED BY CONNECTIONS. That is the point of the spawn and it is also its cost: a client that hangs up stops waiting but no longer stops the work, so axum’s and hyper’s connection limits, and any accept-side bound the host set, no longer bound the tasks this service is running. The bound becomes request RATE times handler latency, and NEITHER FACTOR IS THIS CRATE’S TO SET.

The rate half is the stronger one: the limiter runs INSIDE AuthorizationService::handle and refuses before the store is touched, so a refused request costs a spawn and nothing more. It is still not a global ceiling — the budgets crate::rate_limit ships for the endpoints that name a client are keyed per client_id, which RFC 6749 section 2.2 makes public, so a caller spraying identifiers gets a budget apiece up to crate::rate_limit::DEFAULT_MAX_TRACKED_CLIENTS counters before the rest share an overflow counter.

The latency half is the host’s outright. A handler makes a bounded NUMBER of store calls, but each one is the host’s crate::store::Storage and this crate sets no timeout anywhere, on anything; the token path additionally awaits crate::jwt::Es256Signer, which that trait’s own docs say may be a network round trip to a KMS. Nor is the latency all waiting: a host-installed crate::client::SecretVerifier runs its KDF INLINE on the executor thread polling the request — that trait prices argon2id at ordinary parameters at roughly 200 ms, paid per token request and on the unknown-client path too — so it occupies a worker rather than yielding it. A host that wants a hard ceiling should take a semaphore permit before the spawn, or spawn into a JoinSet it owns, and answer 503 when it cannot get one.

A PANIC in a handler no longer unwinds into hyper. It arrives here as a JoinError and is answered with an empty 500, which is what the panicking connection produced anyway, minus the connection dying with it. RUNTIME SHUTDOWN is the other JoinError: a task cancelled because its runtime is going away answers the same 500. The two are not distinguished on the wire on purpose, because they are the same news to the client (this request did not complete and it does not know whether anything happened), and both are already visible to the host: a panic through its own hook, a shutdown because it asked for one.

§Cost

One tokio::spawn per request, which is ONE allocation: measured with tests/support/alloc.rs on aarch64-apple-darwin at 1 alloc and 128 bytes for a trivial task, the block sized by the task header plus the handler future. tests/allocation.rs budgets the REQUEST path, which this does not touch: nothing inside AuthorizationService::handle changes, and the token endpoint’s own budget there is two orders of magnitude larger than one task. It buys the store sequence the right to finish.

The other half of the cost is not an allocation. Detaching the handler from the connection means in-flight work is bounded by request RATE rather than by concurrent connections: a client that disconnects immediately after sending no longer sheds any load, because the handler it started runs to completion regardless. That is the same property that buys the store sequence its right to finish, seen from the load side. A host that relied on disconnects for backpressure needs a concurrency limit in front of this service — tower::limit or the equivalent — and the rate limiter this crate already has does not substitute for one, because it refuses attempts rather than bounding work already accepted.

Source§

fn from(service: AuthorizationService<S, C>) -> Router

Converts to this type from the input type.

Auto Trait Implementations§

§

impl<S, C> !RefUnwindSafe for AuthorizationService<S, C>

§

impl<S, C> !UnwindSafe for AuthorizationService<S, C>

§

impl<S, C> Freeze for AuthorizationService<S, C>

§

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

§

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

§

impl<S, C> Unpin for AuthorizationService<S, C>

§

impl<S, C> UnsafeUnpin for AuthorizationService<S, C>

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> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> FromRef<T> for T
where T: Clone,

Source§

fn from_ref(input: &T) -> T

Converts to this type from a reference to the input type.
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> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
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.