← All Visualizations
Security Deep Dive

OAuth 2.0 & OIDC
Explained

Follow how clients obtain and use tokens, where PKCE protects the authorization-code exchange, and how OpenID Connect adds identity.

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 read:email or write:calendar. The authorization server decides what they mean and which ones it grants.
CONCEPT

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.

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
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
📱 Client
App (Server)
🔐 Authorization
Server
🗄️ Resource
Server


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 }
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
📱 SPA
(JS in browser)
🔐 Authorization
Server
🗄️ Resource
Server

Security Vulnerabilities
🔗
Token in URL Fragment
The access token returns in the URI fragment as #access_token=eyJ.... Browser code must extract it, which exposes the credential to the user agent and scripts running in the redirect page.
CRITICAL
🧩
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.
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.
1
Generate code_verifier
The 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_challenge
The 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)
🔐 Authorization
Server
🗄️ Resource
Server

// 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.
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)
🔐 Authorization
Server
🗄️ Resource
Server / API

// 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...
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 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.
SECURITY
♻️
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.