ndovuoauth2

FLOW DEEP DIVE · RFC 6749 §4.2 & §4.3 · RETIRED

The retired grants

Why OAuth2 retired the implicit and resource-owner-password grants: what problems they solved in 2012, the exact failures that killed them, and what replaced them.

October 26, 2026do not use for new workWhat replaced them
01 · Why a page about dead things

Because they are not dead. Half the OAuth questions on the internet are legacy deployments still trading on these two grants, and their failures are the clearest lessons in the whole framework: each was retired by a specific, nameable attack surface, and each replacement exists because the fix had a name too. OAuth 2.1 settles the disposition by omission - where RFC 6749 defined four grant types, the 2.1 draft (v16, 2026) defines three: "authorization code, refresh token, and client credentials" (section 1.3). Implicit and resource-owner-password are simply gone.

The formal record, precisely stated: RFC 9700 §2.1.2 says clients "SHOULD NOT use the implicit grant (response type token)" - a SHOULD NOT, not a MUST NOT, a distinction many summaries get wrong. For the password grant the BCP is blunt: it "MUST NOT be used" (§2.4). And OAuth 2.1 removes both from the specification entirely.

02 · Implicit (4.2): the token in the URL fragment

The 2012 problem. Browser apps ran on a different domain than the authorization server, and the same-origin policy made the code flow's token-endpoint POST impossible - CORS was not there to help. The implicit grant was the workaround: skip the code and the token endpoint entirely, and hand the access token straight back through the browser in the redirect's URL fragment. RFC 10017 §4, the browser-apps BCP, tells this history plainly: the fragment delivery "bypass[ed] the need for a cross-origin POST request", and that "was one of the motivations for the definition of the Implicit grant".

