JWT & OAuth2
Issue and validate JWT access tokens and integrate OAuth2 authorization flows for Rust APIs.
Recipe
Quick-reference recipe card - copy-paste ready.
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
#[derive(serde::Serialize, serde::Deserialize)]
struct Claims { sub: String, exp: usize, aud: String }
let token = encode(&Header::default(), &claims, &EncodingKey::from_secret(secret))?;
let data = decode::<Claims>(&token, &DecodingKey::from_secret(secret), &Validation::default())?;When to reach for this: Stateless API auth, microservice identity propagation, or delegating login to Google/Auth0 via OAuth2.
Working Example
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use oauth2::{AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl, basic::BasicClient};
#[derive(serde::Serialize, serde::Deserialize)]
struct AccessClaims {
sub: String,
exp: usize,
aud: String,
}
fn issue_access_token(sub: &str, aud: &str, secret: &[u8], ttl_secs: usize) -> String {
let exp = (std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs()
+ ttl_secs as u64) as usize;
let claims = AccessClaims { sub: sub.into(), exp, aud: aud.into() };
encode(&Header::default(), &claims, &EncodingKey::from_secret(secret)).unwrap()
}
fn validate_access_token(token: &str, secret: &[u8], expected_aud: &str) -> Result<AccessClaims, jsonwebtoken::errors::Error> {
let mut validation = Validation::default();
validation.set_audience(&[expected_aud]);
let data = decode::<AccessClaims>(token, &DecodingKey::from_secret(secret), &validation)?;
Ok(data.claims)
}
fn oauth_client() -> BasicClient {
BasicClient::new(ClientId::new("client-id".into()))
.set_client_secret(ClientSecret::new("client-secret".into()))
.set_auth_uri(AuthUrl::new("https://issuer.example.com/oauth/authorize".into()).unwrap())
.set_token_uri(TokenUrl::new("https://issuer.example.com/oauth/token".into()).unwrap())
.set_redirect_uri(RedirectUrl::new("http://localhost:3000/callback".into()).unwrap())
}What this demonstrates:
- HS256 access token issue and validate with
jsonwebtoken. - Audience (
aud) validation prevents token reuse across services. oauth2crate client setup for authorization code flow.- Expiry (
exp) enforced byValidation.
Deep Dive
How It Works
- JWT: signed JSON claims; servers verify signature and standard claims.
- OAuth2: delegate authentication to issuer; your API validates access tokens (often JWT).
- Refresh tokens stay confidential, longer-lived, stored securely.
Flow Selection
| Flow | Use |
|---|---|
| Client credentials | Machine-to-machine |
| Authorization code + PKCE | User login for SPAs/mobile |
| Refresh token | Renew access without re-login |
Rust Notes
// RS256 with public key from OIDC JWKS
let key = DecodingKey::from_rsa_pem(public_pem)?;Gotchas
- HS256 shared secret across services - Compromise leaks all. Fix: RS256 with JWKS per issuer.
- Skipping aud/iss checks - Token from another app accepted. Fix: Configure
Validationaudience and issuer. - Long-lived access tokens - Stolen token window huge. Fix: Short TTL (15m) + refresh rotation.
- OAuth state parameter omitted - CSRF on callback. Fix: Random
statestored in session. - Logging tokens - Appear in access logs. Fix: Redact
Authorizationin tracing.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Session cookies | Browser-first apps | Mobile/API-only clients |
| PASETO | Opinionated token format | Ecosystem expects JWT/OIDC |
| API keys | Internal scripts | Interactive users |
FAQs
OIDC vs OAuth2?
OIDC adds identity (id_token) on OAuth2 - use for user profile.
Where store refresh token?
HttpOnly cookie or secure mobile keystore - never localStorage.
JWKS rotation?
Cache keys with TTL; refetch on kid mismatch.
Scopes?
Encode in token claims; enforce per handler.
Service accounts?
Client credentials flow with short-lived JWT.
Axum extractor?
See Authentication & Sessions page for FromRequestParts pattern.
Clock skew?
validation.leeway seconds for exp/nbf.
Logout?
Token denylist until exp or session server-side for refresh revoke.
Testing?
Static test keys; encode helper in test module.
Auth0/Keycloak?
Use issuer metadata URL; validate RS256 against published JWKS.
Related
- Authentication & Sessions - Axum guards
- Password Hashing - Local credentials
- Secrets Management - Signing keys
- APIs & Security Best Practices - Auth baseline
- Cryptography (ring / RustCrypto) - Signing primitives
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.