OAuth 2.0 Flows
Not all applications are the same. A web app running on a server has different security capabilities than a mobile app or a command-line tool. OAuth 2.0 provides different flows (also called grant types) for different scenarios.
The Main OAuth 2.0 Flows
Web apps with backend
Mobile apps, SPAs
Server-to-server
Legacy SPAs
Authorization Code Flow
The most secure and commonly used flow for web applications with a backend server.
How It Works
โโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
โ User โ โ Your App โ โ Auth Server โ
โ (Browser)โ โ (Backend) โ โ (e.g. Google) โ
โโโโโโฌโโโโโโ โโโโโโโโโฌโโโโโโโโ โโโโโโโโโโฌโโโโโโโโโ
โ โ โ
โ 1. Click "Login" โ โ
โโโโโโโโโโโโโโโโโโโ>โ โ
โ โ โ
โ 2. Redirect to auth server โ
โ<โโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ 3. Login + Consentโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ>โ
โ โ โ
โ 4. Redirect with authorization code โ
โ<โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ โ
โ 5. Send code to backend โ
โโโโโโโโโโโโโโโโโโโ>โ โ
โ โ โ
โ โ 6. Exchange code for tokens
โ โโโโโโโโโโโโโโโโโโโโโโ>โ
โ โ โ
โ โ 7. Access + Refresh tokens
โ โ<โโโโโโโโโโโโโโโโโโโโโโ
โ โ โ
โ 8. User logged in โ โ
โ<โโโโโโโโโโโโโโโโโโโ โ
Step-by-Step Code Example
Step 1: Redirect user to authorization server// Your app generates the authorization URL
const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
authUrl.searchParams.set('client_id', 'YOUR_CLIENT_ID');
authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/callback');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'openid email profile');
authUrl.searchParams.set('state', generateRandomState()); // CSRF protection
// Redirect user
window.location.href = authUrl.toString();
Step 2: Handle the callback
// User is redirected back to https://yourapp.com/callback?code=ABC123&state=XYZ
app.get('/callback', async (req, res) => {
const { code, state } = req.query;
// Verify state to prevent CSRF
if (state !== req.session.oauthState) {
return res.status(403).send('Invalid state');
}
// Exchange code for tokens
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: 'YOUR_CLIENT_ID',
client_secret: 'YOUR_CLIENT_SECRET', // Kept safe on server!
code: code,
grant_type: 'authorization_code',
redirect_uri: 'https://yourapp.com/callback'
})
});
const tokens = await tokenResponse.json();
// tokens = { access_token, refresh_token, expires_in, id_token }
});
- Client secret never exposed to browser
- Tokens exchanged on secure backend channel
- Authorization code is short-lived and single-use
- State parameter prevents CSRF attacks
Authorization Code with PKCE
PKCE (Proof Key for Code Exchange, pronounced "pixy") adds security for apps that can't safely store a client secret - like mobile apps and single-page applications.The Problem PKCE Solves
Mobile apps can be decompiled. SPAs run entirely in the browser. The client secret isn't secret anymore!
How PKCE Works
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 1. App generates: โ
โ - code_verifier: random 43-128 character string โ
โ - code_challenge: SHA256(code_verifier), base64url โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 2. App sends code_challenge with auth request โ
โ (keeps code_verifier secret) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 3. Auth server stores code_challenge with auth code โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 4. App exchanges code + code_verifier for tokens โ
โ Auth server verifies: SHA256(verifier) == challenge โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
PKCE Code Example
// Step 1: Generate PKCE values
function generatePKCE() {
// Generate random code_verifier
const verifier = base64URLEncode(crypto.getRandomValues(new Uint8Array(32)));
// Create code_challenge from verifier
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
const challenge = base64URLEncode(new Uint8Array(hash));
return { verifier, challenge };
}
// Step 2: Authorization request with challenge
const { verifier, challenge } = await generatePKCE();
sessionStorage.setItem('pkce_verifier', verifier);
const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
authUrl.searchParams.set('client_id', 'YOUR_CLIENT_ID');
authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/callback');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'openid email profile');
authUrl.searchParams.set('code_challenge', challenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
// Step 3: Token exchange with verifier
const verifier = sessionStorage.getItem('pkce_verifier');
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: 'YOUR_CLIENT_ID',
code: authorizationCode,
grant_type: 'authorization_code',
redirect_uri: 'https://yourapp.com/callback',
code_verifier: verifier // Proves we started this flow
})
});
Even if an attacker intercepts the authorization code, they can't exchange it for tokens without the code_verifier. And they can't compute the verifier from the challenge (SHA-256 is one-way).
Client Credentials Flow
For machine-to-machine communication where no user is involved.
Use Cases
- Backend service calling another API
- Scheduled jobs accessing resources
- Microservices communicating internally
How It Works
// Simple token request - no user interaction needed
const tokenResponse = await fetch('https://api.stripe.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic ' + btoa(CLIENT_ID + ':' + CLIENT_SECRET)
},
body: new URLSearchParams({
grant_type: 'client_credentials',
scope: 'read_only'
})
});
const { access_token } = await tokenResponse.json();
// Use token to call API
const data = await fetch('https://api.stripe.com/v1/customers', {
headers: { 'Authorization': `Bearer ${access_token}` }
});
When to Use
- Server-to-server APIs
- Background jobs
- Service accounts
Never Use For
- User-facing applications
- Mobile apps
- Browser-based apps
Choosing the Right Flow
| Application Type | Recommended Flow | Why |
|---|---|---|
| Web app (with backend) | Authorization Code | Can securely store client secret |
| Single-page app (SPA) | Auth Code + PKCE | No backend, can't store secrets |
| Mobile app | Auth Code + PKCE | Can be decompiled, secrets exposed |
| Server-to-server | Client Credentials | No user involved, trusted environment |
| CLI tool | Device Code or Auth Code + PKCE | Limited input capabilities |
Real-World: GitHub OAuth
GitHub uses Authorization Code flow. Here's how CircleCI integrates:
// 1. CircleCI redirects you to GitHub
// https://github.com/login/oauth/authorize?
// client_id=CIRCLECI_CLIENT_ID&
// redirect_uri=https://circleci.com/auth/github&
// scope=repo,user:email&
// state=random123
// 2. After you approve, GitHub redirects back:
// https://circleci.com/auth/github?code=ABC123&state=random123
// 3. CircleCI exchanges code for token (server-side)
const response = await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',
headers: { 'Accept': 'application/json' },
body: JSON.stringify({
client_id: GITHUB_CLIENT_ID,
client_secret: GITHUB_CLIENT_SECRET,
code: 'ABC123'
})
});
// 4. CircleCI can now access your repos via GitHub API
const repos = await fetch('https://api.github.com/user/repos', {
headers: { 'Authorization': `token ${access_token}` }
});
Key Takeaways
- Authorization Code - Best for web apps with backend (can store secrets)
- Auth Code + PKCE - Required for SPAs and mobile (can't store secrets)
- Client Credentials - Machine-to-machine only (no user involved)
- Implicit is deprecated - Always use PKCE for browser apps
Next up: Understanding Tokens - Access tokens, refresh tokens, and how they work together.