Choosing an OAuth Flow
|
This section documents OAuth 2.0 (RFC 6749) as amended by the OAuth 2.0 Security Best Current Practice (RFC 9700 / BCP 240), the OAuth 1.0 Protocol (RFC 5849) for historical context, and OpenID Connect Core 1.0, as published at the IETF Datatracker and the OpenID Foundation specifications — and, on the Spring pages, Spring Boot 4.1.x and Spring Security 7.1.x as published at the Spring Security reference documentation — which are the references these pages are written and verified against. OAuth 2.1 is still an Internet-Draft ( This content was generated with the assistance of AI and should be verified against those official specifications before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Every OAuth integration starts with the same question: which grant type does this client use? The answer
depends entirely on the client’s shape — can it keep a secret, does a human sit in front of it during login,
can it render a browser, can it even display a URL. This page is the entry point for the flow pages that follow:
it gives a decision tree keyed on client shape, a table of every grant_type value defined by RFC 6749 and its
extensions with its current status, the governing rule that a client must never see the resource owner’s
password, and — because this confuses newcomers and experienced engineers alike — a precise separation between
an OAuth grant type, an authentication method, and 2FA, which are three different things that this section
keeps coming back to.
Decision tree by client shape
Pick the row that matches how the client is deployed, not how it is written. A single-page application compiled to run in a browser is a public client regardless of the backend language it was built with; a batch job running inside a locked-down VPC with a provisioned secret is a confidential client even if it is a five-line script.
| Client shape | Recommended grant | Why |
|---|---|---|
Server-side web application (renders HTML, holds a secret on the server) |
Authorization code + PKCE |
Confidential client, can authenticate itself at the token endpoint, browser only ever sees the front channel |
Single-page application (SPA) running entirely in the browser |
Authorization code + PKCE, ideally behind a backend-for-frontend (BFF) |
Public client, cannot hold a secret; PKCE replaces client authentication as the proof of origin |
Native / mobile app (iOS, Android, desktop) |
Authorization code + PKCE via the system browser or an in-app browser tab |
Public client; see Native & Mobile Apps for why an embedded WebView is forbidden |
Input-constrained device (TV, console, set-top box, IoT with no keyboard) |
Device authorization grant |
The device cannot render a redirect URI or accept a callback; the user completes login on a second, capable device — see Device Authorization Grant |
CLI tool run interactively by a person |
Authorization code + PKCE (opens a local browser and a loopback redirect), or the device authorization grant when no local browser is available (SSH session, container shell) |
A human is present and can authenticate; the CLI itself must never see the password |
Daemon / background service acting as itself (no user) |
Client credentials |
Machine-to-machine, no resource owner in the loop — see Client Credentials & Client Authentication |
Service calling a downstream service on behalf of a user (microservice chain) |
Token exchange (RFC 8693) or a JWT bearer assertion grant (RFC 7523) |
The upstream identity must be carried or re-asserted, not silently dropped or silently forged — see Token Exchange & Assertion Grants |
First-party app owned by the same organization as the authorization server |
Authorization code + PKCE, same as any public/confidential client; a narrow first-party exception is still being drafted |
Resist the temptation to special-case "our own app" with the resource owner password grant — see |
The grant-type table
Every grant type OAuth 2.0 and its extensions define, with its exact grant_type value (where it uses one),
the defining specification, the client types it suits, and its status under the in-progress OAuth 2.1
consolidation (draft-ietf-oauth-v2-1-16, 3 September 2026). "Removed in the OAuth 2.1 draft" describes what
that Internet-Draft proposes — it is not yet a published standard, so existing deployments of a "legacy" grant
are not in violation of anything; they are simply building on a foundation the working group intends to retire.
| Grant | grant_type value |
Defining spec | Client types | Status |
|---|---|---|---|---|
Authorization code (+ PKCE) |
|
RFC 6749 §4.1 + RFC 7636 (PKCE) |
Confidential and public clients with a redirect-capable user agent |
Current — the default choice for any client a user interacts with; PKCE mandatory in OAuth 2.1 |
Client credentials |
|
RFC 6749 §4.4 |
Confidential clients, no user |
Current — see Client Credentials & Client Authentication |
Device authorization grant |
|
RFC 8628 |
Input-constrained devices |
Current — see Device Authorization Grant |
Refresh token |
|
RFC 6749 §6, hardened by RFC 9700 §4.14 |
Any client already holding a refresh token |
Current — rotation and sender-constraining required for public clients under RFC 9700; see Access & Refresh Tokens |
Token exchange |
|
RFC 8693 |
Services relaying or re-scoping a subject’s identity |
Current — see Token Exchange & Assertion Grants |
JWT bearer assertion |
|
RFC 7521 / RFC 7523 |
Confidential clients presenting a signed assertion instead of interactive login |
Current — also usable as a client authentication method, see Client Credentials & Client Authentication |
SAML bearer assertion |
|
RFC 7521 / RFC 7522 |
Enterprise clients bridging an existing SAML assertion |
Conditional — narrow use, mainly enterprise federation bridges; see Token Exchange & Assertion Grants |
CIBA (decoupled authentication) |
N/A (backchannel flow, not a token-endpoint |
OpenID Connect CIBA Core 1.0 |
Devices where the authenticating device and the consuming device differ and no redirect is used at all |
Conditional — OIDC-specific, out of scope narrowly on this page; see OpenID Connect |
Implicit |
|
RFC 6749 §4.2 |
Historically: browser-based apps with no backend |
Legacy, removed in the OAuth 2.1 draft — replaced by authorization code + PKCE; see Legacy Implicit & Password Grants |
Resource owner password credentials (ROPC) |
|
RFC 6749 §4.3 |
Historically: highly trusted first-party clients |
Legacy, removed in the OAuth 2.1 draft — replaced by authorization code + PKCE (or client credentials, for non-interactive callers); see Legacy Implicit & Password Grants |
|
"Removed in the OAuth 2.1 draft" is about the grant type itself, not about any particular authentication method a resource owner might use. The draft removes ROPC because the pattern of a client collecting a password and forwarding it is unsafe regardless of what that password is checked against — it says nothing about passwords, OTPs, or passkeys as authentication methods in general. See the distinction below. |
The governing rule: a client never sees the resource owner’s password
Strip away the acronyms and every current grant type in the table above enforces the same rule: the client application never receives the resource owner’s credentials. The client redirects the resource owner to the authorization server (authorization code), presents its own credentials as itself (client credentials), or relays a token issued elsewhere (token exchange, assertion grants) — in no current flow does the resource owner type a password into a screen the client controls.
This is precisely what the two legacy grants got wrong, and precisely why they are gone in the OAuth 2.1 draft:
-
The implicit grant did not involve the password directly, but it handed a bearer access token straight back in a browser redirect fragment with no client authentication and no refresh token — see Legacy Implicit & Password Grants for the full attack surface.
-
The resource owner password credentials grant asked the resource owner to type their password directly into the client’s own form, which the client then forwarded to the token endpoint verbatim. That single design choice defeats the entire point of OAuth — see the same page for the complete list of what it costs (no MFA, no CAPTCHA, no federation, no consent, unusable for passwordless accounts).
The replacement for both, in every client shape that has a redirect-capable user agent, is authorization code
PKCE (Authorization Code and PKCE), because it moves the
actual authentication step into the authorization server’s own login page, rendered in a real browser the
client does not control. Native & Mobile Apps spells out exactly
what that buys a native app (CAPTCHA, brute-force defence, step-up, passkeys, federation, password-manager
autofill, all upgradeable without an app release); Browser-Based
Apps does the same for the SPA case.
Grant type vs. authentication method vs. 2FA — three different things
This is the distinction the whole OAuth Reference keeps returning to, so it is worth stating precisely once, here, before the flow pages use it as a given.
(a) An OAuth grant type is how a client obtains a token. It is a protocol-level exchange between the client
and the authorization server’s token endpoint: which parameters it sends, what it authenticates as, and what
comes back. authorization_code, client_credentials, and urn:ietf:params:oauth:grant-type:device_code are
grant types. The client cares about the grant type because the client is a party to it.
(b) An authentication method is how the authorization server identifies the resource owner behind that grant.
It happens entirely inside the authorization server’s own login ceremony — typically during the authorization
code grant’s redirect to /authorize, or during CIBA’s backchannel exchange — and the client never sees it.
A password checked against a hash, an e-mail or SMS one-time code, a magic link, a push approval on a trusted
device, a WebAuthn/FIDO2 passkey, a device-bound credential, a federated login via "Sign in with Google", and
smart-card mutual TLS are all authentication methods. Swapping one for another changes nothing about which
grant_type the client used; the client still received an authorization code the same way. This is exactly why
OAuth can keep improving how users authenticate without ever touching client code, and it is the subject of its
own page: Authentication Methods, 2FA and
Passwordless.
(c) 2FA/MFA is a third thing again — an additional security layer stacked on top of whichever primary
authentication method is in use. It is not a grant type, and it is not itself a primary authentication
method. A resource owner who logs in with a password and then confirms a TOTP code from an authenticator app
has completed a hardened password login, not a passwordless one, and not a distinct grant type either — the client still receives its token from the same authorization_code exchange it always would have. Stacking
a second factor changes the strength of the authentication event (visible to the client afterwards only as the
acr / amr claims and auth_time, if the client bothers to look), never the grant that carried the token.
| Layer | Lives at |
|---|---|
OAuth grant type |
The token endpoint; a protocol-level exchange the client is a party to |
Authentication method (primary) |
The authorization server’s own login ceremony; invisible to the client |
2FA / MFA (secondary layer) |
Stacked on top of the primary method, inside the same login ceremony; invisible to the client |
If you came here looking for "the 2FA flow" or "the passkey flow," there is no such grant type — both are
authentication concerns handled entirely by the authorization server. The full enumerated list of primary
authentication methods and of 2FA layers, kept in strictly separate lists, lives on
Authentication Methods, 2FA and
Passwordless; the client-visible request/response mechanics (acr_values, prompt, max_age, acr, amr,
auth_time, and RFC 9470 step-up) are covered there too.
Common mistakes when picking a flow
These recur often enough across real integrations to call out explicitly, before the dedicated flow pages go into the mechanics of each one:
-
Choosing the implicit grant for a new SPA because an old tutorial says so. Every current OAuth or OIDC library defaults to authorization code + PKCE for browser-based clients; a tutorial or library version that still recommends
response_type=tokenpredates PKCE’s widespread adoption. See Legacy Implicit and Password Grants before reaching for it. -
Reaching for the resource owner password grant "just for our own first-party app." This is precisely the case the grant’s removal targets — see [The governing rule: a client never sees the resource owner’s password] above, and the narrow, still-drafted exception at
draft-ietf-oauth-first-party-appsbefore assuming this is settled ground. -
Treating client credentials as "the grant for backend services" without checking whether a user is actually involved. If a backend service is calling a downstream API on behalf of a signed-in user rather than as itself, client credentials silently drops that user’s identity from the call chain — what is needed instead is token exchange or an assertion grant, so the downstream service can still see, and authorize against, who the original request was for.
-
Assuming the device authorization grant is only for TVs. Any client that cannot render a redirect-capable browser qualifies — CLIs run over SSH, kiosks, and IoT devices with a display but no browser engine all fit the same shape; see Device Authorization Grant.
-
Conflating "the client needs the user’s identity" with "the client needs a grant type for 2FA." There is no such grant type — see Grant type vs. authentication method vs. 2FA — three different things below before searching further for one.
Worked examples
A few concrete deployments, to make the decision tree above less abstract:
| Deployment | Grant | Notes |
|---|---|---|
A server-rendered dashboard (Thymeleaf, JSP, or similar) that calls its own backend API |
Authorization code + PKCE, confidential client |
The server holds a client secret (or a |
A React SPA calling a public API directly from the browser |
Authorization code + PKCE, public client, ideally behind a BFF |
See Browser-Based Apps for why "public client, no BFF" is the higher-risk variant of this row. |
An iOS app signing users in with the system browser |
Authorization code + PKCE via |
See Native & Mobile Apps. |
A nightly batch job reconciling orders against a partner API, no user involved |
Client credentials |
|
A smart TV app signing a household member in to a streaming service |
Device authorization grant |
|
An internal |
Token exchange (RFC 8693), carrying the caller’s identity forward |
|
A CI pipeline job authenticating to a cloud API with no interactive user available |
Client credentials with |
See the client-authentication comparison on Client Credentials and Client Authentication. |
Where to go next
-
Building the canonical interactive flow end to end: Authorization Code and PKCE.
-
Machine-to-machine calls with no user: Client Credentials and Client Authentication.
-
Devices with no browser: Device Authorization Grant.
-
Relaying identity across services: Token Exchange and Assertion Grants.
-
Understanding exactly why the two legacy grants are gone: Legacy Implicit and Password Grants.
References
-
RFC 7521 — Assertion Framework for OAuth 2.0 Client Authentication and Authorization Grants
-
RFC 7522 — SAML 2.0 Profile for OAuth 2.0 Client Authentication and Authorization Grants
-
RFC 7523 — JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants
-
RFC 9700 / BCP 240 — Best Current Practice for OAuth 2.0 Security
-
draft-ietf-oauth-v2-1-16— The OAuth 2.1 Authorization Framework (Internet-Draft, 3 September 2026) -
draft-ietf-oauth-first-party-apps— OAuth 2.0 for First-Party Applications