Skip to main content

Crate oauth_as

Crate oauth_as 

Source
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, plus store::MemoryStorage for 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, in http 1.x and http-body 1.x with no web framework and no async runtime. axum adds a thin impl From<..> for axum::Router adapter for hosts that want one; nothing else in the crate knows axum exists.
  • jwt (RFC 9068 at+jwt access 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 7523 private_key_jwt and client_secret_jwt).
  • jwt-p256, THE BUILT-IN ES256 BACKEND, and the one to reach for first: jwt compiles the RFC 7515 machinery and the jwt::Es256Signer / jwt::Es256Verifier seams but SIGNS NOTHING BY ITSELF, because where a private key lives is the host’s decision (see the jwt module docs). jwt-p256 supplies jwt::EcdsaP256Key and installs jwt::P256Verifier as 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 no p256. The features are ADDITIVE rather than exclusive, so both at once compiles and the installed signer wins. jwt-pkcs8 adds PKCS#8 DER loading on top of jwt-p256, for a host whose key arrives as the DER its KMS or openssl already emits rather than as a raw scalar.
  • par and jar (RFC 9126 pushed authorization requests, RFC 9101 signed request objects), rar (RFC 9396 authorization_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. See cimd for the full list of what that leaves with the host.
  • test-util, a RUNNABLE conformance harness for the store::Storage contract 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 the events module docs for the rule and for why the refresh family_id is 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::FixedWindowRateLimiter is a per-process, in-memory floor, and the device path already consults it — server::AuthorizationServer::approve_device asks 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 over N nodes is N times 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 consent module’s Authentication, behind the consent feature). 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 enforces max_age and acr_values against it. The report is taken at face value, because there is nothing here that could check it. See the consent module docs for the whole boundary.
  • WHO MAY REGISTER (registration::RegistrationPolicy). RFC 7591 dynamic client registration is OFF unless server::ServerConfig::registration is 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 the registration module 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;cimd
