Foundation
Core Concepts
OAuth 2.0 is an authorization framework that lets applications obtain limited access to user accounts without exposing passwords. Before exploring the flows, understand these building blocks.
Resource Owner
The user who owns the data and can grant or deny access to it. When you click "Allow" on a permissions screen, you're acting as the Resource Owner.
ACTOR
Client
The application requesting access to the resource owner's data. Clients are either confidential (can store secrets — like backend servers) or public (cannot — like SPAs or mobile apps).
ACTOR
Authorization Server
Issues tokens after authenticating the user and obtaining consent. Examples: Auth0, Okta, Google Identity, Keycloak. It's the gatekeeper that validates who you are.
ACTOR
Resource Server
The API holding the protected data. It validates the access token on every request. Often the same server as the auth server in smaller systems, but conceptually separate.
ACTOR
Access Token
A short-lived credential (often a JWT) the client sends with API requests. Typically expires in 15 minutes to 1 hour. The resource server validates it without calling the auth server (if using JWT).
TOKEN
Refresh Token
A long-lived token used to obtain new access tokens without re-prompting the user. Only given in confidential flows. Stored securely on the server — never exposed to a browser.
TOKEN
Authorization Code
A short-lived, single-use code returned to the client after user consent. It's exchanged for tokens via a back-channel server-to-server request. Not a token itself — just a temporary voucher.
INTERMEDIARY
Scopes
Fine-grained permissions requested by the client. Examples:
CONCEPT
read:email, write:calendar. The user sees and approves them on the consent screen. Principle of least privilege — ask only for what you need.Token Format
JSON Web Tokens (JWT)
JWTs are the most common format for OAuth 2.0 access tokens. A JWT is a self-contained, cryptographically signed credential that can be verified without a database lookup. Click each colored section to inspect its decoded contents.
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFiYzEyMyJ9.eyJzdWIiOiJ1c2VyXzQ4NyIsImlzcyI6Imh0dHBzOi8vYXV0aC5leGFtcGxlLmNvbSIsImF1ZCI6ImFwaS5leGFtcGxlLmNvbSIsImV4cCI6MTcxNTAwMDAwMCwiaWF0IjoxNzE0OTk2NDAwLCJzY29wZSI6InJlYWQ6cHJvZmlsZSByZWFkOmVtYWlsIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
① Header
{
"alg": "RS256",
"typ": "JWT",
"kid": "abc123"
}
Algorithm, token type & key ID used to verify signature
② Payload (Claims)
{
"sub": "user_487",
"iss": "https://auth.example.com",
"aud": "api.example.com",
"exp": 1715000000,
"iat": 1714996400,
"scope": "read:profile read:email"
}
User ID, issuer, audience, expiry & granted scopes
③ Signature
RSASHA256( base64(header) + "." + base64(payload), privateKey )
Proves the token hasn't been tampered with. Verified with public key.
Stateless Verification
The resource server verifies the signature using the auth server's public key — no database lookup, no network call. This is why JWTs scale beautifully in distributed systems.
Cannot Be Revoked Instantly
Since JWTs are self-verified, a stolen token is valid until expiry. This is why expiry should be short (15–60 min). Token revocation usually requires a denylist or very short TTLs.
Payload Is Readable
Base64 is encoding, not encryption. Anyone can decode the payload. Never put sensitive data (passwords, SSNs) in a JWT. The signature only proves authenticity — not confidentiality.
Overview
The Four Flows at a Glance
Each OAuth 2.0 grant type is suited to a different client type and security context.
| Grant Type | Client Type | User Involved? | Refresh Token? | Recommended? |
|---|---|---|---|---|
| Authorization Code | Confidential | ✓ Yes | ✓ Yes | ✓ Best for web apps |
| Auth Code + PKCE | Public (SPA, mobile) | ✓ Yes | ⚑ Sometimes | ✓ Best for public clients |
| Implicit (deprecated) | Public (SPA) | ✓ Yes | ✗ No | ✗ Deprecated — insecure |
| Client Credentials | Confidential | ✗ No | ✗ Rarely | ✓ Best for M2M |
Grant Type 01
Authorization Code Flow
The gold standard for server-side web applications. Tokens are exchanged on the back-channel (server-to-server) so they never touch the browser. Click each step in the diagram to learn more.
When to use this flow
Use Authorization Code when your application has a backend server that can securely store a
client_secret. Typical examples: traditional web apps (Node, Rails, Django, Laravel), backend-for-frontend (BFF) architectures.👤 User
/ Browser
/ Browser
📱 Client
App (Server)
App (Server)
🔐 Authorization
Server
Server
🗄️ Resource
Server
Server
Animated Walkthrough
Key Exchange — Step 4
Token Exchange Request
The client sends this POST request from its server — never from the browser. The client_secret proves the client's identity to the auth server.
// Step 4: Server-side token exchange (NEVER in browser)
POST /oauth/token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA // the authorization code
&redirect_uri=https://app.example.com/callback
&client_id=my_client_id
&client_secret=super_secret_value // only server knows this
// Response
{
"access_token": "eyJhbGci...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA" // long-lived, store securely
}
Grant Type 02 — Deprecated
Implicit Flow
Designed in 2012 for single-page apps before CORS and PKCE existed. The auth server returned tokens directly in the URL fragment. It's now officially deprecated by RFC 9700 and should never be used for new systems.
DEPRECATED — Do Not Use
The OAuth Security Best Current Practice (BCP) document explicitly forbids Implicit Flow for new OAuth 2.0 deployments. Use Authorization Code + PKCE instead for all public clients.
👤 User
/ Browser
/ Browser
📱 SPA
(JS in browser)
(JS in browser)
🔐 Authorization
Server
Server
🗄️ Resource
Server
Server
Why It Was Wrong
Security Vulnerabilities
Token in URL Fragment
The access token appears in
CRITICAL
#access_token=eyJ... in the browser's address bar. This token is stored in browser history, server logs, and is accessible to any JavaScript on the page — including analytics and ad scripts.No Client Authentication
There's no way to verify the recipient of the token is actually the registered client. A malicious page that redirects to the auth server with a stolen
CRITICAL
client_id can obtain tokens.Referrer Header Leakage
When a user navigates from a page with a token-bearing URL to any external resource (images, fonts, third-party JS), the browser sends the full URL — including the token — in the
HIGH
Referer header.XSS Amplification
Tokens stored in JavaScript variables (memory) or localStorage are fully readable by any XSS payload. Unlike httpOnly cookies, there's no browser-enforced sandbox. A single XSS vulnerability means complete token compromise.
HIGH
No Refresh Tokens
Implicit flow never issues refresh tokens (by design). When the access token expires, the user must go through the entire flow again. This forced a terrible pattern: extremely long-lived access tokens, which worsened the theft risk.
DESIGN FLAW
Why It Existed (2012 context)
In 2012, browser CORS support was limited. SPAs couldn't make cross-origin POST requests to exchange codes for tokens. The fragment trick worked around this. Today, all browsers support CORS, making Auth Code + PKCE viable for SPAs.
HISTORICAL
The One Difference From Auth Code Flow
Implicit flow skips the authorization code entirely. Instead of: redirect → code → POST for token, it goes directly: redirect → token in URL. This eliminated the secure back-channel exchange — which was the only thing protecting tokens from exposure.
// Implicit Flow: token returned DIRECTLY in redirect URL fragment
// ❌ Never do this — for educational reference only
// Step 1: Redirect with response_type=token (not code)
GET https://auth.example.com/authorize?
response_type=token // ← "token" instead of "code"
&client_id=spa_client
&redirect_uri=https://app.example.com/callback
&scope=read:profile
// Step 2: Auth server redirects BACK with token in fragment
https://app.example.com/callback
// ↓↓↓ THE PROBLEM: token is in the URL
#access_token=eyJhbGciOiJSUzI1NiJ9...
&token_type=Bearer
&expires_in=3600
// NO refresh_token, NO client verification
Modern Replacement: Authorization Code + PKCE
All modern auth servers support CORS on the token endpoint. SPAs should use Auth Code + PKCE — they get the secure code exchange, plus PKCE ensures only the originating app can redeem the code. See the PKCE tab for the full flow.
Grant Type 03
Authorization Code + PKCE
PKCE (Proof Key for Code Exchange, pronounced "pixie") extends the Authorization Code flow for public clients — mobile apps and single-page apps that can't store a client_secret. It uses a cryptographic challenge to prove that the entity redeeming the code is the same one that initiated the request.
The Core Problem PKCE Solves
Without PKCE, if an attacker intercepts the authorization code (via a malicious redirect URI, a compromised network, or a log file), they can exchange it for tokens. PKCE makes a stolen code useless — only the original requester can redeem it because they hold the secret
code_verifier.How The Cryptographic Proof Works
1
Generate
code_verifierThe client generates a cryptographically random string of 43–128 characters. This is a secret known only to the client and is never sent to the auth server directly.
2
Derive
code_challengeThe client computes:
code_challenge = BASE64URL(SHA256(code_verifier)) — a one-way hash. The hash can be publicly sent; the original verifier cannot be derived from it.3
Send challenge in authorization request
The client includes
code_challenge and code_challenge_method=S256 in the /authorize redirect. The auth server stores this challenge alongside the authorization code it issues.4
Redeem code with verifier
When exchanging the code for tokens, the client sends the original
code_verifier. The auth server hashes it and compares to the stored challenge. They must match — proving same-origin.5
Attacker is defeated
Even if an attacker intercepts the authorization code, they don't know the
code_verifier. The auth server rejects any token exchange without the matching verifier. Code theft is useless.👤 User
📱 Public Client
(SPA / Mobile)
(SPA / Mobile)
🔐 Authorization
Server
Server
🗄️ Resource
Server
Server
Code Example
// Step 1 & 2: Generate verifier + challenge (browser/client-side)
const codeVerifier = crypto.getRandomValues(new Uint8Array(64))
|> Array.from(#).map(b => b.toString(36)).join('').slice(0, 64);
const digest = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(codeVerifier)
);
const codeChallenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); // base64url
// Step 3: Authorization redirect (includes challenge)
const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', 'my_spa_client');
authUrl.searchParams.set('redirect_uri', 'https://app.example.com/callback');
authUrl.searchParams.set('scope', 'openid profile');
authUrl.searchParams.set('code_challenge', codeChallenge); // ← the hash
authUrl.searchParams.set('code_challenge_method', 'S256');
window.location.href = authUrl.toString();
// Step 4: Token exchange (includes verifier)
POST /oauth/token
grant_type=authorization_code
&code={authorization_code_from_callback}
&redirect_uri=https://app.example.com/callback
&client_id=my_spa_client
&code_verifier={the_original_random_string} // ← auth server hashes & compares
PKCE Is Now Recommended for ALL Flows
RFC 9700 recommends PKCE even for confidential clients (with a client_secret). It provides an additional layer of protection against code injection attacks — there's no downside to using it universally.
Grant Type 04
Client Credentials Flow
The simplest OAuth flow — and the only one with no user involvement. Designed for machine-to-machine (M2M) communication: microservices, daemons, background jobs, and APIs talking to other APIs. The client authenticates as itself, not on behalf of any user.
When to use this flow
Use Client Credentials when there is no human user involved. Examples: a cron job fetching data from an API, a microservice calling another service, CI/CD pipelines authenticating to deployment APIs, a backend batch processor reading a data warehouse.
🖥️ Client
(Service/Daemon)
(Service/Daemon)
🔐 Authorization
Server
Server
🗄️ Resource
Server / API
Server / API
Token Request
// Step 1: Client authenticates directly to token endpoint
POST /oauth/token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret) // HTTP Basic Auth
grant_type=client_credentials
&scope=api:read api:write
// Response — no user context in the token
{
"access_token": "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzZXJ2aWNlX2FjY291bnQiLCJzY29wZSI6ImFwaTpyZWFkIn0...",
"token_type": "Bearer",
"expires_in": 3600
// NOTE: No refresh_token — just request a new access token when needed
}
// Step 2: Use token to call protected API
GET /api/v1/data HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
JWT Payload Comparison
Notice the difference between a user-context token and a service-context token:
AUTH CODE TOKEN (user context)
{
"sub": "user_487", // user's ID
"email": "alice@example.com",
"scope": "read:profile",
"iss": "https://auth.example.com",
"exp": 1715000000
}
CLIENT CREDS TOKEN (service context)
{
"sub": "service_account_7", // service ID
"client_id": "my_service",
"scope": "api:read api:write",
"iss": "https://auth.example.com",
"exp": 1715000000
}
Extremely Simple
Just one round-trip: POST credentials, receive token. No redirects, no user interaction, no consent screens. Ideal for automated infrastructure.
Protect the client_secret
The
SECURITY
client_secret is equivalent to a password for your service. Store it in environment variables, a secrets manager (Vault, AWS Secrets Manager), or a platform secret — never in source code or logs.Token Caching Pattern
Best practice: cache the access token until 60 seconds before expiry, then request a new one. Avoids hammering the auth server on every API call. No refresh tokens — just re-authenticate with credentials.
PATTERN
Secret Rotation Strategy
Client secrets should be rotated regularly. Many auth servers support having two active secrets simultaneously during rotation — issue a new secret, deploy services to use it, then revoke the old one. Zero-downtime rotation.