Oauth2 Flows
OAuth 2.0 authorization code, PKCE, client credentials, and token exchange patterns
You are an expert in OAuth 2.0 for securing applications. You understand the authorization framework's grant types, token handling, scope management, and how to integrate with identity providers correctly. ## Key Points - **Resource Owner**: The user who authorizes access to their data. - **Client**: The application requesting access (public or confidential). - **Authorization Server (AS)**: Issues tokens after authenticating the resource owner and obtaining consent. - **Resource Server (RS)**: The API that accepts access tokens. - **Authorization Code**: A short-lived, one-time-use code exchanged for tokens. - **PKCE (Proof Key for Code Exchange)**: A mechanism that binds the authorization request to the token exchange, preventing interception attacks. - **Scopes**: Named permissions that limit what an access token can do. - **State Parameter**: An opaque value used to prevent CSRF during the authorization flow. 1. **Always use PKCE**, even for confidential clients. It is now recommended for all OAuth 2.0 flows (RFC 9126). 2. **Prefer the Authorization Code flow** for all client types. The Implicit flow is deprecated. 3. **Validate the `state` parameter** on every callback to prevent CSRF. 4. **Use the BFF pattern for SPAs** to keep tokens on the server and reduce browser exposure.
skilldb get auth-patterns-skills/Oauth2 FlowsFull skill: 194 linesOAuth 2.0 Flows — Authentication & Authorization
You are an expert in OAuth 2.0 for securing applications. You understand the authorization framework's grant types, token handling, scope management, and how to integrate with identity providers correctly.
Core Philosophy
Overview
OAuth 2.0 is an authorization framework that enables third-party applications to obtain limited access to an HTTP service on behalf of a resource owner. It defines several grant types (flows) for different client types and trust levels. OAuth 2.0 is not an authentication protocol on its own — OpenID Connect (OIDC) adds an identity layer on top. The current best practice is the Authorization Code flow with PKCE for all client types.
Core Concepts
- Resource Owner: The user who authorizes access to their data.
- Client: The application requesting access (public or confidential).
- Authorization Server (AS): Issues tokens after authenticating the resource owner and obtaining consent.
- Resource Server (RS): The API that accepts access tokens.
- Authorization Code: A short-lived, one-time-use code exchanged for tokens.
- PKCE (Proof Key for Code Exchange): A mechanism that binds the authorization request to the token exchange, preventing interception attacks.
- Scopes: Named permissions that limit what an access token can do.
- State Parameter: An opaque value used to prevent CSRF during the authorization flow.
Implementation Patterns
Authorization Code Flow with PKCE (Browser SPA)
// Step 1: Generate PKCE verifier and challenge
async function startAuthFlow() {
const verifier = crypto.randomUUID() + crypto.randomUUID();
const encoder = new TextEncoder();
const digest = await crypto.subtle.digest('SHA-256', encoder.encode(verifier));
const challenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
sessionStorage.setItem('pkce_verifier', verifier);
const state = crypto.randomUUID();
sessionStorage.setItem('oauth_state', state);
const params = new URLSearchParams({
response_type: 'code',
client_id: 'your-client-id',
redirect_uri: 'https://app.example.com/callback',
scope: 'openid profile email',
state,
code_challenge: challenge,
code_challenge_method: 'S256',
});
window.location.href = `https://auth.example.com/authorize?${params}`;
}
// Step 2: Handle the callback
async function handleCallback() {
const params = new URLSearchParams(window.location.search);
// Validate state to prevent CSRF
if (params.get('state') !== sessionStorage.getItem('oauth_state')) {
throw new Error('State mismatch — possible CSRF attack');
}
const response = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: params.get('code'),
redirect_uri: 'https://app.example.com/callback',
client_id: 'your-client-id',
code_verifier: sessionStorage.getItem('pkce_verifier'),
}),
});
const tokens = await response.json();
// tokens contains: access_token, refresh_token, id_token, expires_in
sessionStorage.removeItem('pkce_verifier');
sessionStorage.removeItem('oauth_state');
return tokens;
}
Client Credentials Flow (Service-to-Service)
import httpx
async def get_service_token() -> str:
"""Obtain a token for service-to-service communication."""
async with httpx.AsyncClient() as client:
response = await client.post(
"https://auth.example.com/token",
data={
"grant_type": "client_credentials",
"client_id": os.environ["SERVICE_CLIENT_ID"],
"client_secret": os.environ["SERVICE_CLIENT_SECRET"],
"scope": "orders.read inventory.write",
},
)
response.raise_for_status()
data = response.json()
return data["access_token"]
Backend-for-Frontend (BFF) Pattern
// Server-side (Express) handles OAuth flow, keeps tokens out of the browser
app.get('/auth/callback', async (req, res) => {
const { code, state } = req.query;
// Validate state from session
if (state !== req.session.oauthState) {
return res.status(403).send('Invalid state');
}
// Exchange code for tokens server-side
const tokenResponse = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: 'https://app.example.com/auth/callback',
client_id: process.env.OAUTH_CLIENT_ID,
client_secret: process.env.OAUTH_CLIENT_SECRET,
code_verifier: req.session.pkceVerifier,
}),
});
const tokens = await tokenResponse.json();
// Store tokens in server-side session — never expose to browser
req.session.accessToken = tokens.access_token;
req.session.refreshToken = tokens.refresh_token;
req.session.tokenExpiry = Date.now() + tokens.expires_in * 1000;
res.redirect('/dashboard');
});
// Proxy API calls, attaching the token server-side
app.use('/api/proxy/:path(*)', async (req, res) => {
let token = req.session.accessToken;
// Refresh if expired
if (Date.now() > req.session.tokenExpiry - 60000) {
token = await refreshAccessToken(req.session);
}
const apiResponse = await fetch(`https://api.example.com/${req.params.path}`, {
method: req.method,
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: ['GET', 'HEAD'].includes(req.method) ? undefined : JSON.stringify(req.body),
});
res.status(apiResponse.status).json(await apiResponse.json());
});
Best Practices
- Always use PKCE, even for confidential clients. It is now recommended for all OAuth 2.0 flows (RFC 9126).
- Prefer the Authorization Code flow for all client types. The Implicit flow is deprecated.
- Validate the
stateparameter on every callback to prevent CSRF. - Use the BFF pattern for SPAs to keep tokens on the server and reduce browser exposure.
- Request minimal scopes. Follow the principle of least privilege.
- Validate
issandaudin ID tokens (OIDC) to ensure tokens were issued for your application. - Register exact redirect URIs — never use wildcard or partial matching.
- Rotate client secrets periodically and support multiple active secrets during the transition window.
Common Pitfalls
- Using the Implicit flow: It exposes tokens in the URL fragment and has no refresh capability. Use Authorization Code + PKCE instead.
- Storing tokens in localStorage: Vulnerable to XSS. Use HttpOnly cookies or server-side sessions.
- Not validating the
stateparameter: Opens the application to CSRF attacks during the OAuth callback. - Overly broad scopes: Requesting more permissions than needed increases the blast radius of a compromised token.
- Hardcoding redirect URIs with HTTP: Always use HTTPS for redirect URIs (except localhost during development).
- Not handling token expiry: Failing to refresh tokens proactively leads to poor user experience and race conditions.
Anti-Patterns
Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.
Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.
Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.
Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.
Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.
Install this skill directly: skilldb add auth-patterns-skills
Related Skills
API Key Auth
API key generation, hashing, rotation, scoping, and rate limiting patterns
JWT Tokens
JWT creation, validation, refresh token rotation, and secure token storage patterns
Multi Tenancy
Multi-tenant authentication isolation, tenant-scoped tokens, and data boundary enforcement
Passkeys
Passkeys and WebAuthn implementation for passwordless authentication
Rbac
Role-based access control design, permission hierarchies, and enforcement patterns
Session Management
Server-side session handling, cookie security, session stores, and fixation prevention