Why the fragment. A clever detail, worth knowing: fragments (#access_token=...) are never sent to servers, so the token survived in the browser but not in HTTP logs. It was the least-bad place to put a token that had to ride the front channel.

What killed it. Everything that makes the front channel the front channel. The token leaks through browser history and sync, referrer headers, extensions, and misbehaving scripts on the redirect page - RFC 9700 §2.1.2's rationale lists "access token leakage and access token replay" with the citations. Worse, there is no recovery: no refresh token (6749 §4.2.2: "the authorization server MUST NOT issue a refresh token"), and no way to sender-constrain a token that was handed to a browser - as RFC 9700 puts it, no standardized method exists to bind such tokens to a client. Section 10.16 of the original RFC already warned that any sign-in built on implicit "MUST NOT use the implicit flow without additional security mechanisms", because a client cannot tell whose token it just received.

The replacement. CORS arrived, so the original constraint died. The code flow plus PKCE is strictly better for the same client: the token goes to the back channel, the code is worthless intercepted, and refresh tokens become safe. Every SPA migration story ends here; why no fix can make the fragment safe is RFC 10017's threat model (BCP 212), and the replacement architecture patterns (BFF, token-mediating backend) live in its architecture ranking.

On the wire - the whole grant is one redirect (4.2.1, 4.2.2)
GET https://accounts.example.com/authorize?
    response_type=token
    &client_id=sushi-tracker
    &redirect_uri=https://sushi.example.com/callback
    &scope=orders:read
    &state=Nb7Ka81Qxzqk

302 Found
Location: https://sushi.example.com/callback#access_token=<access-token-value>&token_type=bearer&expires_in=3600&state=Nb7Ka81Qxzqk

Same first hop as the code flow - exactly one parameter differs. Butresponse_type=token means there is no second hop: the token endpoint is never called, and the 302's fragment is the response (access_token and token_type REQUIRED,expires_in RECOMMENDED, state echoed back). Fragments never reach a server, so access logs never see the token. That was the design, and it worked exactly as designed.

It is unfixable, not merely risky. Everything that reads the browser reads the fragment: history, sync, referrer headers, extensions, and every script on the callback page - and the client cannot tell its own token from an injected one (10.16). No sender-constraining method exists for tokens issued in the authorization response (RFC 9700 §2.1.2), so the token can never be bound to the client holding it; no refresh token may exist to rotate (4.2.2); and there is no back channel to route the valuable part through. That is why the fix had to be architectural.

03 · Resource owner password (4.3): the anti-pattern

The intended use. The narrowest of the four: the user types their username and password directly into the client, which trades them at the token endpoint for a token. Section 4.3 framed it for "a trust relationship with the client, such as the device operating system or a highly privileged application", and - its real job - as a migration bridge "to migrate existing clients using direct authentication schemes such as HTTP Basic or Digest authentication to OAuth".

Why it died. It is the password anti-pattern OAuth was built to remove - the framework's own security considerations say so (10.7: it "maintains the password anti-pattern this protocol seeks to avoid") and the security BCP finished the sentence with a MUST NOT (9700 §2.4). The reasoning: it "insecurely exposes the credentials of the resource owner to the client"; it cannot work with two-factor authentication, WebAuthn, or anything else that happens at the real authorization endpoint; and it trains users to type passwords somewhere that is not the identity provider - which is phishing's training program. First-party status is also a mirage: the AS cannot verify who is really asking.

The replacement. The authorization-code flow with the real login screen. If the client truly is first-party, SSO makes it one tap - and it inherits every upgrade the provider ships, from passkeys to step-up authentication, with zero client changes.

On the wire - the credentials themselves are the grant (4.3.2)
POST /token HTTP/1.1
Host: accounts.example.com
Authorization: Basic c3VzaGktdHJhY2tlcjpZT1VSLUNMSUVOVC1TRUNSRVQ=  (base64 of "sushi-tracker:YOUR-CLIENT-SECRET")
Content-Type: application/x-www-form-urlencoded

grant_type=password
username=<resource-owner-username>
password=<resource-owner-password>
scope=orders:read

Read the body twice: that is the user's actual password, typed into the client and traded by the client at the token endpoint. The client never visits the AS's login page - it is the login page - so whatever the real authorization endpoint offers (2FA, WebAuthn, step-up) can never run. The AS validates the pair with its existing password validation algorithm and must protect the endpoint against brute force (4.3.2); the client is supposed to discard the credentials once the token arrives (4.3.1) - a MUST addressed to the party that just watched the user type them. The response is the standard 5.1 shape, refresh token allowed, which changes nothing about where the password went.

04 · Side by side

Both grants on one drawing. Where authorization-code returns a worthless code and mints the token on the back channel, each retired grant shortcuts the exchange: implicit hands the token itself to the browser, password hands the user's credentials to the client.

Retired grants sequence: implicit delivers the token through the browser; the password grant posts the user's credentialsClientAuth serverSPA or legacy appauthorize + token endpointsIMPLICIT (4.2) - one hop, front channel only1. GET /authorize (response_type=token)2. 302 redirect, #access_token=... - the token itselfPASSWORD (4.3) - one POST, back channel3. POST /token (grant_type=password + username + password)4. access_token (+ refresh token allowed)authorization-code instead: step 2 returns a single-use code - the token never rides a redirect

Each grant is a shortcut past the code exchange. Implicit's shortcut puts the access token in the browser; password's puts the user's credentials in the client. The code flow avoids both exposures by design.

Implicit (4.2)Password (4.3)Replaced by
Token crosses the browser?Yes - the token itself, in the fragmentNo, but the password crosses the clientOnly the code does (back-channel token)
Client authenticationNone possiblePassword = the credentialSecret or PKCE at the token endpoint
Refresh tokenForbidden (4.2.2)AllowedStandard, with rotation rules (9700 §4.14)
Sender-constraining possible?No - token handed to a browser (9700 §2.1.2)Not the blocker - the grant itself is (§2.4)Yes: mTLS, DPoP
2FA / WebAuthn at the ASWorks (login is at the AS)Impossible (9700 §2.4)Works
Formal statusSHOULD NOT use (9700 §2.1.2); removed in OAuth 2.1MUST NOT be used (9700 §2.4); removed in OAuth 2.1-
05 · If you still run one

The migration order that works: move browser apps from implicit to code + PKCE first (it is a client-side change plus AS CORS configuration); move password-grant clients to the code flow with the provider's own login screen (accept the SSO win); keep any password grant only behind a documented, time-boxed exception with the migration on the calendar. Nothing here is architectural risk any more - the replacements are a decade old and universally supported.

Run the replacement

The PKCE console executes the flow that replaced both of these - including the tamper buttons that demonstrate why it survives the attacks that killed them.

06 · Primary sources