OAuth 2.0: The Complete Guide to Authorization Flows and Security
OAuth 2.0 is the industry-standard protocol for authorization. It enables applications to obtain limited access to user accounts on third-party services without exposing passwords. Every time you click "Sign in with Google" or "Connect with GitHub," OAuth 2.0 is working behind the scenes. This guide covers all grant types, token management, OpenID Connect, and security best practices essential for system design interviews.
OAuth 2.0 Roles
| Role | Description | Example |
|---|---|---|
| Resource Owner | The user who authorizes access to their data | You, the end user |
| Client | The application requesting access | A third-party app (Spotify, Slack) |
| Authorization Server | Authenticates the user and issues tokens | Google Accounts, Auth0, Okta |
| Resource Server | Hosts the protected resources (APIs) | Google Calendar API, GitHub API |
Grant Types Overview
| Grant Type | Best For | Confidential Client? | User Interaction? |
|---|---|---|---|
| Authorization Code | Server-side web apps | Yes | Yes |
| Authorization Code + PKCE | SPAs, mobile apps, native apps | No | Yes |
| Client Credentials | Service-to-service (M2M) | Yes | No |
| Device Code | Smart TVs, CLI tools, IoT | No | Yes (on another device) |
| Implicit (Deprecated) | Legacy SPAs (use PKCE instead) | No | Yes |
Authorization Code Flow
The most secure and common flow for server-side applications. The authorization code is exchanged for tokens via a back-channel (server-to-server), keeping tokens out of the browser.
Step-by-Step Flow
- User clicks "Login with Provider" — Client redirects to authorization server
- User authenticates — Enters credentials on the authorization server's login page
- User grants consent — Approves the requested scopes
- Authorization server redirects back — Returns an authorization code to the client's redirect URI
- Client exchanges code for tokens — Server-to-server POST request with the code
- Authorization server returns tokens — Access token + refresh token
Authorization Request
GET /authorize?
response_type=code
&client_id=abc123
&redirect_uri=https://myapp.com/callback
&scope=openid profile email
&state=xyzRandom123
HTTP/1.1
Host: auth.provider.com
Token Exchange Request
POST /token HTTP/1.1
Host: auth.provider.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=AUTHORIZATION_CODE_HERE
&redirect_uri=https://myapp.com/callback
&client_id=abc123
&client_secret=secret456
Token Response
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBhIHJlZnJl...",
"scope": "openid profile email",
"id_token": "eyJhbGciOiJSUzI1NiIs..."
}
Authorization Code Flow with PKCE
PKCE (Proof Key for Code Exchange) adds an extra security layer for public clients like SPAs and mobile apps that cannot securely store a client secret. It prevents authorization code interception attacks.
const crypto = require('crypto');
// Step 1: Generate code verifier (random string)
function generateCodeVerifier() {
return crypto.randomBytes(32)
.toString('base64url'); // 43-character URL-safe string
}
// Step 2: Generate code challenge (SHA-256 hash of verifier)
function generateCodeChallenge(verifier) {
return crypto.createHash('sha256')
.update(verifier)
.digest('base64url');
}
// Usage
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);
// Include in authorization request:
// &code_challenge=CODE_CHALLENGE
// &code_challenge_method=S256
// Include verifier in token exchange:
// &code_verifier=CODE_VERIFIER
The authorization request includes the code_challenge. When exchanging the code for tokens, the client sends the original code_verifier. The server hashes it and compares — only the original client can complete the exchange.
Client Credentials Flow
Used for machine-to-machine communication where no user is involved. The client authenticates directly with its own credentials.
// Service-to-service token request
async function getM2MToken() {
const response = await fetch('https://auth.provider.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
scope: 'api:read api:write'
})
});
const data = await response.json();
return data.access_token; // No refresh token issued
}
Device Code Flow
Designed for devices with limited input capabilities (smart TVs, game consoles, CLI tools). The device displays a code, and the user completes authentication on another device like a phone or computer.
// Step 1: Device requests codes
// POST /device/code
// Response: { device_code, user_code, verification_uri, interval }
// Step 2: Display to user
console.log(`Visit ${verification_uri} and enter code: ${user_code}`);
// Step 3: Poll for completion
async function pollForToken(deviceCode, interval) {
while (true) {
await sleep(interval * 1000);
const response = await fetch('https://auth.provider.com/token', {
method: 'POST',
body: new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
device_code: deviceCode,
client_id: CLIENT_ID
})
});
const data = await response.json();
if (data.access_token) return data;
if (data.error === 'expired_token') throw new Error('Code expired');
// 'authorization_pending' or 'slow_down' — keep polling
}
}
Access Tokens vs Refresh Tokens
| Feature | Access Token | Refresh Token |
|---|---|---|
| Purpose | Access protected resources | Obtain new access tokens |
| Lifetime | Short (15 min - 1 hour) | Long (days - months) |
| Sent To | Resource server (API) | Authorization server only |
| Storage | Memory (preferred) or secure cookie | HttpOnly secure cookie or secure storage |
| Revocable | Not easily (if JWT) | Yes (server-side check) |
Token Refresh Implementation
async function refreshAccessToken(refreshToken) {
const response = await fetch('https://auth.provider.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET
})
});
if (!response.ok) {
// Refresh token expired or revoked — redirect to login
redirectToLogin();
return null;
}
const data = await response.json();
// Implement refresh token rotation for security
storeTokens(data.access_token, data.refresh_token);
return data.access_token;
}
Learn more about JWT token structure and how access tokens encode claims for authorization decisions.
OpenID Connect (OIDC)
OpenID Connect is an identity layer built on top of OAuth 2.0. While OAuth 2.0 handles authorization (access to resources), OIDC adds authentication (identity verification).
OIDC introduces the ID Token, a JWT that contains user identity claims:
// Decoded ID Token payload
{
"iss": "https://auth.provider.com",
"sub": "user123", // Unique user identifier
"aud": "abc123", // Your client ID
"exp": 1700000000,
"iat": 1699996400,
"nonce": "randomNonce", // Prevents replay attacks
"name": "Jane Developer",
"email": "jane@example.com",
"email_verified": true,
"picture": "https://example.com/jane.jpg"
}
OIDC Scopes
| Scope | Claims Returned |
|---|---|
| openid | sub (required for OIDC) |
| profile | name, family_name, given_name, picture |
| email, email_verified | |
| address | formatted address, street_address, locality |
| phone | phone_number, phone_number_verified |
Security Best Practices
- Always use PKCE — Even for confidential clients, PKCE adds protection. It is mandatory for public clients.
- Validate the state parameter — Prevents CSRF attacks. Generate a random state, store it in session, and verify it in the callback.
- Use short-lived access tokens — 15 minutes is a good default. Use refresh tokens for longer sessions.
- Implement token rotation — Issue a new refresh token with every refresh, and invalidate the old one.
- Validate redirect URIs strictly — Use exact string matching. Never allow open redirects.
- Store tokens securely — Use HttpOnly, Secure, SameSite cookies. Never store tokens in localStorage.
- Use TLS encryption — All OAuth 2.0 communication must use HTTPS.
- Validate ID tokens — Check issuer, audience, expiration, and signature. Verify the nonce to prevent replay attacks.
Test OAuth flows and inspect tokens with our API and Network Tools, and explore Security Crypto Tools for JWT debugging.
Common Vulnerabilities and Mitigations
| Vulnerability | Attack | Mitigation |
|---|---|---|
| Authorization code interception | Attacker intercepts code from redirect | Use PKCE |
| CSRF on callback | Attacker injects their auth code | Validate state parameter |
| Open redirect | Malicious redirect_uri steals code | Exact redirect URI matching |
| Token leakage | Tokens in URL fragments or logs | Use auth code flow, not implicit |
| Refresh token theft | Stolen refresh token used indefinitely | Refresh token rotation + binding |
Frequently Asked Questions
When should I use OAuth 2.0 vs simple API keys?
Use API keys for server-to-server communication where you control both sides and need simple identification. Use OAuth 2.0 when third-party applications need delegated access to user data, when you need fine-grained scopes, or when user consent is required. OAuth 2.0 is also better for user-facing applications because tokens can be scoped and revoked.
Is the implicit grant type still safe to use?
No. The implicit grant is deprecated in the OAuth 2.0 Security Best Current Practice (RFC). It exposes access tokens in the URL fragment, making them vulnerable to interception. Always use Authorization Code flow with PKCE for browser-based applications instead.
What is the difference between OAuth 2.0 and OAuth 2.1?
OAuth 2.1 is a consolidation of OAuth 2.0 with its security best practice RFCs. Key changes include: PKCE is required for all authorization code grants, the implicit grant is removed, refresh tokens must be sender-constrained or one-time use, and bearer tokens in query strings are prohibited. If starting fresh, design for OAuth 2.1 compliance.
How do I choose the right grant type?
Server-rendered web app with a backend: Authorization Code. SPA or mobile app: Authorization Code + PKCE. Backend service calling another API: Client Credentials. Smart TV or CLI tool: Device Code. If you are unsure, Authorization Code + PKCE is the safest default for any user-facing scenario.
How does OAuth 2.0 work with microservices?
In a microservices architecture, the API gateway typically validates access tokens and forwards user claims to downstream services. Services can use client credentials for service-to-service calls. A centralized authorization server issues all tokens, and each service validates them independently. Use our tools to explore these patterns interactively.