OAuth 2 — The Authorization Framework

Authorization Code + PKCE, refresh tokens, scopes, the four roles. The protocol every modern API uses and gets wrong half the time.

Building Block Intermediate
9 min read
oauth authorization security tokens

What it is#

OAuth 2 is an authorization framework that lets a user grant a third-party application limited access to their resources on a service, without sharing the user’s password. It is defined by RFC 6749 (the framework) and a cluster of companion RFCs (PKCE in 7636, token introspection in 7662, JWT-shaped access tokens in 9068).

The framework is built around four roles:

  • Resource Owner — the user.
  • Resource Server — the API that holds the user’s data (Google Drive, GitHub, Spotify).
  • Client — the third-party app that wants access (a calendar tool wanting your Google Calendar, a CI tool wanting your GitHub repos).
  • Authorization Server — the service that issues access tokens (often the same company as the resource server; can be split).

OAuth 2 is not an authentication protocol. It tells you what the bearer of a token is allowed to do; it does not tell you who the bearer is. OpenID Connect is the identity layer built on top of OAuth 2 — same flow, plus an id_token that carries the user’s identity. The two are routinely conflated; the senior signal in an interview is to distinguish them cleanly.

When to use it#

Reach for OAuth 2 when:

  • A third party needs scoped access to your users’ resources. You’re not Google — you’re a third-party tool that wants to read a user’s Google Calendar. OAuth 2 is how Google hands you a token without you ever seeing the user’s password.
  • Your platform has third-party developers. Stripe Connect, GitHub Apps, Slack apps, Google Workspace add-ons, Zoom apps — all OAuth 2 flows.
  • First-party clients separated by trust boundary. Even a first-party mobile app talking to a first-party backend benefits from OAuth 2 — tokens that scope, expire, and revoke are better than long-lived sessions.

Avoid OAuth 2 when:

  • Server-to-server inside one trust boundary. mTLS or a static API key is simpler.
  • The auth model is “users log in directly to my product”. That’s session-based auth (cookies + a session store) or OpenID Connect (OAuth + identity). Raw OAuth 2 is the wrong abstraction.
  • You need single sign-on across many SaaS apps in an enterprise. SAML is the older, broader-deployed answer; OIDC is the modern one. Both layer identity onto an auth flow.

How it works#

OAuth 2 defines several grant types — different protocol flows for different client situations. Two matter today:

Authorization Code Grant + PKCE (the modern default)#

The flow for any client that runs in a browser or mobile app. PKCE (Proof Key for Code Exchange, RFC 7636) is mandatory — it’s what protects the flow against authorization-code interception.

User-Agent Client Authorization Resource
(browser) App Server Server
│ │ │ │
│ 1. clicks │ │ │
│ "Connect" │ │ │
│─────────────────►│ │ │
│ │ 2. computes random │ │
│ │ code_verifier; │ │
│ │ challenge = SHA256(verifier) │
│ │ │ │
│ 3. redirect to /authorize? │ │
│ response_type=code& │ │
│ client_id=...& │ │
│ redirect_uri=...& │ │
│ scope=calendar.read& │ │
│ code_challenge=<challenge>& │ │
│ code_challenge_method=S256 │ │
│◄─────────────────│ │ │
│ 4. user logs in, approves scopes │ │
│─────────────────────────────────────────►│ │
│ 5. redirect back with ?code=AUTH_CODE │ │
│◄─────────────────────────────────────────│ │
│─────────────────►│ │ │
│ │ 6. POST /token │ │
│ │ code=AUTH_CODE │ │
│ │ code_verifier=... │ │
│ │──────────────────────►│ │
│ │ 7. access_token │ │
│ │ + refresh_token │ │
│ │◄──────────────────────│ │
│ │ 8. GET /calendar │ │
│ │ Authorization: Bearer access_token │
│ │────────────────────────────────────────►│
│ │ 9. user's calendar data │
│ │◄────────────────────────────────────────│

The PKCE addition (code_verifier + code_challenge) prevents an attacker who intercepts the authorization code from exchanging it — without the original verifier, the token endpoint refuses.

Client Credentials Grant (machine-to-machine)#

For server-to-server calls with no user in the loop. The client authenticates with client_id + client_secret and gets back an access token directly.

Client Credentials request
POST /oauth/token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&
client_id=svc_invoicing&
client_secret=<secret>&
scope=invoice.read invoice.write

