OAuth Flows: When to Use What

OAuth isn't one flow, it's a small family of them, and picking the wrong one for your application type is one of the most common ways teams end up with a security gap they didn't know they'd introduced. The right flow depends almost entirely on one question: what kind of application is asking for ac

OAuth isn't one flow, it's a small family of them, and picking the wrong one for your application type is one of the most common ways teams end up with a security gap they didn't know they'd introduced. The right flow depends almost entirely on one question: what kind of application is asking for access, and how much can it be trusted to keep a secret?

Here's the practical breakdown — not the full spec, but the version that tells you which flow to reach for.

The core question: can this app keep a secret?

A server-side application — something running in your own infrastructure, where the code and its configuration never reach the end user's device — can hold a client secret safely. Nobody outside your infrastructure can extract it.

A mobile app, a single-page JavaScript app, or anything else whose code ships to a device you don't control cannot safely hold a secret. Decompile the app or open the browser dev tools, and any embedded secret is right there. These are called "public clients" for exactly this reason, and the entire design of modern OAuth flow selection hinges on treating public and confidential clients differently.

flowchart TD
    Q{"Can the app keep a secret?"}
    Q -->|"Yes - server-side, secret never exposed"| Conf["Confidential Client"]
    Q -->|"No - mobile app, SPA, code runs on user's device"| Pub["Public Client"]
    Conf --> ACC["Authorization Code + PKCE (recommended even here)"]
    Pub --> ACP["Authorization Code + PKCE (required)"]

Authorization Code + PKCE: the default answer for almost everything

If you're integrating a web app, a mobile app, or a single-page app with a login system, this is very likely the flow you want, full stop. The user is redirected to the authorization server, authenticates there directly (never handing credentials to your app), and your app receives a short-lived authorization code, which it then exchanges — using a PKCE verifier — for an access token and, if applicable, a refresh token.

sequenceDiagram
    participant U as User
    participant App as Client App
    participant AS as Authorization Server

    App->>App: Generate code_verifier, derive code_challenge
    App->>U: Redirect to AS with code_challenge
    U->>AS: Log in, approve access
    AS->>App: Redirect back with authorization code
    App->>AS: Exchange code + code_verifier for tokens
    AS->>App: Access token (+ refresh token)

PKCE was originally designed for public clients that couldn't hold a secret, but current best practice (per the OAuth 2.1 draft) recommends it for confidential clients too, since it adds meaningful protection against authorization code interception at essentially no cost. If you're not sure which flow to pick for a new integration, this is the one to default to.

Use it for: web apps with a backend, single-page apps, native mobile apps, basically any flow where a real human is present to log in interactively.

Client Credentials: when there's no user at all

Some integrations aren't acting on behalf of a person — a backend service calling another backend service, a scheduled job pulling data from an API, machine-to-machine communication where there's no one to redirect to a login screen. For these, the application authenticates directly using its own client ID and client secret, and receives a token representing the application's own identity, not a user's.

sequenceDiagram
    participant App as Service A
    participant AS as Authorization Server
    participant API as Service B

    App->>AS: client_id + client_secret
    AS->>App: Access token (app identity)
    App->>API: Request with access token
    API->>App: Response

Use it for: server-to-server integrations, scheduled jobs, backend automation — never for anything involving an actual end user, and never from a public client, since it requires a secret only a confidential client can hold.

Device Authorization Grant: when there's no good way to type

Smart TVs, streaming devices, and CLI tools share a problem: they either don't have a convenient keyboard, or the environment isn't well suited to typing a password securely. The device flow sidesteps this by displaying a short code and a URL, which the user opens on a different device — their phone or laptop — to complete the actual login. The TV or CLI tool polls the authorization server in the background until that approval completes.

sequenceDiagram
    participant Dev as TV / CLI
    participant AS as Authorization Server
    participant U as User's Phone

    Dev->>AS: Request device code
    AS->>Dev: device_code + user_code + verification URL
    Dev->>Dev: Display "Go to xyz.com/activate, enter ABCD-1234"
    U->>AS: Open URL on phone, enter code, log in, approve
    Dev->>AS: Poll for token
    AS->>Dev: Access token (once approved)

Use it for: smart TVs, streaming boxes, CLI tools, anything with limited or no text input where a second, more capable device is available to complete login.

Flows you'll still encounter but shouldn't choose for new work

Implicit flow returned access tokens directly in the browser's URL fragment, skipping the code-exchange step entirely — which sounded convenient and turned out to expose tokens to browser history, referrer headers, and any script running on the page. It's formally deprecated in OAuth 2.1. If you see it in an older integration, treat migrating off it as a real security improvement, not just a modernization task.

Resource Owner Password Credentials (ROPC) has the application collect the user's actual username and password directly and exchange them for a token — which defeats the entire premise of OAuth, since it requires the user to hand credentials to the third-party app after all. It exists mainly for legacy migration scenarios where there's genuinely no other option yet, and it should be treated as a temporary bridge, not a destination.

The decision, compressed

flowchart TD
    Start{"What's requesting access?"}
    Start -->|"A human, via a web/mobile/SPA app"| ACPK["Authorization Code + PKCE"]
    Start -->|"A backend service, no human present"| CC["Client Credentials"]
    Start -->|"A device with limited input (TV, CLI)"| Dev["Device Authorization Grant"]
    Start -->|"Legacy system migrating off ROPC"| Legacy["Plan migration to Auth Code + PKCE"]

If you remember nothing else: Authorization Code + PKCE covers the overwhelming majority of real applications, Client Credentials is for service-to-service calls with no user involved, and Device Flow is for the narrow case of input-constrained hardware. Everything else on this list is either deprecated or a stopgap — useful to recognize in an existing system, not something to reach for when building something new.