Volga
Home
API Docs
GitHub
  • English
  • Русский
Home
API Docs
GitHub
  • English
  • Русский
  • Home
  • Getting Started

    • Quick Start
    • Route Parameters
    • Query Parameters
    • Route Groups
  • Requests & Responses

    • Headers
    • Handling JSON
    • Handling Form Data
    • Working with Files
    • Multipart Responses
    • Cookies
  • Middleware & Infrastructure

    • Basic Middleware
    • Custom Middleware
    • Parameterized Middleware
    • Response Compression
    • Request Decompression
    • CORS (Cross-Origin Resource Sharing)
    • Static Files
    • Rate Limiting
    • Configuration Files
  • Security & Access

    • Authentication and Authorization
    • OAuth 2.1 & OpenID Connect
    • OAuth 2.1 Client
  • Reliability & Observability

    • Global Error Handling
    • Tracing & Logging
    • Request cancellation
  • Protocols & Realtime

    • HTTP/1 and HTTP/2
    • HTTPS
    • WebSockets
    • Server-Sent Events (SSE)
  • Advanced Patterns

    • Dependency Injection
    • Custom Handling of HEAD, OPTIONS, and TRACE Methods

OAuth 2.1 Client

volga-oauth-client is an OAuth 2.1 / OpenID Connect client built on the shared protocol types from volga-oauth-core. It is independent of the volga server crate — usable from any Tokio application (a CLI, a background worker, or a volga web app driving a login flow).

It provides three clients, all sharing the transport policy of ClientConfig and the error model of ClientError:

  • DiscoveryClient — fetches Authorization Server Metadata (RFC 8414), Protected Resource Metadata (RFC 9728) and the OpenID Connect provider configuration.
  • OAuthClient — the Authorization Code flow with mandatory PKCE, refresh tokens and resource indicators, plus token persistence.
  • RegistrationClient — Dynamic Client Registration (RFC 7591).

Dependencies

[dependencies]
volga-oauth-client = { version = "..." }

Feature flags

FlagWhat it enables
http1 (default)HTTP/1.1 via hyper
http2HTTP/2 via hyper; negotiated through TLS ALPN when combined with http1, used exclusively (prior knowledge over plaintext) without it

At least one of the two must be enabled.

Discovery

DiscoveryClient resolves the well-known discovery URLs, fetches the documents over HTTPS and validates each against the identifier it was requested for (RFC 8414 §3.3 / RFC 9728 §3.3):

use volga_oauth_client::{ClientError, DiscoveryClient};

async fn discover() -> Result<(), ClientError> {
    let client = DiscoveryClient::new();

    // straight from an issuer identifier (RFC 8414, or the OIDC path):
    let server = client.fetch_server_metadata("https://auth.example.com").await?;

    // or start from the resource and follow it to its authorization server:
    let resource = client.fetch_resource_metadata("https://api.example.com").await?;
    let server = client.discover_authorization_server(&resource).await?;

    assert!(server.token_endpoint.is_some());
    Ok(())
}
  • fetch_server_metadata / fetch_oidc_metadata — the same document shape at the RFC 8414 and OIDC Discovery paths.
  • fetch_resource_metadata / fetch_resource_metadata_from_url — the latter takes the resource_metadata URL straight from a WWW-Authenticate challenge.
  • discover_authorization_server — takes the first advertised authorization server and fetches its metadata, falling back from the RFC 8414 path to the OIDC path automatically.
  • fetch_jwks / fetch_jwks_from_url — the issuer's JSON Web Key Set as raw JSON.

Tips

Attach a MetadataCache with with_cache(...) to reuse your existing storage; discovery documents rarely change. JWKS fetches deliberately bypass the cache in both directions — signing keys rotate, so freshness policy belongs to you.

Authorization Code + PKCE

OAuthClient drives the OAuth 2.1 Authorization Code flow. PKCE (S256) is generated and applied automatically — it is the protection OAuth 2.1 prescribes for public clients.

use std::sync::Arc;
use volga_oauth_client::{ClientError, DiscoveryClient, InMemoryTokenStore, OAuthClient};

