OAuth2 Flows Explained: Which Flow for Which Use Case
OAuth2 has four grant types and choosing the wrong one creates real security holes. Here's how each flow works, which one fits your application, and why the implicit flow is dead.
OAuth2 Is Not One Thing
OAuth2 is a framework, not a single protocol. It defines multiple ways for an application to get an access token, each designed for a different trust model and deployment context.
The problem? Developers pick the flow based on which tutorial they find first. A mobile app uses the authorization code flow without PKCE. A server-side app stores tokens in localStorage. A CLI tool rolls its own login page.
Each of these is a security mistake. The right flow depends on one question: where does your client run, and can it keep a secret?
The Four OAuth2 Flows
| Flow | Client type | Has client secret? | User involved? | Best for |
|---|---|---|---|---|
| Authorization Code | Server-side web app | Yes (stored on server) | Yes | Traditional web apps with a backend |
| Authorization Code + PKCE | SPA, mobile, desktop | No (public client) | Yes | Any app where the client can't store a secret |
| Client Credentials | Backend service | Yes (server-to-server) | No | Machine-to-machine, microservices, cron jobs |
| Device Authorization | Smart TV, CLI, IoT | No (limited input) | Yes (on separate device) | Devices without a browser or keyboard |
๐ด The Implicit Flow is dead
OAuth 2.0 originally defined an Implicit flow for SPAs that returned tokens directly in the URL fragment. OAuth 2.1 removes it entirely. It exposes tokens in browser history, referrer headers, and server logs. Use Authorization Code + PKCE instead -- always.
Try Each Flow
Select a flow below and run it to see every request and redirect in sequence. Watch how the security model changes based on the client type.
Authorization Code Flow
This is the standard flow for server-side web applications -- Rails, Django, Express, Laravel. Your server can store a client_secret safely because users never see your server-side code.
How it works
- User clicks "Login." Browser redirects to the authorization server with
response_type=code,client_id,redirect_uri, andscope. - User authenticates on the auth server's login page (not yours). After consent, the auth server redirects back to your
redirect_uriwith an authorization code in the query string. - Your server exchanges the code. It sends the code,
client_id, andclient_secretdirectly to the auth server's token endpoint (server-to-server -- the browser never sees the secret). - Auth server returns tokens. Access token for API calls, refresh token for getting new access tokens later.
Why the code exchange matters
The authorization code is short-lived and useless without the client_secret. Even if an attacker intercepts the code from the URL, they can't exchange it for tokens without knowing the secret. The secret never leaves your server.
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&client_id=your-app
&client_secret=your-server-side-secret
&redirect_uri=https://yourapp.com/callback
Authorization Code + PKCE
PKCE (Proof Key for Code Exchange, pronounced "pixy") solves a fundamental problem: SPAs and mobile apps can't store a client secret. Anyone can view JavaScript source code or decompile a mobile app.
Without PKCE, an attacker who intercepts the authorization code can exchange it for tokens (since there's no secret to validate). PKCE adds a cryptographic proof that the client requesting the token is the same client that started the flow.
How PKCE works
- Client generates a random
code_verifier(43-128 characters). - Client computes
code_challenge= SHA256(code_verifier) and sends it with the initial authorization request. - Auth server stores the challenge associated with this authorization session.
- When exchanging the code, the client sends the original
code_verifier(not the challenge). - Auth server hashes the verifier and compares it to the stored challenge. If they match, the token is issued.
An attacker who intercepts the authorization code doesn't have the code_verifier (it was generated in the client's memory and never transmitted until the token exchange). Without it, the code is worthless.
// Step 1: Generate PKCE values
const codeVerifier = generateRandomString(64);
const codeChallenge = base64url(sha256(codeVerifier));
// Step 2: Authorization request includes challenge
/authorize?response_type=code
&client_id=spa-app
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
// Step 3: Token exchange includes verifier
POST /token
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
OAuth 2.1 makes PKCE mandatory for all clients using the authorization code flow -- even server-side apps. It provides defense-in-depth against authorization code interception attacks regardless of whether the client has a secret.
Client Credentials Flow
This is the simplest flow because there's no user involved. One service authenticates to another using its own credentials. Think microservice-to-microservice calls, cron jobs hitting an API, or any backend process that acts on its own behalf.
One request. No redirects. No user interaction.
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=service-a
&client_secret=service-a-secret
&scope=read:data write:events
When to use it
- Microservice A calls Microservice B's API
- A cron job syncs data with a third-party API
- Your CI pipeline deploys to a cloud provider
- Any automated process that needs API access without a human
When NOT to use it
Never use client credentials when a user should be involved. This flow has no concept of user identity -- the token represents the application, not a person. If you need to know which user is making the request, use one of the authorization code flows.
Device Authorization Flow
How does a smart TV with no keyboard let you log in? It can't render a secure login page. It can't type a password. But it can display a short code on screen and ask you to authenticate on your phone.
How it works
- Device requests a device code from the auth server.
- Auth server returns a
device_code(for polling), auser_code(short, human-readable), and averification_uri. - Device displays: "Go to auth.example.com/device and enter code: WDJB-MJHT".
- User opens their phone, navigates to the URL, enters the code, and authenticates.
- Device polls the token endpoint every few seconds with the
device_code. Once the user completes authentication, the poll returns tokens.
This is what you see when you sign into Netflix on a new TV, or when a CLI tool like the GitHub CLI asks you to authenticate.
Which Flow Should You Use?
What type of client is making the request?
Refresh Tokens and Token Rotation
Access tokens are short-lived (minutes to hours). When they expire, the client uses a refresh token to get a new access token without making the user log in again.
| Token | Lifetime | Sent to API? | Purpose |
|---|---|---|---|
| Access token | 15 min -- 1 hour | Yes (every request) | Authorize API requests |
| Refresh token | Days -- months | No (only to auth server) | Get new access tokens silently |
Refresh token rotation
For public clients (SPAs, mobile apps), best practice is refresh token rotation: every time a refresh token is used, the auth server issues a new refresh token and invalidates the old one.
If an attacker steals a refresh token and the legitimate client also tries to use it, the auth server detects the reuse and revokes the entire token family. This limits the damage window of a stolen token.
// Client sends expired access token, gets 401
// Client uses refresh token to get new tokens:
POST /oauth/token
grant_type=refresh_token
&refresh_token=old-refresh-token
&client_id=spa-app
// Response: NEW access token + NEW refresh token
// The old refresh token is now invalid
{
"access_token": "new-eyJ...",
"refresh_token": "new-dGhp...",
"expires_in": 3600
}
โ ๏ธ Never store tokens in localStorage
For browser apps, store tokens in memory (JavaScript variables). Use httpOnly cookies for refresh tokens if possible. localStorage is accessible to any script running on your page -- one XSS vulnerability and all tokens are exposed.
OAuth 2.1: What Changed
OAuth 2.1 isn't a new version -- it's a consolidation of OAuth 2.0 plus all the security best practice RFCs into one document. The key changes:
| Change | OAuth 2.0 | OAuth 2.1 |
|---|---|---|
| Implicit flow | Allowed | Removed entirely |
| PKCE | Optional | Required for all authorization code grants |
| Resource Owner Password Credentials | Allowed | Removed |
| Redirect URI matching | Partial match OK | Exact match required |
| Refresh token rotation | Recommended | Required for public clients |
If you're starting a new project, build to the OAuth 2.1 spec. If you have an existing OAuth 2.0 implementation, the migration is straightforward: add PKCE, remove implicit flow support, enforce exact redirect URI matching.
Common Mistakes
Storing client secrets in mobile apps or SPAs
Client secrets in public clients are not secrets. Anyone can extract them. Use PKCE instead.
Using the implicit flow in 2026
It returns tokens in the URL fragment. Browser history, referrer headers, and proxy logs all capture it. It's been deprecated since 2019 and removed in OAuth 2.1.
Not validating the state parameter
The state parameter prevents CSRF attacks on the redirect URI. Generate a random value, store it in the session, and verify it when the callback arrives. Without it, an attacker can initiate an OAuth flow and trick your server into associating their authorization code with a victim's session.
Ignoring token expiration
Don't set access tokens to expire in 30 days to avoid dealing with refresh tokens. Short-lived access tokens limit the damage window if a token is leaked. Handle the refresh flow properly.
Key Takeaways
The flow depends on the client type. Server-side apps use Authorization Code. SPAs and mobile apps use Authorization Code + PKCE. Machine-to-machine uses Client Credentials. Input-constrained devices use Device Authorization.
PKCE replaces client secrets for public clients. It proves the client that started the flow is the same one exchanging the code -- without storing a secret.
The Implicit flow is dead. OAuth 2.1 removes it. Use Authorization Code + PKCE for all browser and mobile apps.
Refresh token rotation limits stolen token damage. Each refresh token is single-use. Reuse detection triggers revocation of the entire token family.
Store tokens carefully. Memory for access tokens in SPAs. httpOnly cookies for refresh tokens when possible. Never localStorage.