OAuth 2.1 & OpenID Connect
Volga ships a full OAuth 2.1 / OpenID Connect foundation on top of its Bearer Token authentication. It lets you build resource servers that validate tokens against an OAuth 2.1 / OIDC issuer's published keys — with no shared secret — and serve the discovery metadata documents clients need to start a flow.
The protocol-level types (error models, metadata documents, the WWW-Authenticate challenge builder, well-known URL derivation) live under volga::auth::oauth and are shared with the standalone OAuth client.
Feature flags
| Feature | What it enables |
|---|---|
oauth | OAuth 2.1 / OIDC foundation types at volga::auth::oauth and metadata serving (implied by jwt-auth). |
oauth-client | Issuer-based bearer validation — App::with_oauth / App::use_oauth. Implies jwt-auth. |
[dependencies]
volga = { version = "...", features = ["oauth-client"] }
Validating Tokens Against an Issuer
Instead of configuring a static DecodingKey, you can point bearer authentication at an OAuth 2.1 / OIDC issuer. Volga fetches the issuer's server metadata (RFC 8414, with an OpenID Connect Discovery fallback) and the JSON Web Key Set it advertises, then validates incoming JWTs keyed by each token's kid.
Describe the issuer with with_oauth(...) and activate it explicitly with use_oauth():
use serde::Deserialize;
use volga::{
App, ok,
auth::{AuthClaims, roles},
};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new()
// audience, expiry and the other token checks stay here
.with_bearer_auth(|auth| auth.with_aud(["https://api.example.com"]))
// the keys and the `iss` constraint come from the issuer
.with_oauth(|oauth| oauth.with_issuer("https://auth.example.com"));
// explicit opt-in — nothing validates against the issuer until this call
app.use_oauth();
app.map_get("/protected", protected)
.authorize::<Claims>(roles(["admin"]));
app.run().await
}
async fn protected() -> &'static str {
"Hello from the protected route!"
}
#[derive(Clone, Deserialize)]
struct Claims {
role: String,
}
impl AuthClaims for Claims {
fn role(&self) -> Option<&str> {
Some(&self.role)
}
}
With issuer-based validation no static decoding key is required — the keys are resolved at runtime. Everything else (aud, expiry, scopes and roles) keeps coming from with_bearer_auth.
Info
The iss claim is constrained to the configured issuer automatically and made required — tokens omitting it, or carrying a different issuer, are rejected.
Key lifecycle
Keys are fetched lazily on the first request and cached, so token validation costs no network round-trip in the common case. The cache maintains itself:
- A token with an unknown
kidtriggers a refresh (key rotation), rate-limited bywith_refresh_cooldown(default 60 s); concurrent misses share a single refresh. - Known
kids are re-checked with the issuer once the cached set is older thanwith_max_key_age(default 15 minutes), so a revoked or re-keyedkidstops validating without a restart. - While the issuer is unreachable and keys were already loaded, the last known set keeps serving — an issuer outage does not take token validation down with it. When no keys have ever loaded, protected routes answer
503(a server-side problem) rather than blaming the token.
Configuration
The issuer is mandatory; everything else has production-safe defaults.
use std::time::Duration;
use volga::App;
let app = App::new()
.with_bearer_auth(|auth| auth.with_aud(["https://api.example.com"]))
.with_oauth(|oauth| oauth
.with_issuer("https://auth.example.com")
.with_refresh_cooldown(Duration::from_secs(30))
.with_max_key_age(Duration::from_secs(600))
// discovery / JWKS transport policy
.with_client_config(|client| client.require_https(true)));
For a local development issuer served over plain HTTP, relax the transport policy:
let app = App::new()
.with_oauth(|oauth| oauth
.with_issuer("http://127.0.0.1:5000")
.with_client_config(|client| client.require_https(false)));
With the config feature the same knobs can be described in the [oauth.client] section of the configuration file — fields present in the file override the builder calls, unknown keys fail startup, and activation still requires the explicit App::use_oauth() call in code:
[oauth.client]
issuer = "https://auth.example.com"
refresh_cooldown_secs = 60 # optional
max_key_age_secs = 900 # optional
require_https = true # optional
timeout_secs = 30 # optional
max_redirects = 5 # optional
Serving Metadata Documents
A resource server tells clients where to authenticate; an authorization server publishes its endpoints and keys. Volga serves both discovery documents from your application.
Protected Resource Metadata (RFC 9728)
Configure it with with_oauth_resource_metadata (or set_oauth_resource_metadata for the whole value, including the &str identifier shorthand) and serve it with use_oauth_resource_metadata:
let mut app = App::new()
.with_oauth_resource_metadata(|metadata| metadata
.with_resource("https://api.example.com")
.with_authorization_servers(["https://auth.example.com"])
.with_scopes(["read", "write"])
.with_bearer_methods(["header"]));
// GET /.well-known/oauth-protected-resource
app.use_oauth_resource_metadata();
When bearer authentication is configured, the derived metadata URL is advertised automatically in WWW-Authenticate challenges (RFC 9728 §5.1), so an unauthenticated client can discover where to authenticate and start a flow.
Authorization Server Metadata (RFC 8414) & OIDC Discovery
Applications that are themselves an authorization server publish their endpoints via with_oauth_server_metadata and serve the document at one or both discovery paths:
let mut app = App::new()
.with_oauth_server_metadata(|metadata| metadata
.with_issuer("https://auth.example.com")
.with_authorization_endpoint("https://auth.example.com/authorize")
.with_token_endpoint("https://auth.example.com/token")
.with_jwks_uri("https://auth.example.com/jwks"));
// authorization servers commonly publish the same document at both paths:
app.use_oauth_server_metadata() // GET /.well-known/oauth-authorization-server
.use_oidc_metadata(); // GET /.well-known/openid-configuration
Tips
The server-metadata closure is seeded with the OAuth 2.1 prefills response_types_supported = ["code"] and grant_types_supported = ["authorization_code"]. OIDC-specific fields required by a compliant provider document (subject_types_supported, id_token_signing_alg_values_supported, userinfo_endpoint, …) can be supplied through with_additional_field(...).
Both documents can also come from the [oauth.resource] / [oauth.server] sections of the configuration file (the config feature); the file overrides prior builder calls. The set_* shorthand configures a minimal document from the identifier alone:
let mut app = App::new()
.set_oauth_resource_metadata("https://api.example.com")
.set_oauth_server_metadata("https://auth.example.com");
app.use_oauth_resource_metadata();
app.use_oauth_server_metadata().use_oidc_metadata();
The Full Flow
Putting the pieces together, a resource server needs only a handful of lines — token validation is wired straight to the issuer's published keys, with no secret configured anywhere:
use volga::{App, auth::{AuthClaims, roles}, ok};
use serde::Deserialize;
#[derive(Clone, Deserialize)]
struct Claims { role: String }
impl AuthClaims for Claims {
fn role(&self) -> Option<&str> { Some(&self.role) }
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new()
.with_oauth(|oauth| oauth.with_issuer("https://auth.example.com"))
// advertised in WWW-Authenticate challenges
.with_oauth_resource_metadata(|m| m
.with_resource("https://api.example.com")
.with_authorization_servers(["https://auth.example.com"]));
app.use_oauth();
app.use_oauth_resource_metadata();
app.map_get("/protected", || async { ok!("Hello from the protected route!") })
.authorize::<Claims>(roles(["admin"]));
app.run().await
}
The client side of the same flow — discovery, the Authorization Code + PKCE exchange and calling the protected route — is covered on the OAuth 2.1 Client page.
Examples
- OAuth Flow — a complete Authorization Code + PKCE flow between an authorization server, a resource server and a client, in one process.
- OAuth Metadata — serving the RFC 8414 / RFC 9728 / OIDC discovery documents.