Foundation
Core Concepts
OAuth 2.0 lets a client request scoped access to a protected resource. The actors and tokens below play different roles in that exchange; keeping them separate makes each flow easier to reason about.
Resource Owner
The party that can grant access to the protected resource. In a user-facing flow, this is often the person reviewing the client's request.
ACTOR
Client
The application requesting access. A confidential client can protect credentials in a controlled backend; a public client, such as a browser or native app, cannot keep a shared secret from its users.
ACTOR
Authorization Server
Issues tokens after it authorizes the client's request. In user-facing flows it commonly authenticates the user and may collect consent. Examples include Auth0, Okta, Google Identity, and Keycloak.
ACTOR
Resource Server
The API that accepts access tokens for protected operations. It validates each token according to the authorization server's contract before serving the request.
ACTOR
Access Token
A credential the client presents to the resource server. Its format, lifetime, audience, and validation method come from the authorization server; OAuth does not require it to be a JWT.
TOKEN
Refresh Token
A credential a client can use to obtain another access token without repeating the full authorization flow. Whether the server issues one and how the client protects it depend on the client type and authorization-server policy.
TOKEN
Authorization Code
A short-lived code returned through the browser after authorization. The client sends it to the token endpoint, along with the required client authentication or PKCE verifier, to request tokens.
INTERMEDIARY
Scopes
Strings that describe the access a client requests, such as
CONCEPT
read:email or write:calendar. The authorization server decides what they mean and which ones it grants.Token Format
JSON Web Tokens (JWT)
An authorization server may issue an access token as a JWT, although OAuth does not require that format. A signed JWT carries claims that a resource server can validate locally when the deployment is designed for it. Select each colored section to inspect this example.
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.
Local verification
The resource server can verify a signed JWT with the issuer's public key and validate claims such as issuer, audience, and expiration. Some deployments still consult server-side state for revocation or policy.
Revocation needs a design
A resource server validating a JWT locally may continue accepting it until expiration. Faster revocation requires another mechanism, such as short lifetimes, server-side state, key rotation, or introspection where the system supports it.
Payload Is Readable
Base64url encoding does not hide the claims. Anyone holding this signed JWT can read its payload; the signature protects integrity and 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
A server-side client can exchange the authorization code at the token endpoint and keep the resulting tokens in its backend. Select a step to see what each actor sends and receives.
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
The implicit grant returned an access token through the browser redirect instead of issuing an authorization code for a token-endpoint exchange. Current OAuth security guidance says clients should use the authorization code flow and avoid the implicit grant. RFC 9700
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 returns in the URI fragment as
CRITICAL
#access_token=eyJ.... Browser code must extract it, which exposes the credential to the user agent and scripts running in the redirect page.No Client Authentication
A public client cannot authenticate with a shared secret, and the implicit flow offers no code-verifier check at the token endpoint. The browser receives a bearer credential directly, leaving fewer opportunities to bind redemption to the initiating client instance.
CRITICAL
Referrer Header Leakage
Tokens delivered through a browser URI can leak through browser behavior, extensions, application code, or incorrect redirect handling. The fragment is normally excluded from HTTP requests, but it still reaches browser-side code.
HIGH
XSS Amplification
A script that executes in the application's origin can read tokens available to JavaScript, including values held in memory or local storage. Storage choice changes exposure, but it does not repair an XSS vulnerability.
HIGH
No Refresh Tokens
The implicit grant does not return a refresh token. Applications historically worked around expiration with another authorization request or longer-lived access tokens, increasing complexity or exposure.
DESIGN FLAW
Why It Existed (2012 context)
The flow addressed browser constraints that made a direct token-endpoint exchange difficult for early single-page applications. Modern authorization servers can support browser-based code exchange with CORS and PKCE.
HISTORICAL
Where the implicit flow diverges
Authorization Code returns a code through the redirect and obtains tokens from the token endpoint. Implicit returns an access token through the redirect itself. That removes the separate redemption step where PKCE can bind the code to the client instance.
// 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
The client authenticates directly at the token endpoint and receives an access token. No resource owner or browser redirect participates, which suits service-to-service access.
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
Reuse the access token until it approaches expiration, allowing enough clock-skew and retry margin for the deployment. Then authenticate again with the client credentials to obtain another token.
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.