TL;DR
- OAuth 2 is delegated authorization: a client app gets permission (a token) from an Authorization Server (AS) to call a Resource Server (API) on behalf of the Resource Owner (user).
- Use the Authorization Code + PKCE flow for browser SPAs, mobile, and desktop. Avoid the implicit flow.
- The client never sees the user’s password; it only handles a short‑lived authorization code, then swaps it (with PKCE) for access/refresh tokens via a back‑channel.
Cast of roles (who’s who)
[Resource Owner] The user granting access
[Client] Your app (SPA/mobile/server) asking for access
[Authorization Server] Login + consent + token issuer (IdP)
[Resource Server] API that holds the data (accepts access tokens)
OAuth 2 is about access to APIs. If you also need “who is the user?” for login, that’s OpenID Connect (OIDC): it adds an ID Token alongside access tokens.
The flow at a glance (Authorization Code + PKCE)
User/Browser Client App Authorization Server Resource Server
│ │ │ │
│ 1. Click "Sign in" │ │ │
│──────────────────────────▶ │ │ │
│ │ 2. Redirect to /authorize with │ │
│ │ client_id, redirect_uri, │ │
│ │ scope, state, code_challenge │ │
│ └────────────────────────────────▶ │ │
│ ◀──────────────────────────────────┘ │
│ 3. User authenticates & consents │ │
│ │ │
│ ┌──────────────────────────────────▶│ │
│ │ 4. AS redirects back to client │ │
│ ◀──────────────────────────┘ redirect_uri?code=...&state=...│ │
│ │ │ │
│ 5. Client exchanges code + code_verifier for tokens │ │
│ ───────────────────────────────────▶│ /token │
│ ◀───────────────────────────────────┘ access_token (+ refresh, IDT) │
│ │ │ │
│ 6. Call API with access token (Authorization: Bearer …) │ │
│ ──────────────────────────────────────────────────────────────────────▶│
│ ◀──────────────────────────────────────────────────────────────────────┘ data
PKCE (Proof Key for Code Exchange)
- Client creates a random
code_verifierand sends its hash (code_challenge) in step 2. - In step 5 the client must present the original
code_verifier. This prevents a stolencode(via redirect interception) from being redeemed by an attacker.
What’s in the requests (minimal fields)
/authorize (front‑channel redirect)
response_type=code
client_id=YOUR_CLIENT_ID
redirect_uri=https://app.example.com/callback
scope=openid profile email api.read
state=RANDOM_CSRF_TOKEN
code_challenge=BASE64URL(SHA256(code_verifier))
code_challenge_method=S256
/token (back‑channel POST)
grant_type=authorization_code
code=AUTH_CODE_FROM_CALLBACK
redirect_uri=https://app.example.com/callback
client_id=YOUR_CLIENT_ID # public clients use PKCE; confidential also send client_secret
code_verifier=ORIGINAL_RANDOM_STRING
API call
Authorization: Bearer ACCESS_TOKEN
Tokens (and where they live)
- Access Token: presented to the API (audience = API). Short‑lived (minutes). Opaque or JWT.
- Refresh Token: optionally returned; used to get a new access token without user interaction. Keep server‑side or in a secure store. Rotate on use.
- ID Token (OIDC): JWT that tells the client who logged in. Not used by the API for access control.
Storage tips
- Browsers: prefer a BFF pattern (server mediates tokens; store session in HTTP‑only cookies). Avoid long‑lived tokens in
localStorage. - Native apps: use OS keystore (Keychain/Keystore).
- Backends: store minimal session, validate tokens on each request.
Why state and PKCE matter
statebinds the callback to the initiating request to prevent CSRF and mix‑up attacks. Validate it on return.- PKCE protects public clients that can’t safely keep a client secret (SPAs/mobile). It makes code theft useless without the verifier.
Which flow when (super short)
| Scenario | Use this grant | |---|---| | Browser SPA, mobile, desktop | Authorization Code + PKCE | | Server‑to‑server (no user) | Client Credentials (no refresh tokens) | | TV/Console devices | Device Code | | Long‑lived sessions | Authorization Code + Refresh Token (rotation) |
Minimal pseudo‑code (client + server)
Client (SPA)
// 1) Build authorize URL with state + code_challenge; redirect the browser
location.href = authUrl({
response_type: "code",
client_id, redirect_uri, scope, state, code_challenge, code_challenge_method: "S256"
});
// 2) On /callback, send ?code to your backend (BFF) along with state
Backend (BFF)
// POST /callback
assert(req.body.state === session.state);
const tokenSet = await POST("/token", {
grant_type: "authorization_code",
code: req.body.code,
redirect_uri,
client_id,
code_verifier: session.code_verifier
});
// Set HTTP-only session cookie; do NOT expose refresh tokens to JS
API call (backend → resource server)
const res = await fetch(API, { headers: { Authorization: `Bearer ${accessToken}` } })
Common pitfalls & fast fixes
| Problem | Why it happens | Fix |
|---|---|---|
| Using implicit flow | Legacy; exposes tokens in URLs | Use Auth Code + PKCE |
| Missing state / not validated | CSRF/mix‑up possible | Generate random state; validate on callback |
| Tokens in localStorage | XSS exfiltration | Use HTTP‑only cookies / BFF; keep tokens server‑side |
| Unvalidated JWTs at API | Accepts forged/foreign tokens | Verify signature, issuer (iss), audience (aud), expiry |
| Over‑broad scopes | Excess privileges | Request minimal scopes per feature |
| Long‑lived access tokens | Bigger blast radius | Short TTL + refresh rotation + revocation |
| Replaying 0‑RTT requests | QUIC/TLS 1.3 nuance | Keep state‑changing endpoints idempotent or disallow 0‑RTT |
Quick checklist
- [ ] Use Authorization Code + PKCE (no implicit).
- [ ] Include
stateand validate it on return. - [ ] Keep tokens server‑side (BFF) or in secure stores; never expose refresh tokens to JS.
- [ ] Validate tokens at APIs (iss/aud/exp/sig).
- [ ] Request least‑privilege scopes; rotate refresh tokens.
- [ ] Use HTTPS end‑to‑end; set redirect URIs exactly (no wildcards).
One‑minute adoption plan
- Register your app with exact redirect URI(s) and required scopes.
- Implement Auth Code + PKCE with
state; use a well‑supported SDK. - Put a BFF in front of SPAs to keep tokens in HTTP‑only cookies.
- Validate tokens at the API; log
sub,scope,jtifor audits. - Turn on refresh rotation and revoke on suspicious use.