Expand description
An embeddable OAuth 2.1 Authorization Server library.
This crate is the AUTHORIZATION SERVER half of OAuth: it registers clients, runs grant state
machines, issues and introspects tokens, and produces exactly the wire shapes the RFCs define.
It is a LIBRARY, not a server binary: the host owns the HTTP listener, TLS, persistence, and
the rate-limiting policy (a per-process floor ships here — see rate_limit — but only the
host has a caller identity to key on, and only a host installs it). The host hands request parameters to server::AuthorizationServer and
serializes the returned response/error types (they carry their own serde shapes and HTTP
status codes), or turns on the optional http feature and gets that wire surface written for
it.
§What is here
ALWAYS COMPILED, no feature and no dependency beyond serde, sha2, base64 and getrandom:
- Protocol types mirroring the specs in OUR OWN structs (a deliberate project rule; no third
party’s generated types):
client::Client,grant::GrantType,token::TokenResponse,scope::ScopeSet,authorization::AuthorizationRequest, and the RFC 6749 section 5.2 / RFC 8628 section 3.5 error object (error::ErrorResponse). - The authorization code grant with MANDATORY PKCE (
authorization), the OAuth 2.1 stance: validation, single-use codes, exact redirect-URI matching, and replay detection that revokes the whole issued family. - The RFC 8628 device authorization grant, as a full state machine (
device):authorization_pending,slow_down(with the mandated 5 second interval increase),expired_token,access_denied, single-use redemption, and user-code normalization per RFC 8628 section 6.1. - Token issuance, introspection, revocation, and refresh rotation (
token): single use, with an absolute family lifetime. - The RFC 8414 metadata document (
metadata) and RFC 7591 dynamic client registration (registration), the latter off unless configured and refusing every registration until a policy is installed. - PKCE S256 primitives (
pkce), verified against the RFC 7636 appendix B vector. - A storage seam (
store::Storage) the HOST implements, plusstore::MemoryStoragefor tests and single-process embedding. This crate never assumes what the host’s store looks like.
OPTIONAL, each its own cargo feature and every one of them off by default:
http, an HTTP service over all of the above, inhttp1.x andhttp-body1.x with no web framework and no async runtime.axumadds a thinimpl From<..> for axum::Routeradapter for hosts that want one; nothing else in the crate knows axum exists.jwt(RFC 9068at+jwtaccess tokens and an RFC 7517 JWKS document),dpop(RFC 9449 sender-constrained tokens),mtls(RFC 8705 certificate-bound tokens and client authentication),client-assertion(RFC 7523private_key_jwtandclient_secret_jwt).jwt-p256, THE BUILT-IN ES256 BACKEND, and the one to reach for first:jwtcompiles the RFC 7515 machinery and thejwt::Es256Signer/jwt::Es256Verifierseams but SIGNS NOTHING BY ITSELF, because where a private key lives is the host’s decision (see thejwtmodule docs).jwt-p256suppliesjwt::EcdsaP256Keyand installsjwt::P256Verifieras the default, which is what a host with no opinion about its key wants; a host with an HSM or a KMS installs its own signer instead and takes nop256. The features are ADDITIVE rather than exclusive, so both at once compiles and the installed signer wins.jwt-pkcs8adds PKCS#8 DER loading on top ofjwt-p256, for a host whose key arrives as the DER its KMS oropensslalready emits rather than as a raw scalar.parandjar(RFC 9126 pushed authorization requests, RFC 9101 signed request objects),rar(RFC 9396authorization_details),token-exchange(RFC 8693),consent(consent records, withdrawal with a revocation cascade, and RFC 9470 step-up authentication),resource-metadata(RFC 9728).cimd(draft-ietf-oauth-client-id-metadata-document-01 client identifier metadata documents), which is VALIDATION ONLY: the HOST fetches the document at the client identifier URL and hands this crate the bytes, because this crate makes no outbound HTTP request. Seecimdfor the full list of what that leaves with the host.test-util, a RUNNABLE conformance harness for thestore::Storagecontract that a host runs from its own test suite against its OWN store.
On docs.rs every item above is rendered with the feature that turns it on. In a local build,
cargo doc --all-features is the equivalent view.
§Quick start
Four things a host owns, and none of them can be defaulted: a config, a store, a sweep, and the two seams the interactive endpoints refuse without.
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use oauth_as::{
ApprovalDecision, AuthorizationServer, MemoryStorage, ServerConfig, ServiceBuilder, Storage,
};
// The issuer identity, and where a user goes to type an RFC 8628 device code.
let config = ServerConfig::new("https://as.example.com", "https://as.example.com/device");
// MemoryStorage is single process. A multi-node host implements `Storage` itself and proves
// its `take_*` really is an atomic remove-and-return with the `test-util` harness.
let server = Arc::new(AuthorizationServer::new(config, MemoryStorage::new()));
// THE SWEEP. Nothing in this crate reclaims an expired record; this task is the only thing
// that does, and the device authorization endpoint takes no credential, so an unswept store
// grows at a rate an attacker chooses.
let sweeper = Arc::clone(&server);
tokio::spawn(async move {
loop {
let _ = sweeper.store().sweep_expired(SystemTime::now()).await;
tokio::time::sleep(Duration::from_secs(60)).await;
}
});
// Both seams are REQUIRED: with no consent resolver the authorization endpoint answers 403
// rather than deciding on the user's behalf. Returning `Approve` unconditionally, as here, is
// an AUTO-APPROVING authorization server (RFC 6749 s10.12); a real host reads its own session
// here and returns `ApprovalDecision::Respond` with a consent screen. See
// `examples/production_server.rs` for both done properly, plus CSRF and audit.
let service = ServiceBuilder::new(server)
.with_subject_resolver(|_headers| Some("user-1".to_string()))
.with_approval_resolver(|_request| ApprovalDecision::Approve)
.build()?;§Host seams: observation, throttling, and secret storage
Three things a real deployment needs that this library deliberately does not do itself, each
installed on the server and each costing an uninstalled host nothing (see events::Hooks):
- AUDIT EVENTS (
events::EventSink). This crate logs nothing on its own. A host that wants to see issuance, refusal, or the two compromise events (authorization code replay, refresh token reuse) installs a sink. Events carry no credential of any kind; see theeventsmodule docs for the rule and for why the refreshfamily_idis safe to carry. - RATE LIMITING (
events::RateLimiter). This crate never sees a request, so it has no IP, TLS peer, session or caller identity to key a counter on, and a throttle keyed on the things this crate DOES see is the only kind it can write. It writes that one:rate_limit::FixedWindowRateLimiteris a per-process, in-memory floor, and the device path already consults it —server::AuthorizationServer::approve_deviceasks the installed limiter on its first statement, BEFORE the user code is looked up, which is what RFC 8628 section 5.1 means when it makes the user code’s entropy adequate only IN COMBINATION WITH rate limiting of code entry. What remains the host’s is the part a library cannot do: the limiter must be INSTALLED (server::AuthorizationServer::with_rate_limiter) — nothing is throttled until it is — and a multi-node deployment still owes a SHARED limiter behind the same trait, because a per-process count of an attacker spread overNnodes isNtimes too generous. - CLIENT SECRET STORAGE (
client::SecretHash,client::SecretVerifier). Hosts should store a one-way verifier, not the secret. The built-in scheme needs no host code; a host whose policy names argon2id or an HSM installs a verifier. - WHEN AND HOW THE USER LOGGED IN (the
consentmodule’sAuthentication, behind theconsentfeature). This crate cannot authenticate anybody and will not grow a login page, so a host that wants RFC 9470 step-up authentication REPORTS when and how it authenticated the user; the library records that report and enforcesmax_ageandacr_valuesagainst it. The report is taken at face value, because there is nothing here that could check it. See theconsentmodule docs for the whole boundary. - WHO MAY REGISTER (
registration::RegistrationPolicy). RFC 7591 dynamic client registration is OFF unlessserver::ServerConfig::registrationis set, and even then every registration is REFUSED until a policy is installed. RFC 7591 section 5: an open registration endpoint lets anyone on the internet mint a client, which weakens every threat model that assumed controlling a registered client was hard. See theregistrationmodule docs before enabling it.
§Zero cost until enabled
A host that compiles this crate in but never turns it on must pay nothing at runtime. The crate
keeps that promise structurally: there are NO global statics, NO lazy singletons, NO background
tasks, and no allocation at load time. The only allocation entry point is
server::AuthorizationServer::new (plus whatever Storage the host constructs to pass in),
so “enabled by config” for a host means exactly “construct the value when config says so”.
§THE SWEEP: an obligation that comes with the no-background-tasks promise
“No background tasks” has a price and the HOST pays it. Nothing here reclaims an expired
record; store::Storage::sweep_expired does, and it runs when the host calls it and at no
other time. A host that never calls it has a store that only grows, and the growth is
ATTACKER-PACED: the RFC 8628 section 3.1 device authorization endpoint takes no credential
from a public client, so anyone who can open a socket can allocate a device grant per request
forever. Expiry is enforced on read, so an unswept deployment is not insecure, it is unbounded,
which is a process that dies rather than a grant that leaks.
So: spawn one task per process, on an interval well under the shortest artifact lifetime
(server::ServerConfig::device_code_ttl, 600 seconds by default), log a failure and retry on
the next tick rather than returning. examples/production_server.rs does exactly that, with
the reasoning, alongside every other seam a real deployment has to wire.
§A worked production wiring
examples/production_server.rs is the one to copy: it wires the storage contract, a rate
limiter, a real consent screen, the device form’s CSRF tokens, a session-backed subject
resolver, the sweeper, an audit sink and signing key management, and says at each site what
breaks if you get it wrong. examples/conformance_server.rs is NOT: it is a fixture for the
black-box harness, and it auto-approves consent, disables the CSRF protection and signs with a
key printed in an RFC.
§Concurrency contract
Single-use artifacts (device codes at redemption, rotating refresh tokens) are consumed through
the storage trait’s atomic take_* operations. store::MemoryStorage satisfies the contract
with a mutex; a multi-node host must back take_* with a genuinely atomic remove-and-return
(compare-and-set, DELETE ... RETURNING, or equivalent) or single-use guarantees become
per-node only.
Re-exports§
pub use authorization::AuthorizationCodeRecord;pub use authorization::AuthorizationCodeState;pub use authorization::AuthorizationError;pub use authorization::AuthorizationErrorRedirect;pub use authorization::AuthorizationRequest;pub use authorization::AuthorizationResponse;pub use authorization::CodeChallengeMethod;pub use authorization::ResponseType;pub use authorization::ValidatedAuthorizationRequest;pub use cimd::CimdError;cimdpub use cimd::CimdPolicy;cimdpub use cimd::ClientIdUrl;cimdpub use cimd::ValidatedClientIdDocument;cimdpub use cimd::MAX_CLIENT_ID_DOCUMENT_BYTES;cimdpub use cimd::MAX_CLIENT_ID_URL_BYTES;cimdpub use client::Client;pub use client::ClientAuth;pub use client::ClientId;pub use client::DynamicRegistration;pub use client::SecretHash;pub use client::SecretVerifier;pub use client_assertion::AssertionFailure;client-assertionpub use client_assertion::AssertionKeys;client-assertionpub use client_assertion::VerifiedAssertion;client-assertionpub use client_assertion::CLIENT_ASSERTION_TYPE;client-assertionpub use client_assertion::CLIENT_SECRET_JWT;client-assertionpub use client_assertion::MAX_ASSERTION_LIFETIME;client-assertionpub use client_assertion::MIN_CLIENT_SECRET_JWT_KEY_LENGTH;client-assertionpub use client_assertion::PRIVATE_KEY_JWT;client-assertionpub use consent::step_up_challenge;consentpub use consent::Authentication;consentpub use consent::AuthenticationRequirement;consentpub use consent::ConsentRecord;consentpub use consent::StepUpFailure;consentpub use consent::MAX_CONSENT_RESOURCES;consentpub use device::DeviceAuthorizationResponse;pub use device::DeviceGrant;pub use device::DeviceGrantState;pub use dpop::DpopFailure;dpoppub use dpop::VerifiedProof;dpoppub use dpop::DPOP_HEADER;dpoppub use dpop::DPOP_TOKEN_TYPE;dpoppub use dpop::MAX_PROOF_AGE;dpoppub use dpop::MAX_PROOF_BYTES;dpoppub use error::ErrorCode;pub use error::ErrorResponse;pub use events::Attempt;pub use events::AttemptOutcome;pub use events::ClientAuthFailure;pub use events::Event;pub use events::EventSink;pub use events::Hooks;pub use events::RateLimitDecision;pub use events::RateLimiter;pub use grant::GrantType;pub use http::ApprovalDecision;httppub use http::ApprovalRequest;httppub use http::ApprovalResolver;httppub use http::AuthorizationService;httppub use http::Body;httppub use http::CsrfTokenHook;httppub use http::Response;httppub use http::ServiceBuilder;httppub use http::ServiceError;httppub use http::SubjectResolver;httppub use http::MAX_BODY_BYTES;httppub use http::MAX_FORM_PARAMETERS;httppub use http::AuthenticationReporter;consentandhttppub use jwt::AccessTokenFormat;jwtpub use jwt::Audience;jwtpub use jwt::Es256Signer;jwtpub use jwt::Es256Verifier;jwtpub use jwt::Jwk;jwtpub use jwt::Jwks;jwtpub use jwt::JwtConfig;jwtpub use jwt::JwtError;jwtpub use jwt::PublicJwk;jwtpub use jwt::SignerError;jwtpub use jwt::VerifyError;jwtpub use jwt::EcdsaP256Key;jwt-p256pub use jwt::KeyError;jwt-p256pub use jwt::P256Verifier;jwt-p256pub use metadata::well_known_path;pub use metadata::AuthorizationServerMetadata;pub use metadata::WELL_KNOWN_PATH;pub use mtls::CertificateThumbprint;mtlspub use mtls::ClientCertificate;mtlspub use mtls::ExpectedSubject;mtlspub use mtls::MtlsClientRegistration;mtlspub use mtls::MtlsRegistrationError;mtlspub use mtls::RegisteredCertificates;mtlspub use mtls::SELF_SIGNED_TLS_CLIENT_AUTH;mtlspub use mtls::TLS_CLIENT_AUTH;mtlspub use mtls::TLS_CLIENT_AUTH_SAN_DNS;mtlspub use mtls::TLS_CLIENT_AUTH_SAN_EMAIL;mtlspub use mtls::TLS_CLIENT_AUTH_SAN_IP;mtlspub use mtls::TLS_CLIENT_AUTH_SAN_URI;mtlspub use mtls::TLS_CLIENT_AUTH_SUBJECT_DN;mtlspub use par::JarConfig;jarpub use par::RegisteredRequestObjectKey;jarpub use par::RequestObjectAlg;jarpub use par::RequestObjectKeyError;jarpub use par::RequestObjectKeys;jarpub use par::REQUEST_OBJECT_SIGNING_ALGS;jarpub use par::REQUEST_OBJECT_TYP;jarpub use par::ParConfig;parpub use par::PushedAuthorizationRequest;parpub use par::PushedAuthorizationResponse;parpub use par::REQUEST_URI_PREFIX;parpub use rar::AuthorizationDetail;rarpub use rar::AuthorizationDetails;rarpub use rar::MAX_AUTHORIZATION_DETAILS_BYTES;rarpub use rar::MAX_AUTHORIZATION_DETAILS_DEPTH;rarpub use rar::MAX_AUTHORIZATION_DETAILS_ELEMENTS;rarpub use rate_limit::FixedWindowRateLimiter;pub use rate_limit::RateLimitConfig;pub use rate_limit::MAX_TRACKED_CLIENT_ID_LEN;pub use rate_limit::MIN_WINDOW;pub use registration::ClientInformation;pub use registration::ClientMetadata;pub use registration::RegistrationAttempt;pub use registration::RegistrationConfig;pub use registration::RegistrationDecision;pub use registration::RegistrationErrorCode;pub use registration::RegistrationErrorResponse;pub use registration::RegistrationFailure;pub use registration::RegistrationPolicy;pub use registration::MAX_REGISTERED_REDIRECT_URIS;pub use resource_metadata::BearerMethod;resource-metadatapub use resource_metadata::ProtectedResourceConfig;resource-metadatapub use resource_metadata::ProtectedResourceMetadata;resource-metadatapub use resource_metadata::PROTECTED_RESOURCE_WELL_KNOWN_PATH;resource-metadatapub use scope::Scope;pub use scope::ScopeSet;pub use server::AuthorizationServer;pub use server::ClientCredential;pub use server::Clock;pub use server::DeviceApprovalError;pub use server::ResourceServerRegistration;pub use server::ServerConfig;pub use server::SystemClock;pub use server::TokenRequest;pub use server::TokenRequestContext;pub use server::UserApproval;pub use server::MAX_RESOURCE_INDICATORS;pub use server::MIN_USER_CODE_LENGTH;pub use store::MemoryStorage;pub use store::RevocationBarrier;pub use store::RevocationWindow;pub use store::Storage;pub use store::StorageError;pub use store::WriteOutcome;pub use token::Confirmation;dpopormtlspub use token::IntrospectionResponse;pub use token::IssuedToken;pub use token::RefreshTokenRecord;pub use token::RefreshTokenState;pub use token::TokenResponse;pub use token::TokenType;pub use token::TokenTypeHint;pub use token_exchange::ActClaim;token-exchangepub use token_exchange::ExchangeSemantics;token-exchangepub use token_exchange::ExchangedToken;token-exchangepub use token_exchange::TokenExchange;token-exchangepub use token_exchange::TokenExchangeRequest;token-exchangepub use token_exchange::TokenExchangeResponse;token-exchangepub use token_exchange::TokenTypeIdentifier;token-exchangepub use token_exchange::MAX_AUDIENCE_VALUES;token-exchangepub use token_exchange::TOKEN_EXCHANGE_GRANT_URN;token-exchange
Modules§
- authorization
- The authorization endpoint (RFC 6749 section 4.1) under the OAuth 2.1 constraints:
codeis the only response type (the implicit grant is removed), PKCE is REQUIRED, onlyS256is offered, and registered redirect URIs match exactly. - cimd
cimd - Client identifier metadata documents (draft-ietf-oauth-client-id-metadata-document-01): a client identifies itself with an HTTPS URL, and the metadata that would otherwise have come from an RFC 7591 registration is fetched from that URL instead.
- client
- Registered OAuth clients, mirrored from RFC 6749 section 2 with the OAuth 2.1 public / confidential split.
- client_
assertion client-assertion - RFC 7523 JWT client authentication:
private_key_jwtandclient_secret_jwt. - consent
consent - Consent records, consent withdrawal, and RFC 9470 step-up authentication.
- delegate
delegate_storage!, the answer to “I want Postgres for clients and memory for codes”.- device
- Device authorization grant shapes, mirrored from RFC 8628: the section 3.2 device authorization
response, and the grant record whose state machine
crate::server::AuthorizationServerdrives (PendingtoApproved/Denied, expiry by clock, single-use redemption by removal). - dpop
dpop - RFC 9449 DPoP: sender-constrained access tokens.
- error
- The OAuth error response object, mirrored from RFC 6749 section 5.2 (token endpoint), section 4.1.2.1 (authorization endpoint), and the RFC 8628 section 3.5 device-grant extension codes. One enum, one struct, owned here: never a third party’s generated types.
- events
- The host seams this library cannot fill for itself: an AUDIT EVENT channel and a RATE LIMITING
decision point, plus the one slot (
Hooks) the server carries for both of them and for the client secret verifier (crate::client::SecretVerifier). - grant
- Grant types, mirrored from RFC 6749 section 4 plus the RFC 8628 device-grant URN. The wire spelling of the device grant is the full URN, exactly as registered.
- http
http - An OPTIONAL HTTP service over
AuthorizationServer, behind thehttpcargo feature. - jwt
jwt - RFC 9068 JWT access tokens and the RFC 7517 key set that lets a resource server verify them.
Compiled ONLY under the off-by-default
jwtfeature; with the feature off this module does not exist and the crate’s dependency set is unchanged. - metadata
- RFC 8414 authorization server metadata: the discovery document served at
{issuer}/.well-known/oauth-authorization-server. - mtls
mtls - RFC 8705 mutual-TLS client authentication (
tls_client_auth,self_signed_tls_client_auth) and certificate-bound access tokens. Compiled ONLY under the off-by-defaultmtlscargo feature; with the feature off this module does not exist, no type here appears in the public API, and the crate’s dependency set and runtime cost are unchanged. - par
jarorpar - RFC 9126 pushed authorization requests (PAR) and RFC 9101 JWT-secured authorization requests
(JAR). Compiled ONLY under the off-by-default
parandjarcargo features; with both off this module does not exist and nothing else in the crate changes. - pkce
- PKCE (RFC 7636),
S256only per OAuth 2.1. The derivation iscode_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))with no padding (section 4.2), andtests/rfc_vectors.rslocks it against the appendix B vector. - rar
rar - RFC 9396 rich authorization requests: the
authorization_detailsparameter, which says what a client is asking for as STRUCTURE rather than as a scope string. - rate_
limit - A rate limiter the crate SHIPS, so that “the host must throttle this” is a line of code rather than a paragraph of documentation.
- registration
- RFC 7591 dynamic client registration and RFC 7592 registration management.
- resource_
metadata resource-metadata - RFC 9728 protected resource metadata: the document served at
{resource}/.well-known/oauth-protected-resource. - scope
- Access-token scope, mirrored from RFC 6749 section 3.3: a scope is a space-delimited set of
case-sensitive tokens, each drawn from
%x21 / %x23-5B / %x5D-7E(printable ASCII minus space, double quote, and backslash). - server
- The authorization server itself: configuration, the clock seam, and the grant state machines.
- signer_
conformance jwtandtest-util - A RUNNABLE conformance harness for the
Es256SignerandEs256Verifiercontracts, behind thetest-utilcargo feature (off by default), for a HOST to run from its OWN test suite against the backend it is about to deploy. - storage_
conformance test-util - A RUNNABLE conformance harness for the
Storagecontract, behind thetest-utilcargo feature (off by default), for a HOST to run from its OWN test suite against its OWN store. - store
- The storage seam. This crate never assumes what the host’s persistence looks like: the host
implements
Storage, and the server only ever talks through it.MemoryStorageis the reference implementation, used by this crate’s tests and suitable for single-process embedding. - token
- Token wire and storage shapes: the RFC 6749 section 5.1 success response, plus the records the
server persists through
crate::store::Storage. - token_
exchange token-exchange - RFC 8693 token exchange:
grant_type=urn:ietf:params:oauth:grant-type:token-exchange.
Macros§
- delegate_
storage - Forward the named
crate::store::Storagemethods to an inner store.