The OAuth Flows

OAuth isn't one procedure — it's a family of flows for different kinds of clients, and picking the wrong one is a security bug, not a style choice. The good news is that modern guidance has collapsed the confusion: for almost every case today, the answer is the authorization code flow with PKCE, and knowing why the alternatives were deprecated is knowing OAuth security.

The last post covered what OAuth is; this one covers how a client actually obtains a token — the grant types (flows). Historically there were several, and choosing among them confused everyone. Modern OAuth security guidance (embodied in OAuth 2.1) has simplified this dramatically by deprecating the dangerous ones, so this post explains the flows that matter, the one you should almost always use, and why the others fell away.

Why there are multiple flows

Different clients have different security capabilities, and a token-issuing procedure safe for one is unsafe for another. The key distinction:

Flows exist to handle these differently, plus the case of no user at all (service-to-service). The security question every flow answers is: how does the client prove it’s entitled to the token, and how do we keep the token (and the code that yields it) from being stolen in transit?

The authorization code flow

The authorization code flow is the primary, secure flow for user-authorization, and understanding it is understanding OAuth. Its defining feature is a two-step exchange that keeps the token off the browser’s address bar:

1. Client redirects the user to the Authorization Server (with client_id, scopes, redirect_uri)
2. User authenticates and consents at the Authorization Server
3. Auth Server redirects back to the client with a short-lived AUTHORIZATION CODE
4. Client exchanges the code (on the BACK CHANNEL) for tokens
   → sends the code + (for confidential clients) its secret to the token endpoint
5. Auth Server returns the ACCESS TOKEN (and refresh/ID tokens)

The cleverness is the authorization code as an intermediary. Rather than handing the token straight to the browser (where it’d be exposed in the URL, history, and logs), the auth server returns a short-lived, single-use code via the browser, which the client then exchanges for the actual token over a direct back-channel server call. The valuable token never travels through the browser’s front channel. For confidential clients, the code exchange also requires the client secret, proving the client’s identity.

PKCE: securing the flow for everyone

The authorization code flow has a weakness for public clients (SPAs, mobile): they can’t use a client secret to prove themselves at the exchange step, so an attacker who intercepts the authorization code (e.g. via a malicious app registering the same redirect) could exchange it for a token. PKCE (Proof Key for Code Exchange) closes this:

1. Client generates a random secret ("code verifier") and sends its hash
   ("code challenge") when starting the flow
2. ... authorization proceeds, client gets the code ...
3. At the code exchange, the client sends the original code verifier
4. Auth Server checks it hashes to the challenge from step 1 → only the
   client that started the flow can complete it

PKCE ties the code exchange to the specific client that initiated the flow, using a dynamically generated secret instead of a static one — so a stolen authorization code is useless to anyone else. Originally designed for mobile/SPA public clients, PKCE is now recommended for all clients, including confidential ones, as defense in depth. The modern default is therefore: authorization code flow with PKCE, for essentially every user-facing client. If you remember one thing about OAuth flows, remember that.

The client credentials flow

Not all OAuth is on behalf of a user. The client credentials flow handles machine-to-machine access — a backend service accessing an API as itself, with no user involved:

Client (a confidential service) → sends its client_id + client_secret → Token endpoint
Token endpoint → returns an access token (no user, no consent screen)

There’s no resource owner, no browser redirect, no consent — just a service authenticating with its credentials to get a token for its own access. Use this for backend jobs, service integrations, and daemons. It’s only for confidential clients (it relies on a secret), and it grants the service’s own permissions, not any user’s.

The flows that got deprecated (and why)

Modern OAuth security guidance (OAuth 2.1 consolidates it) removed two once-common flows, and knowing why teaches the security principles:

The through-line: both deprecated flows compromised the core protections (don’t expose tokens in the browser; never let the client touch the password), and modern OAuth removed them once the authorization code flow with PKCE could safely cover their use cases.

Choosing a flow

The decision is now refreshingly simple:

That’s essentially it: authorization code + PKCE for users, client credentials for machines. The historical complexity is gone; the modern guidance is clear. With a token obtained, the next question is what a token actually is and how to validate it — the subject of the next post.

Key takeaways

Further reading

Sources & References