OAuth 2.1 vs OAuth 2.0: What Changed and How to Migrate

Photo by FlyD on Unsplash
OAuth 2.1 consolidates OAuth 2.0 (RFC 6749) and its security best practices into one specification that obsoletes RFC 6749 and RFC 6750. It removes insecure features rather than adding new ones: PKCE becomes mandatory, the implicit and password grants are gone, and rules for tokens and redirects are tightened.
Yes. In OAuth 2.0 PKCE was optional and mainly recommended for mobile and single-page apps. OAuth 2.1 requires PKCE for all clients using the authorization code flow, including confidential server-side web apps, to prevent authorization code interception and injection attacks.
Two grants are omitted: the implicit grant, which returned an access token directly in the redirect URL, and the resource owner password credentials grant, which had the app collect the user's real password. Browser apps should use authorization code with PKCE, and machine clients should use client credentials.
MCP's authorization specification is built directly on OAuth 2.1. It states authorization servers MUST implement OAuth 2.1 and MCP clients MUST implement PKCE, referencing draft-ietf-oauth-v2-1-13. A protected MCP server acts as an OAuth 2.1 resource server and access tokens must be sent in the Authorization header, never in a query string.
Migrate incrementally: first add PKCE to every authorization code client since it is backward compatible, then retire the implicit and password grants, then stop accepting bearer tokens in query strings, and finally enforce exact redirect-URI matching and refresh-token rotation for public clients. No big-bang rewrite is needed.

Photo by FlyD on Unsplash
Key Takeaway
OAuth 2.1 consolidates OAuth 2.0 and its security best practices into one specification. It makes PKCE mandatory for every client using the authorization code flow, removes the implicit and password grants, bans bearer tokens in URL query strings, requires exact redirect URI matching, and mandates refresh token rotation for public clients.
OAuth 2.0 was never a single document. It was RFC 6749 from 2012, the bearer token rules in RFC 6750, and then a decade of separate best-practice notes patching holes that the original framework left open. If you learned OAuth from a tutorial written in 2015, you almost certainly learned patterns that the community has since quietly retired. OAuth 2.1 is the cleanup: one specification that keeps what works, deletes what proved dangerous, and folds the security guidance directly into the normative text.
I have shipped enough integrations to know the gap between what the old spec allowed and what you should actually do in production. OAuth 2.1 closes that gap by making the safe path the only path. This post walks through exactly what changed, shows an accurate Authorization Code plus PKCE exchange, and gives you a migration order you can follow without breaking live clients.
The draft is explicit that it replaces and obsoletes RFC 6749 and RFC 6750, consolidating the information from those documents and the security best current practice into a single Standards Track specification while removing features found to be insecure. There is no new token format, no new core grant, and no new endpoint. If you already run the authorization code flow correctly today, most of your system is already OAuth 2.1 compliant. The work is in what you must stop doing.
Under OAuth 2.0, Proof Key for Code Exchange was an add-on, recommended mainly for mobile and single-page apps that could not keep a secret. OAuth 2.1 promotes it to a baseline requirement: PKCE is required for all clients using the authorization code flow, confidential web servers included. The client generates a random verifier, sends only its hash as a challenge on the authorization request, and reveals the verifier when it redeems the code. An attacker who intercepts the code cannot exchange it without the original verifier.
# 1. Client generates a PKCE pair (S256)
code_verifier = base64url( random(32 bytes) )
code_challenge = base64url( SHA256(code_verifier) )
# 2. Authorization request — a browser redirect that returns a code, never a token
GET /authorize?response_type=code
&client_id=s6BhdRkqt3
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
&scope=read%20write
&state=xyz
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256 HTTP/1.1
Host: as.example.com
# 3. Authorization server redirects back with a short-lived, one-time code
HTTP/1.1 302 Found
Location: https://app.example.com/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz
# 4. Token request — the client proves it owns the verifier behind that challenge
POST /token HTTP/1.1
Host: as.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
&client_id=s6BhdRkqt3
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXkThe example verifier and challenge values above are the canonical test vectors from RFC 7636 Appendix B. Use them to sanity-check your own S256 implementation: hashing that exact verifier with SHA-256 and base64url-encoding it must reproduce that exact challenge, or your encoding is wrong.
OAuth 2.1 omits two flows that RFC 6749 defined. Both leaked tokens or credentials in ways no configuration could fully fix, so instead of documenting workarounds the spec simply removes them.
If you have either flow in production, that is the migration that carries real risk, because it changes how clients obtain tokens. Everything else in OAuth 2.1 is mostly tightening rules you were probably already close to following.
Three smaller but load-bearing changes harden the parts of OAuth that attackers target most.
Exact redirect URI matching breaks integrations that relied on dynamic query parameters appended to the callback. Move any per-request data into the state parameter or PKCE-protected session, not the redirect URI, before you flip the switch, or your callbacks will start getting rejected.
| Aspect | OAuth 2.0 | OAuth 2.1 |
|---|---|---|
| PKCE | Optional, recommended for public clients | Required for all clients using the authorization code flow |
| Implicit grant (token in redirect) | Defined and allowed | Removed from the specification |
| Password grant (owner credentials) | Defined and allowed | Removed from the specification |
| Bearer token in a URL query string | Permitted by RFC 6750 | Not allowed; header or request body only |
| Redirect URI matching | Loose matching common in practice | Exact string matching required |
| Refresh tokens for public clients | Long-lived and reusable | Must be sender-constrained or one-time use |
| Base documents | RFC 6749 plus RFC 6750 plus separate notes | One spec that obsoletes 6749 and 6750 |
The reason OAuth 2.1 suddenly matters to a lot of developers is the Model Context Protocol. MCP is how AI clients connect to external tools and data servers, and its authorization spec is built directly on OAuth 2.1. The MCP specification states that authorization servers must implement OAuth 2.1 with appropriate security measures for both confidential and public clients, and that MCP clients must implement PKCE according to OAuth 2.1. It pins the exact reference: draft-ietf-oauth-v2-1-13.
In MCP terms, a protected MCP server acts as an OAuth 2.1 resource server, the MCP client is an OAuth 2.1 client, and access tokens travel only in the Authorization header, never in a query string. MCP also layers on resource indicators from RFC 8707 so a token is bound to the specific server it was issued for, and it requires refresh token rotation for public clients. If you build an MCP server, you are implementing OAuth 2.1 whether you set out to or not.
You do not need a big-bang rewrite. Migrate in an order that removes the riskiest patterns first and leaves the compatible tightening for last.
Do these four steps in order and you land on OAuth 2.1 without a flag day. The payoff is a smaller attack surface, one authoritative spec instead of a scavenger hunt across RFCs, and a token setup that is already compatible with MCP and the wave of AI tooling built on it.