If you support or implement OAuth 2.0 / OpenID Connect (OIDC) authorization servers, you've probably needed to quickly recall which flow uses a client secret, which one is safe for a browser app, or why the implicit grant keeps getting flagged in security reviews. This guide is a single-page reference for all of that.
The Decision Flowchart
Before diving into each flow, here's the quick decision tree:
Is a user involved?
├── NO → Client Credentials Grant
└── YES
├── Does the device have a browser and keyboard?
│ ├── NO → Device Authorization Grant (Device Code Flow)
│ └── YES
│ ├── Need the user's identity (authentication)?
│ │ ├── NO → Authorization Code + PKCE (OAuth 2.0)
│ │ └── YES → Authorization Code + PKCE (OIDC, scope=openid)
│ └── Can the client securely store a secret?
│ ├── YES → Confidential client: Auth Code + PKCE + client_secret
│ └── NO → Public client: Auth Code + PKCE (no client_secret)
Quick Comparison Matrix
| Flow | Client Type | Requires Client Secret? | Returns ID Token? | Returns Refresh Token? | Recommended? |
|---|---|---|---|---|---|
| Authorization Code | Server-side web app | Yes | No | Yes | Yes |
| Auth Code + PKCE | SPA, mobile, desktop, server | Optional | No | Yes | Yes — the default choice |
| Implicit | SPA (legacy) | No | No | No | Deprecated |
| Client Credentials | M2M, backend services | Yes | No | No | Yes (M2M only) |
| ROPC (Password) | Legacy first-party | Optional | No | Yes | Deprecated |
| Device Code | IoT, smart TV, CLI | No (public) or Yes | No | Yes | Yes (input-constrained) |
| Refresh Token | Any (after initial grant) | Yes (confidential) | No | Yes (rotation) | Yes |
| OIDC Auth Code | Server, SPA, mobile (+ PKCE) | Optional | Yes | Yes | Yes — default for OIDC |
| OIDC Implicit | SPA (legacy) | No | Yes | No | Deprecated |
| OIDC Hybrid | Server-side web apps | Yes | Yes | Yes | Legacy |
OAuth 2.0 Grant Types in Detail
1. Authorization Code Grant
Status: Recommended | RFC: 6749 §4.1 | Best for: Server-side web apps with a secure backend
Credentials involved: client_id, client_secret, authorization code (short-lived), access token, optional refresh token.
How it works
- Client redirects the browser to
/authorizewithresponse_type=code,client_id,redirect_uri,scope, andstate. - User authenticates and consents.
- Authorization server redirects back with an authorization code and
state. - Client validates
state. - Client makes a back-channel POST to
/tokenwith the code,client_id,client_secret, andredirect_uri. - Authorization server returns an access token (and optionally a refresh token).
Key security notes: The access token is never exposed to the browser. The state parameter prevents CSRF. The client secret must be stored server-side only. Requires HTTPS everywhere.
2. Authorization Code + PKCE
Status: Required for public clients; required for ALL clients under OAuth 2.1 | RFCs: 7636, OAuth 2.1, RFC 9700 | Best for: SPAs, native mobile apps, desktop apps, and (under 2.1) all clients
Additional credentials: code_verifier (random 43-128 char string), code_challenge (Base64url SHA-256 of the verifier), code_challenge_method=S256.
How it works
- Client generates a cryptographically random
code_verifier. - Client computes
code_challenge = BASE64URL(SHA256(code_verifier)). - Client redirects to
/authorizewith all standard params pluscode_challengeandcode_challenge_method=S256. - User authenticates and consents. Authorization server stores the challenge with the code.
- Client sends the code plus the original
code_verifierto/token. - Authorization server hashes the verifier, compares to the stored challenge. If matched, tokens are issued.
Key security notes: Even if an attacker intercepts the authorization code, they can't exchange it without the code_verifier (which never leaves the client). Always use S256, never plain in production.
3. Implicit Grant
Status: DEPRECATED — Removed from OAuth 2.1. RFC 9700 recommends against it.
Was designed for browser SPAs that couldn't make back-channel requests. Returns the access token directly in the URL fragment (#access_token=...). No authorization code, no refresh token, no client secret.
Why it's deprecated: Tokens leak through browser history, referrer headers, and extensions. No integrity verification. No refresh tokens. Authorization Code + PKCE does everything better.
4. Client Credentials Grant
Status: Recommended for M2M | RFC: 6749 §4.4 | Best for: Server-to-server, daemons, microservices, CI/CD pipelines
Credentials: client_id + client_secret (or private_key_jwt, mTLS). No user involved.
How it works
- Client POSTs to
/tokenwithgrant_type=client_credentials, credentials, andscope. - Authorization server validates and returns an access token.
Key security notes: The secret must live in a secrets manager or environment variable — never in source code. The token represents the application, not a user. Use least-privilege scopes. Consider private_key_jwt or mTLS for stronger authentication.
5. Resource Owner Password Credentials (ROPC)
Status: DEPRECATED — Removed from OAuth 2.1.
The user gives their username and password directly to the client, which forwards them to /token. This fundamentally breaks the OAuth model (delegated authorization without sharing credentials). It can't support MFA or federated identity. Use Authorization Code + PKCE instead, even for first-party apps.
6. Device Authorization Grant (Device Code Flow)
Status: Recommended for input-constrained devices | RFC: 8628 | Best for: Smart TVs, IoT devices, CLI tools, game consoles
Credentials: client_id, device_code, user_code, verification_uri.
How it works
- Device requests a device code from the authorization server.
- Server returns a
device_code,user_code,verification_uri, and pollinginterval. - Device displays: "Go to https://example.com/device and enter code: ABCD-1234"
- User opens a browser on a secondary device (phone/laptop), navigates to the URI, enters the code, authenticates, and consents.
- Meanwhile, the device polls
/tokenwith thedevice_codeat the specified interval. - Once the user completes authorization, polling returns an access token + refresh token.
Key security notes: The user code needs enough entropy to resist brute-force but be easy to type. Respect the polling interval. Watch for session fixation (an attacker could trick a user into authorizing a pre-generated device code).
7. Refresh Token Grant
Status: Recommended | RFC: 6749 §6 | Best for: Any app that needs long-lived access without re-authentication
- Client detects the access token is expired (or about to expire).
- Client POSTs to
/tokenwithgrant_type=refresh_token, the refresh token, and client credentials (if confidential). - Authorization server validates and returns a new access token (and optionally a new refresh token via rotation).
Key security notes: Refresh token rotation is strongly recommended (RFC 9700) — each use invalidates the old token and issues a new one, allowing detection of stolen tokens. For public clients, rotation is essential. Set finite lifetimes. Consider sender-constraining with DPoP or mTLS.
OpenID Connect (OIDC) Flows
OIDC adds an identity layer on top of OAuth 2.0. The key addition is the ID Token — a JWT containing claims about the authenticated user. OIDC flows mirror OAuth 2.0 flows with these additions:
- The
scopeincludesopenid(plus optionallyprofile,email,address,phone). - A
nonceparameter prevents ID token replay attacks. - The token response includes an
id_token(JWT). - A UserInfo endpoint is available for additional claims.
OIDC Authorization Code Flow
Status: Recommended | Spec: OIDC Core 1.0 §3.1
Identical to the OAuth 2.0 Authorization Code flow (with or without PKCE), but includes scope=openid, a nonce parameter, and returns an id_token alongside the access token. The client must validate the ID token: check iss, aud, exp, iat, nonce, and the signature (using the server's JWKS keys).
OIDC Implicit Flow
Status: DEPRECATED
Returns the ID token (and optionally an access token) directly in the URL fragment via response_type=id_token or response_type=id_token token. Same vulnerabilities as OAuth 2.0 Implicit. Use Auth Code + PKCE instead.
OIDC Hybrid Flow
Status: Legacy | Spec: OIDC Core 1.0 §3.3
Combines front-channel and back-channel token delivery using response types like code id_token, code token, or code id_token token. The ID token delivered on the front channel includes a c_hash (code hash) for integrity binding. Was useful when apps needed immediate identity info before completing the back-channel exchange, but Auth Code + PKCE is now fast enough to make this unnecessary.
Security Best Practices
Token Storage by Client Type
| Client Type | Access Token | Refresh Token |
|---|---|---|
| Server-side web app | Server-side session or encrypted cookie | Server-side session or encrypted DB |
| SPA (browser) | In-memory only (NOT localStorage) | BFF pattern or secure httpOnly cookie; use rotation |
| Native mobile app | OS secure storage (Keychain / Keystore) | OS secure storage |
| M2M / daemon | Env var or secrets manager | N/A (just request a new token) |
Never store tokens in localStorage or sessionStorage — any JavaScript on the page can read them (XSS risk).
Recommended Token Lifetimes
| Token Type | Recommended Lifetime | Notes |
|---|---|---|
| Access token | 5–15 minutes | Short-lived limits theft damage |
| Refresh token | Hours to days | Never indefinite; use rotation |
| Authorization code | 30 sec – 10 min | Single-use; invalidate after exchange |
| ID token | Minutes | Authentication moment only; not a session token |
| Device code | 5–15 minutes | Enough for the user to complete on another device |
Common Vulnerabilities & Mitigations
| Vulnerability | Mitigation |
|---|---|
| Authorization code interception | Use PKCE |
| Token leakage via URL fragment | Don't use Implicit; use Auth Code + PKCE |
| Open redirectors | Exact redirect URI matching; pre-register all URIs |
| CSRF on callback | state parameter + PKCE |
| Token injection / substitution | Sender-constrained tokens (DPoP / mTLS); audience restriction |
| Refresh token theft | Rotation, sender-constraining, secure storage |
| ID token replay | nonce validation; aud validation |
| Mix-up attacks (multiple IdPs) | Issuer identification (iss in auth response, RFC 9207) |
| JWT confusion (access token used as ID token) | Validate typ header and aud claim |
| Token in query string | Use Authorization header only (OAuth 2.1 prohibits query strings) |
Redirect URI Rules
- OAuth 2.1 requires exact string matching — no wildcards, no partial matches.
- Pre-register all redirect URIs with the authorization server.
- Never use open redirectors as redirect URIs.
- Native apps: use custom URL schemes, claimed HTTPS links (Universal Links / App Links), or the loopback interface (
http://127.0.0.1:{port}).
Sender-Constrained Tokens
DPoP (RFC 9449): Binds the access token to a client-held cryptographic key. Each API request includes a signed DPoP proof in the DPoP header. Stolen tokens are useless without the private key. Particularly valuable for SPAs using non-extractable keys via Web Crypto API.
mTLS (RFC 8705): Binds the token to the client's TLS certificate. More common in enterprise and B2B scenarios.
OAuth 2.1: What Changed
OAuth 2.1 (draft-ietf-oauth-v2-1) consolidates OAuth 2.0 and its security extensions into a single modernized spec. Key changes:
- PKCE is mandatory for all authorization code grants (public and confidential).
- Implicit grant is removed.
- ROPC grant is removed.
- Exact redirect URI matching is required.
- Bearer tokens in query strings are prohibited — use the
Authorizationheader. - Refresh token rotation is recommended for public clients.
- Sender-constrained tokens (DPoP, mTLS) are recommended.
RFC 9700 (published January 2025) serves as the authoritative Security BCP document, encoding many of these as MUST/SHOULD requirements.
References
- RFC 6749 — The OAuth 2.0 Authorization Framework
- RFC 7636 — Proof Key for Code Exchange (PKCE)
- RFC 8628 — OAuth 2.0 Device Authorization Grant
- RFC 8705 — OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens
- RFC 9207 — OAuth 2.0 Authorization Server Issuer Identification
- RFC 9449 — OAuth 2.0 Demonstrating Proof of Possession (DPoP)
- RFC 9700 — Best Current Practice for OAuth 2.0 Security
- OpenID Connect Core 1.0
- OAuth 2.1 Draft (draft-ietf-oauth-v2-1)