The other historical grant types (Implicit, Resource Owner Password Credentials) are deprecated — Implicit is unsafe without PKCE, and Password Grant defeats the entire point of OAuth (the user gives their password to the client). Don’t use them.

Acquiring a token — three-language example#

The Authorization Code exchange (step 6 → 7 above) in Python, Go, and Node. Realistic shape.

Exchange authorization code for token — Python
import requests
import hashlib
import secrets
import base64
# Generated earlier, before redirecting the user
code_verifier = secrets.token_urlsafe(64)
resp = requests.post(
"https://auth.example.com/oauth/token",
data={
"grant_type": "authorization_code",
"code": AUTH_CODE,
"redirect_uri": "https://app.example.com/callback",
"client_id": "client_abc",
"code_verifier": code_verifier,
},
timeout=5,
)
resp.raise_for_status()
tokens = resp.json()
# tokens = {"access_token": "...", "refresh_token": "...",
# "expires_in": 3600, "token_type": "Bearer", "scope": "calendar.read"}

Refresh tokens#

Access tokens are short-lived (commonly 1 hour). When they expire, the client uses the refresh token to mint a new one without bothering the user:

Refresh-token grant
POST /oauth/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&
refresh_token=<long-lived-token>&
client_id=client_abc

Refresh tokens should be rotated on use (the server returns a new refresh token and invalidates the old one) — this limits the window for a stolen refresh token to do damage.

Scopes#

Scopes are the OAuth 2 way to say “you can do X but not Y”. The client requests scopes at authorization time; the user approves them; the resource server enforces them per request. Naming convention is dotted (calendar.read, repo.write, admin:org); enforcement is per-endpoint.

A well-designed scope vocabulary is narrow (each scope grants one logical thing) and stable (renaming a scope breaks every existing token). Stripe Connect, Google Workspace, and GitHub Apps are studies in scope design.

Variants#

VariantMechanismWhen it fits
Authorization Code + PKCEThree-leg flow with PKCE; refresh tokens.Browser and mobile clients with a user. The modern default.
Client CredentialsTwo-leg flow; client_id + secret → access_token.Server-to-server with no user.
Device Authorization GrantDisplay a code on a TV, user enters it on their phone.TVs, CLI tools, IoT — devices without easy text input.
Token Exchange (RFC 8693)Swap one token for another (e.g. delegation).Microservice meshes; on-behalf-of patterns.

Trade-offs#

What OAuth 2 gives you:

  • Standardised delegation. Every modern API consumes it; libraries exist in every language.
  • Revocable, scoped, time-limited access. A leaked access token expires in an hour; a stolen refresh token can be revoked.
  • No passwords across boundaries. Third parties never see the user’s password.
  • Composes with OIDC. Adding identity is one extra response field (id_token).

What OAuth 2 costs you:

  • Protocol complexity. Four roles, multiple grant types, deprecated flows, PKCE, refresh rotation, scope design — it’s a lot.
  • Cookie-vs-token debates. First-party SPAs can use OAuth 2 or session cookies; the choice has security trade-offs neither side fully wins.
  • Token storage on the client. Where do you put the access token? localStorage (XSS-exposed), an HttpOnly cookie (CSRF concerns), an in-memory store (lost on reload)?
  • Discovery and metadata. Each provider publishes its own .well-known/oauth-authorization-server document; clients must read it.

Common pitfalls#

  • Treating access tokens as identity. They’re not — they say what the bearer can do, not who they are. Use OIDC id_token for identity.
  • Skipping PKCE on browser-based clients. Implicit grant is deprecated for a reason; PKCE on Authorization Code is mandatory now.
  • Long-lived access tokens. “Just give it 30 days” defeats the entire revocation model. Keep access tokens short; rely on refresh tokens for longevity.
  • Wildcard redirect_uri. Open redirect → account takeover. Always match the exact URL.
  • Storing tokens in localStorage without an XSS strategy. Any XSS exfiltrates the token. The HttpOnly-cookie pattern is safer (and CSRF can be mitigated with SameSite).
  • No refresh-token rotation. A stolen refresh token is permanent if you don’t rotate on use.
  • Confusing OAuth 2 with OpenID Connect in design documents. The auditor will pick on it.
Search ESC

Keyboard shortcuts

Shortcuts are disabled while typing in inputs.