async fn authorize() -> Result<(), ClientError> {
    let metadata = DiscoveryClient::new()
        .fetch_server_metadata("https://auth.example.com")
        .await?;

    let client = OAuthClient::new("my-client")
        .with_redirect_uri("https://app.example.com/callback")
        .with_token_store(Arc::new(InMemoryTokenStore::new()));

    // 1. build the authorization request (state and PKCE are generated)
    let auth = client
        .authorization_request(&metadata)
        .with_scopes(["read"])
        .with_resource("https://api.example.com")
        .build()?;

    // 2. send the user to `auth.url`. The provider redirects back to your
    //    callback with the real `code` and `state` query parameters — read
    //    them there. (This snippet reuses the generated `auth.state` so the
    //    check below holds on the happy path.)
    let (code, state) = ("the-authorization-code", auth.state.as_str());

    // always verify the callback state before exchanging — CSRF protection
    if !auth.matches_state(state) {
        return Ok(()); // reject — possible CSRF
    }

    // 3. exchange the code for tokens (the PKCE verifier goes along)
    let tokens = client.exchange_code(&metadata, code, &auth).await?;
    client.store_tokens("alice", &tokens);

    // 4. later — served from the store, transparently refreshed when stale:
    let tokens = client.token("alice", &metadata).await?;
    Ok(())
}

The AuthorizationRequest that build() returns holds the url to redirect to, the state to check on the callback and the PKCE pair. It is Serialize/Deserialize, so a web application can stash it in the session between the redirect and the callback.

The request builder accepts with_scopes, with_resource (RFC 8707, repeatable), with_state (override the generated value) and with_param (e.g. the OIDC nonce or prompt).

Warning

Always verify the callback state with matches_state before exchanging the code — it is your CSRF defence.

Transparent refresh

token(key, &metadata) reads the stored tokens and refreshes a stale access token behind the scenes. It returns Ok(None) when interactive authorization is required — nothing is stored, the entry has no refresh token, or the server rejected the refresh token (invalid_grant); in the latter cases the dead entry is removed from the store. You can also refresh explicitly with refresh.

Confidential clients

Without a secret the client acts as a public client (PKCE is the protection). Attach a secret to authenticate to the token endpoint:

use volga_oauth_client::{ClientAuthMethod, OAuthClient};

let client = OAuthClient::new("my-client")
    .with_secret("s3cret")
    // `client_secret_basic` (default) or `client_secret_post`
    .with_auth_method(ClientAuthMethod::Post);

Token Store

Persistence goes through the TokenStore trait. InMemoryTokenStore is the built-in process-local implementation — suitable for CLIs, tests and single-instance services; anything durable (a database, an encrypted file, an OS keychain) is one trait impl away.

use volga_oauth_client::{TokenSet, TokenStore};

struct MyStore;

impl TokenStore for MyStore {
    fn get(&self, key: &str) -> Option<TokenSet> { /* ... */ None }
    fn put(&self, key: &str, tokens: &TokenSet) { /* ... */ }
    fn remove(&self, key: &str) { /* ... */ }
}

The key is chosen by the application — typically a user or session identifier, combined with the resource when one client serves several audiences. A TokenSet carries the access token, an optional refresh token, the granted scope, the OIDC id_token (passed through, not validated) and an absolute expires_at; tokens are redacted from its Debug output.

Dynamic Client Registration

RegistrationClient submits ClientMetadata to a server's registration endpoint (RFC 7591) and returns the issued credentials. OAuthClient::from_registration adopts them into a ready-to-use client:

use volga_oauth_client::{
    ClientError, ClientMetadata, DiscoveryClient, OAuthClient, RegistrationClient,
};

async fn register() -> Result<(), ClientError> {
    let metadata = DiscoveryClient::new()
        .fetch_server_metadata("https://auth.example.com")
        .await?;

    let registered = RegistrationClient::new()
        .register(
            &metadata,
            &ClientMetadata::new()
                .with_redirect_uris(["https://app.example.com/callback"])
                .with_client_name("My App"),
        )
        .await?;

    // ready-to-use client under the issued credentials
    let client = OAuthClient::from_registration(&registered)?;
    Ok(())
}

For servers that do not allow open registration, attach an initial access token with with_initial_access_token.

Info

The RFC 7592 management protocol (reading, updating and deleting a registration) is not implemented, but the registration_access_token / registration_client_uri pair from the response is surfaced for applications that need it.

Transport Policy & Errors

ClientConfig carries the policy shared by every client operation — HTTPS enforcement, per-request timeouts and redirect limits. The defaults are safe for production; the most common override is disabling HTTPS for a local development server:

use std::time::Duration;
use volga_oauth_client::{ClientConfig, OAuthClient};

let config = ClientConfig::new()
    .require_https(false)              // local development only
    .with_timeout(Duration::from_secs(5))
    .with_max_redirects(0);

let client = OAuthClient::new("my-client").with_config(config);

ClientError separates a parsed OAuth error response (Protocol, carrying the RFC 6749 §5.2 OAuthError) from transport, decode, insecure-URL and validation failures — so you can distinguish "the server said invalid_grant" from "the connection dropped".

Examples

  • OAuth Flow — a full discovery → authorization → code exchange → protected call flow driven by volga-oauth-client.
Last Updated: 7/15/26, 3:36 PM
Prev
OAuth 2.1 & OpenID Connect