OAuth 2.0: Choosing the Right Flow for Your App
OAuth 2.0 isn’t a single mechanism — it’s a toolbox of flows, each designed for different application types and security needs. The right choice depends on where your app runs and what secrets it can safely store.
Here’s a clear, practical guide to the four most common flows you’ll encounter:
Authorization Code with PKCE, JWT Bearer, Device Authorization, and Token Refresh.
1) Authorization Code + PKCE (for browsers, SPAs, and mobile apps)
Concept:
In this flow, the user signs in at the Authorization Server. Your application — which acts as a public client (no client secret) — generates a code_challenge derived from a random code_verifier.
After the user approves access and your app receives an authorization code, it exchanges that code for tokens by providing the original code_verifier.
This PKCE process prevents code interception or replay attacks.
When to use:
Single Page Applications (SPAs), mobile apps, and desktop apps that cannot securely store client secrets.
Pros:
✅ Modern standard for public clients.
✅ Protects against code interception.
Trade-offs:
⚙️ Requires redirect handling and proper state/nonce validation.
Code Example — Generate PKCE (Node.js)
import crypto from "crypto";
const codeVerifier = crypto.randomBytes(32).toString("base64url");
const codeChallenge = crypto.createHash("sha256").update(codeVerifier).digest("base64url");
// Save codeVerifier in user session; send codeChallenge to /authorize
Authorization URL (User Redirect):
GET https://auth.example.com/authorize? response_type=code& client_id=YOUR_CLIENT_ID& redirect_uri=https%3A%2F%2Fapp.example.com%2Fcb& scope=openid%20profile%20email& state=xyz123& code_challenge=<codeChallenge>& code_challenge_method=S256
Exchange Authorization Code (No Client Secret):
curl -X POST https://auth.example.com/token \ -d grant_type=authorization_code \ -d code=<AUTH_CODE> \ -d client_id=YOUR_CLIENT_ID \ -d redirect_uri=https://app.example.com/cb \ -d code_verifier=<codeVerifier>
2) JWT Bearer Flow (Server-to-Server, No User Interaction)
Concept:
This flow is meant for systems communicating directly — no user or browser involved.
Your backend creates a short-lived, signed JWT assertion and exchanges it for an access token. It’s ideal for backend integrations or scheduled jobs.
When to use:
Microservices, automated background jobs, or partner API integrations.
Pros:
✅ No interactive login required.
✅ Strong key-based authentication.
Trade-offs:
⚙️ Requires secure key management and rotation.
Code Example — Build & Exchange (Node.js)
import jwt from "jsonwebtoken";
import fetch from "node-fetch";
const now = Math.floor(Date.now()/1000);
const assertion = jwt.sign({
iss: "YOUR_CLIENT_ID",
sub: "service@yourdomain",
aud: "https://auth.example.com/token",
iat: now,
exp: now + 300,
scope: "orders:write users:read"
}, process.env.PRIVATE_KEY_PEM, { algorithm: "RS256", keyid: "kid-123" });
const res = await fetch("https://auth.example.com/token", {
method: "POST",
headers: { "Content-Type":"application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion
})
});
const tokens = await res.json();
3) Device Authorization Flow (for TVs, Consoles, and CLI Apps)
Concept:
This flow is perfect for devices that don’t have a built-in browser or keyboard.
The device displays a user code and a URL, and the user completes authentication on another device (like a phone or laptop).
Meanwhile, the original device keeps polling the token endpoint until authorization is complete.
When to use:
Smart TVs, kiosks, terminals, or command-line tools.
Pros:
✅ Works even without a browser or keyboard.
Trade-offs:
⚙️ Requires polling and a second device for approval.
Code Example — Request Device Code
curl -X POST https://auth.example.com/device_authorization \ -d client_id=YOUR_CLIENT_ID \ -d scope="openid profile offline_access" # -> device_code, user_code, verification_uri, interval
Poll for Token (Respect Interval):
curl -X POST https://auth.example.com/token \ -d grant_type=urn:ietf:params:oauth:grant-type:device_code \ -d device_code=<DEVICE_CODE> \ -d client_id=YOUR_CLIENT_ID # Handle: authorization_pending, slow_down, expired_token
4) Token Refresh (Keeping Sessions Alive Smoothly)
Concept:
The refresh token allows your app to request a new access token without forcing the user to log in again.
This is typically obtained from the Authorization Code + PKCE or Device Flow, provided the scope includes offline_access (or its equivalent).
When to use:
Whenever your app needs to maintain long-lived sessions without constant user reauthentication.
Pros:
✅ Short-lived access tokens combined with seamless user experience.
Trade-offs:
⚙️ Requires secure storage and rotation of refresh tokens.
Code Example — Refresh Tokens
curl -X POST https://auth.example.com/token \ -d grant_type=refresh_token \ -d client_id=YOUR_CLIENT_ID \ -d refresh_token=<REFRESH_TOKEN>
Minimal Rotation Logic (JavaScript):
async function refresh(rt) {
const res = await fetch("https://auth.example.com/token", {
method: "POST",
headers: {"Content-Type":"application/x-www-form-urlencoded"},
body: new URLSearchParams({ grant_type:"refresh_token", client_id:"YOUR_CLIENT_ID", refresh_token: rt })
});
const json = await res.json();
// Persist new refresh_token if returned
return json;
}
Short Real-World Scenario: Mobile App + Backend + TV
-
Mobile App (SPA or Native):
Uses Authorization Code + PKCE to getaccess_tokenandrefresh_token. Refreshes silently in the background. -
Backend Service:
Uses JWT Bearer Flow to call external APIs every hour — no user interaction needed. -
Smart TV App:
Uses Device Flow — the user approves on their phone, and the app securely stores a refresh token for future renewals.
Tiny Expiry Helper
const isExpired = (claims) => (claims.exp || 0) - 60 <= Math.floor(Date.now()/1000);
Final Thoughts
Each OAuth 2.0 flow exists for a reason — and the right one depends on how your app runs and what level of security it can enforce.
-
? Auth Code + PKCE: For SPAs and mobile sign-ins.
-
? JWT Bearer: For server-to-server communication.
-
? Device Flow: For devices with limited input.
-
? Token Refresh: To keep user sessions alive securely.
Implement them with the right scopes, strict redirect URIs, PKCE, and regular key rotation — and your authentication setup becomes invisible, reliable, and rock-solid.

