rgoussu@goussu: ~/library/rust/rust-security
~/library/rust/rust-security cat oauth2-and-oidc.md

OAuth2 & OIDC in Rust

# The oauth2 crate's typed flows, openidconnect for discovery and ID-token validation, jsonwebtoken for resource servers, session auth with axum-login, and the missing authorization-server story.

Conceptsaved 2026-08-09 #rust#security#oauth2#oidc#jwt

Overview

The protocol family lands in Rust as three composable crates rather than a framework module: oauth2 (the flows, as a typestate-guided client), openidconnect (OIDC discovery, ID-token validation and claims on top of oauth2), and jsonwebtoken (JWT signing/verification, the resource-server workhorse). The architecture matches Go's — client crates plus hand-wired middleware, external IdP assumed — with Rust's characteristic twist: misuse the API and it usually fails to compile (PKCE verifiers and CSRF tokens are distinct types you must thread through, not strings you may forget).

Key points

  • oauth2 crate: BasicClient configured with endpoints; auth-code + PKCE flow as typed steps — authorize_url() hands back a CsrfToken and takes a PkceCodeChallenge; exchange_code() demands the matching verifier. Client credentials, device code and refresh flows included; async via a pluggable reqwest hook.
  • openidconnect crate: CoreClient::from_provider_metadata (.well-known discovery), nonce-checked ID-token validation with signature, issuer, audience and expiry verification typed into the API; the claims struct is generic for custom claims. This is the browser-login half.
  • Resource server = jsonwebtoken + JWKS: fetch the IdP's JWKS (a small reqwest+cache affair — or jwks-client-style crates), pick the key by kid, then jsonwebtoken::decode::<Claims> with an explicit Validation (algorithms, audience, issuer). Wire it as an axum extractor so Claims in a handler signature is the auth requirement.
  • Framework glue: axum-login (on tower-sessions) provides the session-based login/backend abstraction for first-party auth; actix-identity/actix-session mirror it; middleware-level token validation is a thin tower layer either way.
  • Serving OAuth2 yourself is the thin corner: oxide-auth exists (framework-agnostic authorization-server toolkit), but there is no Keycloak-class Rust IdP — the assumed architecture is an external IdP (Keycloak, Auth0, Zitadel, Ory), same conclusion as Go and Quarkus.
  • The usual discipline is unchanged: authorization code + PKCE for anything user-facing, validate aud/iss/exp/nbf explicitly, pin allowed algorithms (never accept none — jsonwebtoken won't), short-lived access tokens; protocol semantics in Authentication & authorization.

Examples

#[derive(Debug, Deserialize)]
struct Claims { sub: String, scope: String, exp: usize }

impl<S: Send + Sync> FromRequestParts<S> for Claims {
    type Rejection = AppError;
    async fn from_request_parts(parts: &mut Parts, _s: &S) -> Result<Self, Self::Rejection> {
        let token = bearer_token(parts).ok_or(AppError::Unauthorized)?;
        let key = JWKS.decoding_key_for(&token)?;          // kid-matched, cached
        let mut validation = Validation::new(Algorithm::RS256);
        validation.set_audience(&["orders-api"]);
        validation.set_issuer(&["https://idp.example.com/"]);
        Ok(decode::<Claims>(token, &key, &validation)?.claims)
    }
}

async fn list_orders(claims: Claims) -> Result<Json<Vec<Order>>, AppError> { /* … */ }

Related