OAuth 2.0 & OIDC Flows
OAuth 2.0 lets apps get limited access to user resources via scoped tokens, and OIDC adds an identity layer with ID tokens so apps know who the user is.
The Problem
Applications need to access user data on third-party services without handling user passwords. Users want to grant limited access to specific apps without sharing their credentials. OAuth 2.0 solves the authorization problem, and OIDC adds standardized identity so apps can also know who the user is.
Mental Model
Like a valet parking ticket — the owner gives limited permission (just park and retrieve the car) without handing over the house keys. The valet ticket is the access token, the parking garage is the resource server, and the valet stand is the authorization server.
Architecture Diagram
How It Works
OAuth 2.0 is the authorization framework that powers "Sign in with Google," GitHub app permissions, and almost every third-party API integration on the internet. It solves a specific problem: how does a user grant a third-party application limited access to their resources without sharing their password?
The answer is tokens. Instead of sharing credentials, the user authenticates directly with the authorization server and approves specific permissions (scopes). The authorization server then issues an access token to the application, which uses it to access the user's resources on the resource server.
The Core OAuth 2.0 Flows
Authorization Code + PKCE (Recommended for All Clients)
This is the recommended flow for virtually every scenario — web apps, SPAs, mobile apps, and server-side applications. PKCE (Proof Key for Code Exchange, pronounced "pixie") was originally designed for mobile apps that cannot securely store a client secret, but it is now recommended for all clients.
Here is the step-by-step flow:
Step 1 — Generate PKCE Parameters: The client generates a random code_verifier (43-128 characters) and computes code_challenge = BASE64URL(SHA256(code_verifier)).
Step 2 — Authorization Request: The client redirects the user to the authorization server's /authorize endpoint with:
GET /authorize?
response_type=code&
client_id=my-app&
redirect_uri=https://my-app.com/callback&
scope=openid profile email read:orders&
state=random-csrf-token&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256
Step 3 — User Authentication + Consent: The authorization server presents a login page. The user authenticates and sees a consent screen listing the requested scopes. They approve or deny.
Step 4 — Authorization Code: The authorization server redirects back to the client's redirect_uri with a short-lived authorization code:
GET /callback?code=AUTH_CODE_HERE&state=random-csrf-token
Step 5 — Token Exchange: The client sends the authorization code and the original code_verifier to the token endpoint:
curl -X POST https://auth-server.com/token \
-d "grant_type=authorization_code" \
-d "code=AUTH_CODE_HERE" \
-d "redirect_uri=https://my-app.com/callback" \
-d "client_id=my-app" \
-d "code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
Step 6 — Tokens Issued: The authorization server verifies that SHA256(code_verifier) matches the original code_challenge. If valid, it returns:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "v1.MjE0ODM2...",
"id_token": "eyJhbGciOiJSUzI1NiIs..."
}
Why PKCE Matters
Without PKCE, the authorization code can be intercepted (via a malicious browser extension, compromised redirect, or mobile app interception) and exchanged for tokens by the attacker. PKCE prevents this because the attacker does not have the code_verifier — only the legitimate client that generated it can complete the exchange.
Client Credentials Flow (Machine-to-Machine)
When there is no user involved — service-to-service API calls, cron jobs, backend processes — use the Client Credentials flow:
curl -X POST https://auth-server.com/token \
-d "grant_type=client_credentials" \
-d "client_id=payment-service" \
-d "client_secret=SECRET" \
-d "scope=billing:write orders:read"
The authorization server authenticates the client directly (using its client ID and secret) and issues an access token. No user, no redirect, no browser.
Refresh Token Rotation
Access tokens are short-lived (typically 15 minutes to 1 hour). When they expire, the client uses the refresh token to get a new one without requiring the user to log in again:
curl -X POST https://auth-server.com/token \
-d "grant_type=refresh_token" \
-d "refresh_token=v1.MjE0ODM2..." \
-d "client_id=my-app"
Refresh token rotation is a critical security practice: each time a refresh token is used, the authorization server issues a new refresh token and invalidates the old one. If an attacker steals and uses a refresh token, the legitimate client's next refresh attempt will fail (because the token was already rotated), alerting the system to the compromise.
OpenID Connect (OIDC)
OAuth 2.0 answers what someone can do. OpenID Connect answers who someone is. OIDC is an identity layer built on top of OAuth 2.0 that adds standardized authentication.
The ID Token
The key addition is the ID Token — a JWT that contains identity claims about the authenticated user:
{
"iss": "https://auth-server.com",
"sub": "user-12345",
"aud": "my-app-client-id",
"exp": 1714000000,
"iat": 1713996400,
"nonce": "random-nonce",
"email": "user@example.com",
"name": "Jane Doe",
"picture": "https://example.com/photo.jpg"
}
Critical claims:
iss(issuer) — Must match the expected authorization server URL.sub(subject) — Unique, stable identifier for the user. Use this as the user ID, not email.aud(audience) — Must match the application's client ID. Prevents token misuse across applications.exp(expiry) — Reject tokens past this timestamp.nonce— Prevents replay attacks. The client generates a random nonce, includes it in the auth request, and verifies it appears in the ID token.
OIDC Discovery
OIDC providers publish a discovery document at /.well-known/openid-configuration that contains all the endpoints and supported features:
curl -s https://accounts.google.com/.well-known/openid-configuration | jq '{
authorization_endpoint,
token_endpoint,
userinfo_endpoint,
jwks_uri,
scopes_supported,
response_types_supported
}'
The jwks_uri endpoint is critical — it provides the public keys used to verify ID token signatures. Resource servers fetch these keys and cache them to validate JWTs without calling the authorization server on every request.
JWT Validation on the Resource Server
When a resource server receives a request with Authorization: Bearer <token>, it must validate the token before granting access:
# Python example using PyJWT
import jwt
import requests
# Fetch the authorization server's public keys (cache these!)
jwks_url = "https://auth-server.com/.well-known/jwks.json"
jwks = requests.get(jwks_url).json()
# Decode and validate the token
try:
payload = jwt.decode(
token,
jwks, # Public keys for signature verification
algorithms=["RS256"], # Only accept expected algorithm
audience="my-app-client-id", # Verify audience claim
issuer="https://auth-server.com" # Verify issuer claim
)
user_id = payload["sub"]
scopes = payload.get("scope", "").split()
except jwt.ExpiredSignatureError:
# Token is expired — client should refresh
return 401, "Token expired"
except jwt.InvalidTokenError:
# Signature invalid, wrong audience, wrong issuer, etc.
return 401, "Invalid token"
Common JWT Pitfalls
- Algorithm confusion attack — Always validate that the JWT uses the expected algorithm (RS256, ES256). Never accept
alg: noneor symmetric algorithms (HS256) when asymmetric is expected. - Missing audience validation — Without checking
aud, a token intended for App A can be replayed against App B. - Using JWTs as sessions — JWTs cannot be revoked before expiry. For immediate revocation, a deny list or short-lived tokens + refresh token rotation is needed.
Token Storage Best Practices
Where tokens are stored matters as much as how they are validated:
| Storage | Security | Notes |
|---|---|---|
| HttpOnly cookie | Best for web | Not accessible to JavaScript, immune to XSS, vulnerable to CSRF (mitigate with SameSite) |
| In-memory variable | Good | Lost on page refresh, but immune to XSS and CSRF |
| localStorage | Avoid | Accessible to any JavaScript — one XSS vulnerability exposes all tokens |
| sessionStorage | Avoid | Same XSS risk as localStorage, slightly better as it clears on tab close |
The recommended pattern for SPAs: use the Backend for Frontend (BFF) pattern. The SPA talks to a backend proxy that handles the OAuth flow and stores tokens in HttpOnly cookies. The SPA never sees the raw tokens.
Browser ←→ BFF (has cookies) ←→ Authorization Server
←→ Resource Server
This eliminates the token storage problem entirely for browser-based applications.
Key Points
- •OAuth 2.0 is an authorization framework, not an authentication protocol. OIDC adds the authentication layer on top.
- •Authorization Code + PKCE is the recommended flow for all clients — SPAs, mobile apps, and server-side apps.
- •The Implicit flow is deprecated because it exposes tokens in the URL fragment, vulnerable to history and referrer leaks.
- •Client Credentials flow is for machine-to-machine communication — no user involved, the client authenticates with its own credentials.
- •Refresh token rotation (issuing a new refresh token with each use) prevents stolen refresh tokens from being used indefinitely.
Key Components
| Component | Role |
|---|---|
| Authorization Server | Issues tokens after authenticating the user and obtaining consent — the central authority in the OAuth flow |
| Resource Server | The API that accepts access tokens and serves protected resources based on token scopes |
| Access Token | Short-lived credential (typically JWT) that grants access to specific resources and scopes |
| Refresh Token | Long-lived credential used to obtain new access tokens without re-authenticating the user |
| ID Token (OIDC) | JWT containing user identity claims (sub, email, name), issued alongside the access token in OIDC flows |
When to Use
Use Authorization Code + PKCE for any user-facing application (web, mobile, SPA). Use Client Credentials for server-to-server API calls. Use OIDC when the application needs to know who the user is, not just what they can access. Never roll a custom OAuth implementation — use a battle-tested library or service.
Tool Comparison
| Tool | Type | Best For | Scale |
|---|---|---|---|
| Auth0 | Managed | Full-featured identity platform with Universal Login, MFA, and extensive SDKs | Small-Enterprise |
| Keycloak | Open Source | Self-hosted identity provider with OIDC, SAML, LDAP federation, and fine-grained authorization | Small-Enterprise |
| Okta | Commercial | Enterprise workforce identity with SSO, lifecycle management, and compliance certifications | Enterprise |
| AWS Cognito | Managed | Serverless-friendly user pools with built-in hosted UI, integrated with API Gateway and ALB | Small-Enterprise |
Debug Checklist
- Decode the JWT at jwt.io to inspect the header, payload, and verify the signature algorithm.
- Check token expiry: decode the 'exp' claim and compare to current time — expired tokens are the #1 cause of 401 errors.
- Verify the issuer (iss) and audience (aud) claims match the expected values on the resource server.
- Test the token endpoint directly with curl: curl -X POST https://auth-server/token -d 'grant_type=authorization_code&code=...'.
- Check CORS headers if the token endpoint is called from a browser — missing CORS is a common SPA issue.
Common Mistakes
- Using the Implicit flow for SPAs. It was deprecated in the OAuth 2.0 Security BCP. Use Authorization Code + PKCE instead.
- Storing tokens in localStorage where they are accessible to any JavaScript on the page via XSS attacks.
- Not validating JWT signatures on the resource server. Accepting any well-formed JWT without checking the signature is an open door.
- Using overly broad scopes. Tokens should request the minimum scopes needed — 'read:orders' not 'admin'.
- Treating the access token as an identity assertion. Access tokens prove authorization, not identity. Use the ID token for identity.
Real World Usage
- •Google uses OAuth 2.0 + OIDC for Sign in with Google, issuing ID tokens that apps use to identify users.
- •GitHub's OAuth app flow lets third-party apps access repositories with scoped permissions using Authorization Code flow.
- •Stripe uses Client Credentials flow for server-to-server API authentication, with API keys acting as client credentials.
- •Slack's app installation flow uses Authorization Code to grant workspace-scoped access tokens.
- •AWS Cognito powers authentication for thousands of mobile apps, handling token issuance and refresh transparently.