OAuth 2.0 Flows in Production: Which One You Should Actually Use
Auth Code, Implicit, Client Credentials, Device Code, Resource Owner Password. Most engineers know the names. Few know which one fits which problem and why three of them are now considered insecure.
You are designing the auth for a new service. Your team lead asks "use OAuth." You go to the OAuth 2.0 spec and find five different flows: Authorization Code, Implicit, Resource Owner Password Credentials, Client Credentials, Device Code. Which one do you pick?
If you guess wrong, you either build something insecure (Implicit and Password flows have known weaknesses) or something painful (a CLI tool that opens a browser when it does not need to). The official RFCs do not tell you which flow to pick, only how each one works.
This is the production guide to OAuth 2.0 flows. What each one is, when it fits, when it does not, and which three should be off your list entirely in 2026.
The mental model: who is the client and what does it want?#
OAuth flows differ along two dimensions:
1. Who is the client? A web app running in a browser, a mobile app on a phone, a backend service, a CLI on a developer's laptop, a smart TV, a CI runner. Each has different capabilities for storing secrets and handling redirects.
2. Is there a human in the loop? Some flows require a user to log in and grant permission. Others run unattended (machine-to-machine). The presence or absence of a user changes everything.
If you can answer those two questions, the right flow is almost always determined.
The five flows#
1. Authorization Code (with PKCE): the default for everything with a user#
Used when: a user logs in to your app and grants access to their data.
Flow:
- App redirects user's browser to the auth server with
response_type=codeand a PKCEcode_challenge. - User authenticates and grants consent.
- Auth server redirects back with an
authorization_code. - App's backend exchanges the code (plus the PKCE
code_verifier) for anaccess_tokenandrefresh_token.
PKCE (Proof Key for Code Exchange, RFC 7636) is what makes this safe for public clients (single-page apps, mobile apps) that cannot keep a client secret. The verifier-challenge pair binds the redirect to the original client.
This is the right answer for: web apps, single-page apps, mobile apps, desktop apps, anything where a user is logging in. Use it with PKCE always, even for confidential clients with a backend secret.
Browser → /authorize?response_type=code&client_id=...&code_challenge=...&redirect_uri=...
→ user logs in
← redirect with ?code=AUTH_CODE
Backend → /token?grant_type=authorization_code&code=AUTH_CODE&code_verifier=...&client_id=...
← {"access_token": "...", "refresh_token": "...", "expires_in": 3600}
2. Client Credentials: the default for machine-to-machine#
Used when: one service authenticates to another with no user involved.
Flow:
- Service holds a
client_idandclient_secret. - Service POSTs to
/tokenwithgrant_type=client_credentials. - Auth server returns an
access_token.
That is it. No browser, no consent screen, no refresh token. The credentials are the auth.
This is the right answer for: backend service to backend service, cron jobs calling APIs, CI/CD pipelines deploying to cloud APIs, microservice-to-microservice within a system that does not have a real user.
Service → POST /token
grant_type=client_credentials
client_id=svc-payments
client_secret=...
← {"access_token": "eyJ...", "expires_in": 3600}
The access token is bound to the client (the service), not to a user. Permissions in the token (scopes, claims) describe what this client can do.
3. Device Code: for inputs without a browser#
Used when: the device cannot easily run a browser to handle the redirect (smart TV, CLI on a server, IoT device).
Flow:
- Device asks auth server for a
device_codeand auser_code. - Device displays the
user_codeand a URL to the user (e.g., "go to example.com/device and enter ABCD-1234"). - User opens the URL on their phone or laptop, logs in, enters the code.
- Device polls the auth server with the
device_codeuntil the user completes auth, then receives anaccess_token.
This is the right answer for: CLI tools (gh auth login, gcloud auth login, kubectl oidc-login), smart TV apps, devices with constrained input. The decoupling of the auth flow from the device's display is the whole point.
Device → POST /device_authorization
← {"device_code": "...", "user_code": "ABCD-1234",
"verification_uri": "https://example.com/device",
"interval": 5}
Device → "Open https://example.com/device and enter: ABCD-1234"
[user opens URL on phone, logs in, enters ABCD-1234]
Device → POST /token (every 5s)
grant_type=urn:ietf:params:oauth:grant-type:device_code
device_code=...
← {"access_token": "..."} (once user completes auth)
4. Implicit: DO NOT USE#
Used when: historically, single-page apps that could not securely exchange a code for a token (no backend).
Flow:
- App redirects to auth server with
response_type=token. - Auth server redirects back with the access token in the URL fragment.
Why this exists: when SPAs were new (circa 2012), they could not run server-side code to do the code-exchange step, and CORS was not flexible enough to make the /token endpoint callable from a browser. So the spec created a flow that returned the token directly in the redirect.
Why it is now bad: the access token ends up in browser history, in HTTP referer headers, in proxy logs, in browser extensions. There is no refresh token (no backend to securely store it). Token leakage is the default behavior.
The OAuth Security Best Current Practice (RFC 9700, 2024) explicitly deprecates Implicit. Modern SPAs should use Authorization Code with PKCE instead.
This is the right answer for: nothing in 2026. Migrate off it.
5. Resource Owner Password Credentials: DO NOT USE#
Used when: historically, an "trusted" first-party app that asked the user for their password directly and exchanged it for a token.
Flow:
- User gives the app their username and password.
- App POSTs to
/tokenwithgrant_type=password, the user's credentials, andclient_id. - Auth server returns an
access_token.
Why this exists: it was meant for migration scenarios where an existing app already collected passwords and wanted to transition to OAuth without changing the user experience.
Why it is now bad: it teaches users to enter their password into apps instead of into the auth server. It bypasses MFA. It bypasses consent screens. The whole point of OAuth was to avoid this pattern.
RFC 9700 deprecates Password too.
This is the right answer for: nothing in 2026. Migrate off it.
What about Refresh Tokens?#
Refresh tokens are not a flow of their own; they are a follow-up grant type used after Authorization Code (and historically after Password).
App → POST /token
grant_type=refresh_token
refresh_token=eyJ...
client_id=...
← {"access_token": "newJWT", "refresh_token": "newRefresh", "expires_in": 3600}
The point: access tokens should be short-lived (15-60 minutes) so a stolen one quickly expires. Refresh tokens are longer-lived (days to weeks) and used to get fresh access tokens without re-prompting the user.
Two non-obvious rules:
Rotate refresh tokens. Every refresh-token use returns a new refresh token. The old one is invalidated. This catches refresh token theft: if both the legitimate app and an attacker try to use the same refresh token, one of them gets rejected, alerting the auth server to revoke everything.
Bind refresh tokens to client. A refresh token issued to client A should not be usable by client B. Public clients (mobile apps, SPAs) should additionally use DPoP or mTLS for sender-constrained tokens.
OpenID Connect: OAuth + identity#
OAuth 2.0 by itself is for authorization (giving an app permission to call APIs as you). It does not standardize authentication (proving who the user is). OpenID Connect (OIDC) is the layer on top that adds an id_token to the response, which is a signed JWT containing the user's identity claims.
If you want to log a user in with Google or Okta and know who they are, you want OIDC, not raw OAuth 2.0. The flow is the same Authorization Code with PKCE, but you ask for the openid scope and get an id_token back alongside the access_token.
... same Authorization Code flow ...
← {
"access_token": "...",
"refresh_token": "...",
"id_token": "eyJ.<header>.<claims>.<sig>",
"expires_in": 3600
}
The id_token is for your app to consume (verify signature, extract sub, email, etc.). The access_token is for calling APIs.
Almost every "Sign in with X" flow you have ever used is OIDC. K8s API server's --oidc-issuer-url config integrates with this.
Three production patterns#
Three patterns from real production systems.
Pattern 1: Web app + microservices#
A user logs into a web app, which talks to multiple backend services on the user's behalf.
- Web app uses Authorization Code with PKCE to log the user in via the auth server. Receives
access_token(for the web app's own API calls) andid_token(to know who the user is). - Web app's backend calls downstream services using the
access_token. Each service validates the token (signature, expiry, audience). - Service-to-service calls (no user) use Client Credentials: each service has its own client and asks the auth server for a service token.
The key separation: tokens with user identity are obtained via Authorization Code; tokens for service-to-service work are obtained via Client Credentials. Mixing the two leads to confusion (services that operate on behalf of users while also doing background jobs need to handle both).
Pattern 2: CLI tool#
A developer runs mycorp-cli login on their laptop.
- CLI uses Device Code flow: opens the user's browser to a
verification_uri, displays auser_code. User authenticates in browser. CLI polls until done. - Stores the resulting tokens (access and refresh) in the user's keyring (macOS Keychain, GNOME Keyring, Windows Credential Vault).
- Later commands use the access token until it expires; on expiry, use the refresh token to get a new one.
This is exactly what gh auth login, gcloud auth login, and kubectl oidc-login do. Notably, none of them use Authorization Code with a localhost redirect (the older alternative for CLIs); Device Code is more reliable across platforms because it doesn't require the CLI to bind a port.
Pattern 3: CI/CD pipeline deploying to cloud#
A GitHub Actions workflow needs to deploy to AWS.
- Workflow uses OIDC federation (which is built on OAuth 2.0 + JWT). GitHub mints an OIDC token signed by GitHub for this specific workflow run.
- Workflow exchanges the OIDC token at AWS STS via
AssumeRoleWithWebIdentity. STS validates the token signature against the GitHub OIDC issuer and returns short-lived AWS credentials. - No long-lived AWS access keys stored anywhere. The OIDC token is workflow-scoped and expires when the workflow ends.
This pattern (variously called workload identity federation, federated identity, or OIDC trust) is the modern replacement for storing cloud credentials as CI secrets. Available on GitHub Actions, GitLab CI, CircleCI, Buildkite, and most cloud providers.
Common production mistakes#
1. Implicit flow in 2026. If your SPA or mobile app still uses Implicit, migrate. Auth Code with PKCE is straightforward to retrofit.
2. Long-lived access tokens. Access tokens valid for hours or days are stolen-token landmines. 15-60 minutes is the right range, with refresh tokens for the longer-lived persistence.
3. Refresh tokens not rotated. A stolen refresh token works forever if you don't rotate. Always rotate-on-use.
4. Tokens in URL parameters. Putting a token in a query string ends up in browser history, server logs, proxy logs. Only POST bodies and Authorization headers.
5. Resource Owner Password for "trusted internal apps". Even for first-party apps, this bypasses MFA and trains users to enter passwords into apps. Use Auth Code with PKCE.
6. Client Credentials for user-context calls. A service token authenticates the service, not any user. Never use it to call APIs that need user context (the API has no idea which user is making the request).
7. No audience validation. A token issued for service A is not for service B, even if both trust the same auth server. Check the aud claim in the JWT.
8. Skipping signature verification. Validating only that the token is well-formed JSON is not validation. Verify the signature against the auth server's published JWKS.
9. Accepting tokens from any issuer. A JWT from https://attacker.example.com looks structurally identical to one from your real auth server. Pin the issuer.
10. Client secrets in the SPA bundle. The SPA runs in the user's browser; nothing in it is secret. PKCE replaces the need for a client secret in public clients. If you find yourself shipping a "secret" to the browser, you have the wrong flow.
Quick reference: which flow when#
Use Authorization Code + PKCE when:
- A human user is involved
- Web app, SPA, mobile, desktop
- Always with PKCE, even for confidential clients
Use Client Credentials when:
- No user involved
- Service-to-service auth
- Backend has a real secret store
Use Device Code when:
- User involved but device cannot easily handle a browser redirect
- CLI tools
- Smart TVs, IoT devices
Do not use Implicit:
- Use Auth Code + PKCE in browser-based apps instead
Do not use Resource Owner Password:
- Use Auth Code + PKCE always
For CI/CD to cloud:
- OIDC federation with the cloud's STS-equivalent
- Not "OAuth flow" per se, but the underlying primitive
The mental model#
OAuth's complexity is not the spec; it is that one spec has to cover human-in-the-loop and machine-to-machine, web and mobile and CLI, public and confidential clients. The flows are answers to "given those constraints, what is the safest token-issuance ceremony."
In 2026, you really only need three: Authorization Code with PKCE for anything with a user, Client Credentials for service-to-service, Device Code for CLIs and headless devices. The other two are deprecated and the modern best practices documents say so explicitly.
If you can name what your client is and whether a user is in the loop, the right flow follows.
OAuth and OIDC are foundational to the auth chapter of the Kubernetes Security course, where we cover how K8s integrates with external IdPs, how SA tokens use the same JWT primitives, and how OIDC federation removes long-lived credentials from CI/CD entirely. Pair with the SSL/TLS Certificate Management course for the cryptography that makes JWTs and mTLS work.