OAuth 2.1 vs 2.0: What Changed and Why
Pick an auth standard in 2026 and you'll hear the same advice on repeat: "use OAuth 2.1." Fair enough. But it helps to know what you're actually adopting. OAuth 2.1 isn't a brand new protocol. It's a cleanup of OAuth 2.0 that folds years of hard-won security advice straight into the spec, so the safe path becomes the default path. Below: what actually changed, why each change exists, and a short checklist for moving an existing app over.
NamoID bakes these rules in from the start. It's a single OIDC issuer that enforces PKCE on every flow, rotates refresh tokens, and never ships an implicit grant. You inherit the OAuth 2.1 posture instead of bolting it on later.
What OAuth 2.1 actually is
OAuth 2.0 lives across a base spec (RFC 6749) plus a long tail of follow-on RFCs and best-current-practice documents. Over the years, the community learned which parts were unsafe in practice, and that guidance landed in the OAuth 2.0 Security Best Current Practice. The catch? All of it lived in separate documents. A developer reading only RFC 6749 could ship a flow that was technically valid and still wide open.
OAuth 2.1 is a consolidation. It takes OAuth 2.0, pulls in the widely adopted security advice, and removes the modes that turned out to be foot-guns. You can read the working draft at oauth.net/2.1. The short version is this:
- It does not invent new grant types.
- It deletes the dangerous ones.
- It makes the safe options required instead of optional.
If you've already followed modern OAuth advice, you're most of the way there. If you're running an older integration that copied a 2012-era tutorial, this is the gap to close.
Four headline changes carry the rest of this post: PKCE is mandatory, the implicit and password grants are gone, redirect URIs must match exactly, and refresh tokens should rotate or be sender-constrained. One at a time.
PKCE is now mandatory
In OAuth 2.0, PKCE (Proof Key for Code Exchange, RFC 7636) started life as an add-on for mobile and single-page apps that couldn't keep a client secret. The idea is simple. The client generates a random secret called a code verifier, hashes it, and sends the hash (the code challenge) when it kicks off the flow. Later, when it exchanges the authorization code for tokens, it sends the original verifier. The server checks that the verifier hashes to the challenge it saw earlier.
That closes the authorization-code interception attack. Steal the authorization code as it passes through a redirect, and it's useless without the verifier, which never left the client.
OAuth 2.1 makes PKCE mandatory for every authorization-code flow, not just public clients. Confidential clients with a secret? Included. Trusted first-party apps? Also included. PKCE defends against a different class of attack than the client secret does, so having a secret is no excuse to skip it.
Here's what the start of a PKCE flow looks like over plain HTTP.
# 1. Create a code verifier and its S256 challenge
CODE_VERIFIER=$(openssl rand -base64 60 | tr -d '\n=+/' | cut -c1-64)
CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" \
| openssl dgst -sha256 -binary \
| openssl base64 -A | tr '+/' '-_' | tr -d '=')
# 2. Send the challenge on the authorize request
echo "https://issuer.example.com/v1/oauth/authorize?\
response_type=code&client_id=your-client&\
redirect_uri=https://app.example.com/callback&\
scope=openid%20profile&state=$(openssl rand -hex 16)&\
code_challenge=$CODE_CHALLENGE&code_challenge_method=S256"Then on the token exchange you send the verifier instead of relying only on a secret.
curl -s -X POST https://issuer.example.com/v1/oauth/token \
-d grant_type=authorization_code \
-d code="$AUTH_CODE" \
-d redirect_uri=https://app.example.com/callback \
-d client_id=your-client \
-d code_verifier="$CODE_VERIFIER"Use the S256 method, not plain. plain sends the verifier as the challenge with no hashing, which throws away the protection entirely. NamoID enforces PKCE on every flow, including trusted first-party clients, with no implicit path to fall back to. Want the verifier and challenge math walked through in detail? See PKCE explained.
Implicit and password grants are gone
Two OAuth 2.0 grant types are removed in OAuth 2.1.
The implicit grant (response_type=token) returned an access token directly in the URL fragment after login. It existed because old browsers and single-page apps couldn't safely do a server-side code exchange. The cost was steep. Tokens landed in browser history, in the Referer header, and in server logs, with no way to authenticate the client at the token step. The modern answer is the authorization-code flow with PKCE, which gives single-page apps a safe path. So the implicit grant is dropped entirely. In migration notes you'll see it as "implicit grant removed."
The resource owner password credentials grant (grant_type=password) let an app collect a username and password directly and trade them for tokens. That defeats the whole point of OAuth, which is to keep the user's credentials away from the client. It also makes federation, MFA, passkeys, and step-up auth nearly impossible to add later, since the app sits in the middle of the password instead of redirecting to the identity provider. OAuth 2.1 removes it.
| Grant type | OAuth 2.0 | OAuth 2.1 |
|---|---|---|
| Authorization code + PKCE | Allowed (PKCE optional) | Required path, PKCE mandatory |
Implicit (response_type=token) | Allowed | Removed |
Password (grant_type=password) | Allowed | Removed |
| Client credentials | Allowed | Allowed (machine-to-machine) |
| Refresh token | Allowed | Allowed, with rotation or sender-constraint |
The practical takeaway: if your app today opens its own login form and posts the password to a token endpoint, that's the pattern OAuth 2.1 wants you to retire. Redirect to the issuer instead, and let it own the credential, the MFA prompt, and the passkey ceremony.
Exact redirect-URI matching
When the authorization server finishes login, it sends the authorization code back to a redirect_uri. Steer that redirect to a URL you control, and you capture the code. OAuth 2.0 let some servers match redirect URIs loosely, say by prefix or by treating registered URIs as a base path. That opened the door to open-redirect and code-interception tricks.
OAuth 2.1 requires exact string matching of the redirect URI against the pre-registered values, with a narrow exception for localhost ports during native-app development. No wildcards, no "starts with," no ignoring the query string. The URI the client sends must be one of the registered URIs, character for character.
What this means in practice:
- Register every callback URL you actually use, including staging and preview environments, rather than relying on a wildcard.
- Do not append per-request data to the redirect URI. Carry that state in the
stateparameter or in your own session, not in the callback path or query. - Treat the redirect-URI list as a security boundary. Adding one should be a deliberate, reviewable change.
This pairs with the state parameter, which you should still send and verify on the way back to defend against CSRF on the redirect.
Refresh-token rotation
Access tokens are short-lived by design. Refresh tokens aren't, which makes them the high-value target. Let a refresh token leak, and an attacker mints access tokens until someone notices. OAuth 2.1 says refresh tokens for public clients must be either sender-constrained or rotated on every use. Refresh token rotation is the common answer, and the one most CIAM systems implement.
Rotation works like this. Every time a client redeems a refresh token, the server issues a new one and invalidates the old. The old token is now single-use and spent. The security win is in what happens on reuse: if the server ever sees an already-spent refresh token come back, it treats that as a stolen-and-replayed token and revokes the entire token family.
{
"access_token": "eyJhbGciOiJSUzI1Ni…",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "new-rotated-refresh-token-value",
"id_token": "eyJhbGciOiJSUzI1Ni…",
"scope": "openid profile"
}The subtle part is the legitimate race. A client that retries a refresh after a flaky network can redeem the same token twice by accident. A good implementation tells a tight retry window apart from a genuine replay. When in doubt, though, the safe default is to revoke the chain and force a fresh login.
NamoID rotates refresh tokens on every grant. If an old refresh token is presented after it has been rotated out, NamoID revokes the whole chain and appends an audit event for it, so a replay is both stopped and recorded. Tokens are RS256-signed and you verify them against the public keys at the JWKS endpoint, so verification stays stateless. If you are weighing a hosted issuer against building this yourself, the trade-offs are laid out in build vs buy auth in India.
A migration checklist
Moving an OAuth 2.0 integration to OAuth 2.1 is mostly about removing the risky modes and switching on the safe ones. Work through these in order.
- Add PKCE to every authorization-code flow. Generate a code verifier and
S256challenge on the client, send the challenge on/authorize, and send the verifier on/token. Do this even for confidential clients. - Remove the implicit grant. Replace any
response_type=tokenflow with authorization code plus PKCE. Single-page apps can do this safely now. - Remove the password grant. Stop collecting passwords in your own UI and posting them to a token endpoint. Redirect to the issuer and let it own login, MFA, and passkeys.
- Tighten redirect URIs. Replace wildcard or prefix matching with an exact-match allowlist. Register staging and preview URLs explicitly. Keep per-request data in
state, not in the URL. - Turn on refresh-token rotation. Issue a new refresh token on every redeem, invalidate the old one, and revoke the family on reuse. Make sure your client stores the latest refresh token after each call.
- Keep
stateand verify it. Generate a randomstateon/authorizeand check it on the callback to block CSRF. - Confirm token validation. Verify JWT signatures against the issuer's JWKS, check
iss,aud, andexp, and allow only a small clock-skew leeway.
If your integration already follows the OAuth 2.0 Security BCP, most of this is review, not rework. The two changes that trip people up: redirect-URI tightening (because wildcards are convenient) and password-grant removal (because it touches the login UI).
FAQ
Is OAuth 2.1 backwards compatible with OAuth 2.0?
Mostly, with two caveats. The authorization-code, client-credentials, and refresh-token flows carry over, so a well-built 2.0 client usually keeps working. But if you lean on the implicit grant or the password grant, those are gone. You'll need to migrate those flows to authorization code plus PKCE before you're 2.1 compliant.
Do confidential clients with a secret still need PKCE?
Yes. OAuth 2.1 makes PKCE mandatory for all authorization-code flows, including confidential clients and trusted first-party apps. A client secret authenticates the client at the token endpoint, while PKCE protects the authorization code in transit. They defend against different attacks, so you use both. NamoID enforces PKCE on every flow with no exceptions.
What replaces the implicit grant for single-page apps?
The authorization-code flow with PKCE. Modern browsers and the redirect-based code exchange make this safe for single-page apps, which is why OAuth 2.1 removed the implicit grant rather than keeping it for SPAs. You get an authorization code in the redirect, then exchange it for tokens using your PKCE verifier.
Is OAuth 2.1 a finished, published RFC?
As of writing, it's an active IETF working draft, not yet a numbered RFC. That's fine in practice. OAuth 2.1 consolidates stable, widely deployed OAuth 2.0 specs and best practices rather than inventing new protocol design. Build to the 2.1 posture today and you're just following advice that's already industry standard. Track the status at oauth.net/2.1.
See where NamoID fits
NamoID is built to give you the OAuth 2.1 posture out of the box: PKCE on every flow, no implicit or password grants, exact redirect-URI matching, rotating refresh tokens, and RS256 JWTs verified through a JWKS endpoint. It exposes a single OIDC issuer URL, and it puts India verification rails and a DPDP-grade audit trail behind that same issuer.
Want to see how this maps to your stack? Book a 30-minute walkthrough at calendly.com/polymindslabs/30min or email us at hello@namoid.in. We're happy to talk through your current flows and where the migration gaps are.