blog/security/oauth2-flows-explained
Security & Authentication

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.

ยท12 min read

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

FlowClient typeHas client secret?User involved?Best for
Authorization CodeServer-side web appYes (stored on server)YesTraditional web apps with a backend
Authorization Code + PKCESPA, mobile, desktopNo (public client)YesAny app where the client can't store a secret
Client CredentialsBackend serviceYes (server-to-server)NoMachine-to-machine, microservices, cron jobs
Device AuthorizationSmart TV, CLI, IoTNo (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.

OAuth2 Flow Simulator
Use case: Server-side web apps
Actors: Browser โ†’ Your Server โ†’ Auth Server โ†’ API
1. User initiates login
2. Redirect to auth server
3. User authenticates
4. Auth code returned
5. Exchange code for token
6. Tokens received
7. Access API
Flow log
Select a flow and click Run to see it in action...

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.

Authorization Code Flow
login clickredirectcodecodecode + secrettokensBearer tokenUserclicks LoginBrowserredirect chainAuth ServerGoogle, Auth0...Your Serverexchanges codeProtected APIvalidates token

How it works

  1. User clicks "Login." Browser redirects to the authorization server with response_type=code, client_id, redirect_uri, and scope.
  2. User authenticates on the auth server's login page (not yours). After consent, the auth server redirects back to your redirect_uri with an authorization code in the query string.
  3. Your server exchanges the code. It sends the code, client_id, and client_secret directly to the auth server's token endpoint (server-to-server -- the browser never sees the secret).
  4. 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.

PKCE: Proof Key for Code Exchange
Generate
code_verifier (random)
hash
SHA256
code_challenge
Auth Request
sends challenge
Token Exchange
sends verifier

How PKCE works

  1. Client generates a random code_verifier (43-128 characters).
  2. Client computes code_challenge = SHA256(code_verifier) and sends it with the initial authorization request.
  3. Auth server stores the challenge associated with this authorization session.
  4. When exchanging the code, the client sends the original code_verifier (not the challenge).
  5. 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
๐Ÿ“Œ PKCE is now required for all public clients

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.

Client Credentials: Machine-to-Machine
Your Service
backend process
client_id + secret
Auth Server
validates credentials
Protected API
verifies token

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.

Device Flow: Two-Screen Authentication
Device
displays code
request code
Auth Server
generates code
User's Phone
enters code
Device Polls
token received!

How it works

  1. Device requests a device code from the auth server.
  2. Auth server returns a device_code (for polling), a user_code (short, human-readable), and a verification_uri.
  3. Device displays: "Go to auth.example.com/device and enter code: WDJB-MJHT".
  4. User opens their phone, navigates to the URL, enters the code, and authenticates.
  5. 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?

OAuth2 Flow Decision Tree

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.

TokenLifetimeSent to API?Purpose
Access token15 min -- 1 hourYes (every request)Authorize API requests
Refresh tokenDays -- monthsNo (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:

ChangeOAuth 2.0OAuth 2.1
Implicit flowAllowedRemoved entirely
PKCEOptionalRequired for all authorization code grants
Resource Owner Password CredentialsAllowedRemoved
Redirect URI matchingPartial match OKExact match required
Refresh token rotationRecommendedRequired 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.