All Visualizations
Security Deep Dive

OAuth 2.0 & OIDC
Explained

A deep-dive into authorization flows — from foundational concepts to PKCE, JWTs, and why Implicit Flow was deprecated.

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: read:email, write:calendar. The user sees and approves them on the consent screen. Principle of least privilege — ask only for what you need.
CONCEPT

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.

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

Security Vulnerabilities
🔗
Token in URL Fragment
The access token appears in #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.
CRITICAL
🧩
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 client_id can obtain tokens.
CRITICAL
📜
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 Referer header.
HIGH
🦠
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.
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
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 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
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.