Implicit and Password Grants: Why They Are Gone

Two flows you will still find in tutorials, what they were for, and what replaced them.

OAuth 2.0 defined four grant types. Two of them are now actively discouraged, and OAuth 2.1 removes them. You will still meet both in old code and old blog posts.

Implicit grant (response_type=token)

Designed for browsers before CORS was universal: the authorization server put the access token directly in the redirect URL fragment, so an SPA could read it without a back-channel call.

Why it is dead:

  • The token lands in the URL — browser history, referrers, logging proxies, shoulder-surfing.
  • No client authentication at all, and no PKCE, so an intercepted redirect hands over a working token.
  • No refresh token, so implementations resorted to hidden iframes to renew — which third-party cookie blocking has since broken anyway.

Replacement: Authorization Code + PKCE. CORS made the back-channel exchange possible from a browser, which removed implicit's only justification.

Resource owner password credentials (ROPC)

The client collects the user's username and password and posts them to /token. It exists only as a migration aid for legacy apps.

Why it is dead:

  • It defeats the entire purpose of OAuth — the app handles the password again.
  • It cannot support MFA, CAPTCHA, risk checks, or any interactive step.
  • It cannot be used with an external identity provider.
  • It trains users to type their credentials into third-party UIs, which is the phishing pattern.

Replacement: Authorization Code + PKCE, with a system browser on mobile and a redirect in the web.

What remains

GrantStatusUse for
Authorization Code + PKCE✅ recommendedevery user-facing client
Client Credentials✅ recommendedmachine-to-machine
Device Code✅ recommendedinput-constrained devices
Refresh Token✅ recommendedrenewing access
Implicit❌ removed in 2.1
Password (ROPC)❌ removed in 2.1

Your own login is not ROPC

One clarification: your first-party app posting an email and password to your own /login endpoint is not the ROPC grant and is perfectly fine. ROPC specifically means using OAuth's password grant, usually against a third-party authorization server.

Example

Example · bash
# ❌ Implicit — the token is in the URL, and stays in history
https://auth.dfg.com/authorize?response_type=token&client_id=abc-spa
  &redirect_uri=https://abc.com/callback

# comes back as:
https://abc.com/callback#access_token=ya29.a0Af...&expires_in=3600
#                        ^ in the address bar, in history, in any Referer

# ❌ ROPC — the app sees the password, and MFA becomes impossible
curl -X POST https://auth.dfg.com/oauth/token \
  -d grant_type=password -d username=alice -d password=s3cret \
  -d client_id=abc-spa

# ✅ Authorization Code + PKCE — replaces both
https://auth.dfg.com/authorize?response_type=code&client_id=abc-spa
  &redirect_uri=https://abc.com/callback
  &code_challenge=E9Melhoa2Owv...&code_challenge_method=S256&state=...

When to use it

  • A legacy SPA using the implicit grant is migrated to Authorization Code + PKCE, moving tokens out of the URL and enabling refresh tokens.
  • A mobile app abandons ROPC when the company introduces mandatory MFA, since the password grant has no way to prompt for a second factor.
  • A security review flags access tokens in server access logs, traced back to an implicit-grant redirect that put them in the query string.

More examples

Migrating implicit to code + PKCE

Three improvements in one migration: the token leaves the URL, PKCE binds the exchange to this client, and offline_access finally makes refresh tokens available.

Example · javascript
// ---------- BEFORE: implicit ----------
location.assign('https://auth.dfg.com/authorize?' + new URLSearchParams({
  response_type: 'token',                      // ← token straight into the URL
  client_id: 'abc-spa',
  redirect_uri: 'https://abc.com/callback',
  scope: 'openid profile',
}));

// on the callback page
const token = new URLSearchParams(location.hash.slice(1)).get('access_token');
localStorage.setItem('token', token);          // ← and now it is XSS-readable too

// ---------- AFTER: code + PKCE ----------
const verifier = randomVerifier();
sessionStorage.setItem('pkce_verifier', verifier);

location.assign('https://auth.dfg.com/authorize?' + new URLSearchParams({
  response_type: 'code',                       // ← a code, not a token
  client_id: 'abc-spa',
  redirect_uri: 'https://abc.com/callback',
  scope: 'openid profile offline_access',      // ← and now refresh is possible
  state: randomVerifier(),
  code_challenge: await challengeFor(verifier),
  code_challenge_method: 'S256',
}));

// on the callback page: exchange over the back channel, keep the token in memory
const tokens = await exchangeCode(params.get('code'), verifier);
setAccessTokenInMemory(tokens.access_token);
history.replaceState({}, '', '/');             // ← nothing sensitive left in the URL

Telling ROPC apart from your own login

This confusion causes real arguments in code review. Owning the user table means owning the password check; ROPC is about handing credentials to someone who does not.

Example · javascript
// ❌ ROPC: the OAuth password grant, usually against a third party.
//    Your app handles someone else's credentials. Avoid.
await fetch('https://auth.thirdparty.com/oauth/token', {
  method: 'POST',
  body: new URLSearchParams({
    grant_type: 'password',
    username, password,
    client_id: 'abc-spa',
  }),
});

// ✅ Your own first-party login endpoint. Not ROPC, and entirely fine —
//    you are the identity provider here, so you already hold the password hash.
await fetch('https://dfg.com/auth/login', {
  method: 'POST',
  credentials: 'include',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email, password }),
});

// The distinction: does a party OTHER than the credential's owner-of-record
// get to see the password? If yes, that is what OAuth exists to prevent.

Discussion

  • Be the first to comment on this lesson.