pub use cimd::CimdPolicy;cimd
pub use cimd::ClientIdUrl;cimd
pub use cimd::ValidatedClientIdDocument;cimd
pub use cimd::MAX_CLIENT_ID_DOCUMENT_BYTES;cimd
pub use cimd::MAX_CLIENT_ID_URL_BYTES;cimd
pub 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-assertion
pub use client_assertion::AssertionKeys;client-assertion
pub use client_assertion::VerifiedAssertion;client-assertion
pub use client_assertion::CLIENT_ASSERTION_TYPE;client-assertion
pub use client_assertion::CLIENT_SECRET_JWT;client-assertion
pub use client_assertion::MAX_ASSERTION_LIFETIME;client-assertion
pub use client_assertion::MIN_CLIENT_SECRET_JWT_KEY_LENGTH;client-assertion
pub use client_assertion::PRIVATE_KEY_JWT;client-assertion
pub use consent::step_up_challenge;consent
pub use consent::Authentication;consent
pub use consent::AuthenticationRequirement;consent
pub use consent::ConsentRecord;consent
pub use consent::StepUpFailure;consent
pub use device::DeviceAuthorizationResponse;
pub use device::DeviceGrant;
pub use device::DeviceGrantState;
pub use dpop::DpopFailure;dpop
pub use dpop::VerifiedProof;dpop
pub use dpop::DPOP_HEADER;dpop
pub use dpop::DPOP_TOKEN_TYPE;dpop
pub use dpop::MAX_PROOF_AGE;dpop
pub use dpop::MAX_PROOF_BYTES;dpop
pub 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;http
pub use http::ApprovalRequest;http
pub use http::ApprovalResolver;http
pub use http::AuthorizationService;http
pub use http::Body;http
pub use http::CsrfTokenHook;http
pub use http::Response;http
pub use http::ServiceBuilder;http
pub use http::ServiceError;http
pub use http::SubjectResolver;http
pub use http::MAX_BODY_BYTES;http
pub use http::MAX_FORM_PARAMETERS;http
pub use http::AuthenticationReporter;consent and http
pub use jwt::AccessTokenFormat;jwt
pub use jwt::Audience;jwt
pub use jwt::Es256Signer;jwt
pub use jwt::Es256Verifier;jwt
pub use jwt::Jwk;jwt
pub use jwt::Jwks;jwt
pub use jwt::JwtConfig;jwt
pub use jwt::JwtError;jwt
pub use jwt::PublicJwk;jwt
pub use jwt::SignerError;jwt
pub use jwt::VerifyError;jwt
pub use jwt::EcdsaP256Key;jwt-p256
pub use jwt::KeyError;jwt-p256
pub use jwt::P256Verifier;jwt-p256
pub use metadata::well_known_path;
pub use metadata::AuthorizationServerMetadata;
pub use metadata::WELL_KNOWN_PATH;
pub use mtls::CertificateThumbprint;mtls
pub use mtls::ClientCertificate;mtls
pub use mtls::ExpectedSubject;mtls
pub use mtls::MtlsClientRegistration;mtls
pub use mtls::MtlsRegistrationError;mtls
pub use mtls::RegisteredCertificates;mtls
pub use mtls::SELF_SIGNED_TLS_CLIENT_AUTH;mtls
pub use mtls::TLS_CLIENT_AUTH;mtls
pub use mtls::TLS_CLIENT_AUTH_SAN_DNS;mtls
pub use mtls::TLS_CLIENT_AUTH_SAN_EMAIL;mtls
pub use mtls::TLS_CLIENT_AUTH_SAN_IP;mtls
pub use mtls::TLS_CLIENT_AUTH_SAN_URI;mtls
pub use mtls::TLS_CLIENT_AUTH_SUBJECT_DN;mtls
pub use par::JarConfig;jar
pub use par::RegisteredRequestObjectKey;jar
pub use par::RequestObjectAlg;jar
pub use par::RequestObjectKeyError;jar
pub use par::RequestObjectKeys;jar
pub use par::REQUEST_OBJECT_SIGNING_ALGS;jar
pub use par::REQUEST_OBJECT_TYP;jar
pub use par::ParConfig;par
pub use par::PushedAuthorizationRequest;par
pub use par::PushedAuthorizationResponse;par
pub use par::REQUEST_URI_PREFIX;par
pub use rar::AuthorizationDetail;rar
pub use rar::AuthorizationDetails;rar
pub use rar::MAX_AUTHORIZATION_DETAILS_BYTES;rar
pub use rar::MAX_AUTHORIZATION_DETAILS_DEPTH;rar
pub use rar::MAX_AUTHORIZATION_DETAILS_ELEMENTS;rar
pub 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-metadata
pub use resource_metadata::ProtectedResourceConfig;resource-metadata
pub use resource_metadata::ProtectedResourceMetadata;resource-metadata
pub use resource_metadata::PROTECTED_RESOURCE_WELL_KNOWN_PATH;resource-metadata
pub 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;dpop or mtls
pub 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-exchange
pub use token_exchange::ExchangeSemantics;token-exchange
pub use token_exchange::ExchangedToken;token-exchange
pub use token_exchange::TokenExchange;token-exchange
pub use token_exchange::TokenExchangeRequest;token-exchange
pub use token_exchange::TokenExchangeResponse;token-exchange
pub use token_exchange::TokenTypeIdentifier;token-exchange
pub use token_exchange::MAX_AUDIENCE_VALUES;token-exchange
pub 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: code is the only response type (the implicit grant is removed), PKCE is REQUIRED, only S256 is offered, and registered redirect URIs match exactly.
cimdcimd
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_assertionclient-assertion
RFC 7523 JWT client authentication: private_key_jwt and client_secret_jwt.
consentconsent
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::AuthorizationServer drives (Pending to Approved/Denied, expiry by clock, single-use redemption by removal).
dpopdpop
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.
httphttp
An OPTIONAL HTTP service over AuthorizationServer, behind the http cargo feature.
jwtjwt
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 jwt feature; 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.
mtlsmtls
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-default mtls cargo 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.
parjar or par
RFC 9126 pushed authorization requests (PAR) and RFC 9101 JWT-secured authorization requests (JAR). Compiled ONLY under the off-by-default par and jar cargo features; with both off this module does not exist and nothing else in the crate changes.
pkce
PKCE (RFC 7636), S256 only per OAuth 2.1. The derivation is code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) with no padding (section 4.2), and tests/rfc_vectors.rs locks it against the appendix B vector.
rarrar
RFC 9396 rich authorization requests: the authorization_details parameter, 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_metadataresource-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_conformancejwt and test-util
A RUNNABLE conformance harness for the Es256Signer and Es256Verifier contracts, behind the test-util cargo feature (off by default), for a HOST to run from its OWN test suite against the backend it is about to deploy.
storage_conformancetest-util
A RUNNABLE conformance harness for the Storage contract, behind the test-util cargo 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. MemoryStorage is 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_exchangetoken-exchange
RFC 8693 token exchange: grant_type=urn:ietf:params:oauth:grant-type:token-exchange.

Macros§

delegate_storage
Forward the named crate::store::Storage methods to an inner store.