OAuth 2.1 is the IETF's consolidated rewrite of OAuth 2.0: one document that replaces RFC 6749 and RFC 6750 and folds in PKCE, the native app and browser app guidance, and the OAuth Security Best Current Practice (RFC 9700). It keeps the authorization code, client credentials and refresh token grants, makes PKCE mandatory, requires exact redirect URI matching, and drops the implicit grant, the password grant and bearer tokens in query strings.
As of 23 October 2026 it is still an Internet-Draft, not an RFC. The current version is draft-ietf-oauth-v2-1-16, dated 3 September 2026. That matters less than it sounds, because almost every rule in it already sits in a published RFC, most of them in RFC 9700, a Best Current Practice since January 2025. If you follow RFC 9700 today, moving to 2.1 is mostly deleting things you should not be using.
For the basics of delegated access, start with What is OAuth?. This post covers the flows message by message, every change from 2.0, the weakness classes each change answers, and commands to check your own authorization server.
Key takeaways
- OAuth 2.1 (draft-ietf-oauth-v2-1-16, 3 September 2026) "replaces and obsoletes" RFC 6749 and RFC 6750, and consolidates RFC 7636 (PKCE), RFC 8252 (native apps), the browser-based apps draft and RFC 9700.
- PKCE with the
S256method is required for the authorization code grant and servers must enforce it. Theplainmethod is forbidden as of draft 16. - Redirect URIs must match a registered value exactly. The only exception is the port of a loopback redirect for native apps.
- The implicit grant, the password grant and access tokens in URI query strings are gone. Public client refresh tokens must be rotated or sender constrained.
- Draft 16 requires servers to send
issin the authorization response (RFC 9207), a simple mix-up defense. DPoP (RFC 9449) or mTLS (RFC 8705) sender constrained tokens are a SHOULD. - The Model Context Protocol builds its HTTP authorization on OAuth 2.1. Its current revision, 2026-07-28, cites draft 13 and makes clients refuse to proceed if the authorization server does not advertise PKCE.
What OAuth is for, the layer and the standard
OAuth lets an application act on a user's resources without ever holding the user's password. Instead of sharing a password, the app gets an access token, "a credential representing a specific set of access attributes such as scope and lifetime."
There are four roles. The resource owner is usually the user. The client is the application asking for access. The authorization server authenticates the resource owner, collects consent and issues tokens. The resource server hosts the protected API and accepts tokens.
OAuth is an application layer protocol that runs over HTTP; the draft says use "over any protocol other than HTTP is out of scope." It has no port of its own. Every endpoint is an HTTPS URL, normally on 443, and the draft requires the https scheme everywhere except loopback redirect URIs for native apps. It is an authorization protocol, not an authentication protocol; sign in is the job of OpenID Connect, covered in our posts on OpenID Connect and SAML vs OAuth vs OpenID Connect.
The standards involved
| Document | Date | Track | What it does |
|---|---|---|---|
| RFC 6749, OAuth 2.0 Authorization Framework | October 2012 | Standards Track | Core OAuth 2.0; obsoletes RFC 5849 (OAuth 1.0) |
| RFC 6750, Bearer Token Usage | October 2012 | Standards Track | Sending bearer tokens to a resource server |
| RFC 7636, PKCE | September 2015 | Standards Track | Binds an authorization code to the client that asked for it |
| RFC 8252, OAuth 2.0 for Native Apps | October 2017 | BCP 212, updates 6749 | External browser and loopback redirects |
| RFC 8414, Authorization Server Metadata | June 2018 | Standards Track | /.well-known/oauth-authorization-server |
| RFC 8628, Device Authorization Grant | August 2019 | Standards Track | Sign in on input constrained devices |
| RFC 8705, Mutual TLS and certificate bound tokens | February 2020 | Standards Track | Sender constraint through the TLS client certificate |
| RFC 8707, Resource Indicators | February 2020 | Standards Track | The resource parameter naming the target API |
| RFC 9126, Pushed Authorization Requests | September 2021 | Standards Track | Send the authorization request over a back channel |
| RFC 9207, Issuer Identification | March 2022 | Standards Track | The iss authorization response parameter |
| RFC 9449, DPoP | September 2023 | Standards Track | Sender constraint at the application layer |
| RFC 9700, OAuth 2.0 Security BCP | January 2025 | BCP 240, updates 6749, 6750, 6819 | Modern security rules for OAuth 2.0 |
| RFC 9728, Protected Resource Metadata | April 2025 | Standards Track | /.well-known/oauth-protected-resource |
| draft-ietf-oauth-v2-1-16 | 3 September 2026 | Internet-Draft | OAuth 2.1, intended to obsolete RFC 6749 and 6750 |
What OAuth 2.1 consolidates, and its status today
Section 10 of the draft, "Differences from OAuth 2.0", lists what it absorbs: RFC 6749, RFC 8252, RFC 7636, the Browser-Based Apps draft, RFC 9700 and RFC 6750. Where a later document changed or retired part of RFC 6749, 2.1 carries the change or drops the feature. Section 1.8 sums it up: 2.1 is "compatible with OAuth 2.0 with the extensions and restrictions from known best current practices applied." There is no new message format. A 2.0 client already using the code grant with S256 PKCE, exact redirect URIs and header based tokens is, in practice, a 2.1 client.
Is OAuth 2.1 an RFC yet?
No. Draft 00 was posted on 30 July 2020. Draft 16 was posted on 3 September 2026 and expires on 7 March 2027 unless replaced. On the day we checked, the datatracker listed it as an active WG Document with IESG state "I-D Exists", meaning it has not been sent to the IESG. Section 9, on browser-based apps, still reads "TODO: Bring in the normative text of the browser-based apps BCP when it is finalized."
Draft 16 removed the PKCE plain method, made the iss response parameter required, added a consent phishing section and mentioned PAR in the authorization endpoint definition. Pin the draft number you build against, as the MCP specification does.
How OAuth 2.1 works on the wire
OAuth 2.1 has two client types. Confidential clients hold credentials registered with the authorization server, typically a server side web app. Public clients hold none, typically a native or single page app. It defines three grants: authorization code, client credentials and refresh token. Extension grants such as the device authorization grant (RFC 8628) stay separate documents.
The authorization code flow with PKCE, message by message
Browser (user agent) Client Authorization server Resource server
| | | |
| | 1. make code_verifier | |
| | (43 to 128 chars), | |
| | code_challenge = | |
| | BASE64URL(SHA256(v)) | |
| 2. redirect to /authorize?response_type=code&client_id&code_challenge |
| &code_challenge_method=S256&redirect_uri&state&scope |
|------------------------------------------------------>| |
| 3. user signs in and approves (or denies) | |
|<----------------------------------------------------->| |
| 4. 302 to redirect_uri?code&state&iss | |
|<------------------------------------------------------| |
|------------------------>| | |
| | 5. check state and iss | |
| | 6. POST /token | |
| | grant_type=authorization_code&code&code_verifier |
| |--------------------------->| |
| | | 7. check code, client, |
| | | SHA256(verifier) |
| | | matches challenge |
| | 8. access_token, | |
| | token_type, expires_in, | |
| | refresh_token | |
| |<---------------------------| |
| | 9. GET /resource Authorization: Bearer ... |
| |------------------------------------------------------>|
Step 1, the verifier. For every request the client generates a fresh code_verifier, a random string of 43 to 128 unreserved characters; the draft recommends 32 random octets, base64url encoded. It sends only code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier))).
Step 2, the authorization request. The draft's example:
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
&code_challenge=6fdkQaPm51l13DSukcAH3Mdx7_ntecHYd1vi3n0hMZY
&code_challenge_method=S256 HTTP/1.1
Host: server.example.com
| Parameter | Required in 2.1? | What it does |
|---|---|---|
response_type | Required | Must be code; token is no longer defined |
client_id | Required | Identifies the registered client |
code_challenge | Required, one narrow exception | Hash of the verifier |
code_challenge_method | Required | S256, or a future extension method |
redirect_uri | Required only if several are registered | Must exactly match a registered value |
scope, state | Optional | Requested access; opaque value echoed back |
The exception, in section 7.5.1, is a confidential client where the server has reasonable assurance the client checks the OpenID Connect nonce properly. PKCE is still recommended there, because with PKCE the server refuses to issue tokens for an injected code, while a nonce check only lets the client reject the ID token afterwards.
Step 3, consent. The server validates the client and redirect URI first. If either is missing or mismatched it "MUST NOT redirect the user agent to the invalid redirect URI" and should show its own error with HTTP 400.
Step 4, the authorization response.
HTTP/1.1 302 Found
Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
&state=xyz&iss=https%3A%2F%2Fauthorization-server.example.com
The code "MUST expire shortly after it is issued", with a recommended maximum of 10 minutes, and is bound to the client, the challenge and the redirect URI. iss is required from the server as of draft 16.
Step 5, client checks. The client checks state if it sent one and, if it uses more than one authorization server, compares iss with the issuer it recorded before the redirect.
Step 6, the token request.
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&code_verifier=3641a2d12d66101249cdf7a79c000c1f8c05d2aafcf14bf146497bed
There is no redirect_uri, a visible change from 2.0. Confidential clients authenticate; public clients send client_id.
Step 7, server checks. The server confirms the code belongs to this client, that a code_verifier is present if and only if a challenge was sent, and that the transformed verifier matches. No challenge in the original request means rejection. A code is redeemable once; a second valid redemption "MUST" be denied and tokens from that code "SHOULD" be revoked, but only if the replayed request is otherwise valid, so an attacker without the verifier cannot revoke the real client's tokens.
Steps 8 and 9. The server returns JSON with access_token, token_type, usually expires_in, and optionally refresh_token. The client sends the token in the Authorization header (Bearer, or DPoP for a DPoP bound token). Form encoded bodies remain allowed under strict conditions and never with GET. The query string is not allowed, and resource servers must ignore a token found there.
The client credentials flow
Client Authorization server
| 1. POST /token |
| grant_type=client_credentials |
| (client authenticates: secret, mTLS, |
| or private_key_jwt) |
|--------------------------------------------->|
| | 2. authenticate client,
| | apply its policy
| 3. access_token, token_type, expires_in |
|<---------------------------------------------|
No user, no browser. The grant "MUST only be used by confidential clients." The draft recommends asymmetric client authentication, mTLS (RFC 8705) or private_key_jwt, so the server stores no shared secret, and forbids more than one authentication method per request.
The refresh token grant
Refresh tokens stay between the authorization server and the client they were issued to, bound to that client. For public clients the server must sender constrain them or rotate them on every use, a new token must carry exactly the old scope, and tokens should expire after client inactivity. RFC 9700 explains rotation as a tripwire: if a stolen token is used by both attacker and client, one presents an invalidated token and the server revokes the active one.
What was removed and what became mandatory
Removed: the implicit grant
The implicit grant returned an access token in the redirect URL. RFC 9700 section 2.1.2 explains why it went: front channel tokens are exposed to leakage and injection, and "no standardized method for sender-constraining exists" for them. 2.1 no longer defines response_type=token. OpenID Connect's response_type=id_token is unaffected. Single page apps move to the code flow with PKCE as public clients.
Removed: the resource owner password credentials grant
RFC 9700 section 2.4 says it "MUST NOT be used." The client collects the user's password, which widens where it can leak, trains users to type passwords outside the authorization server, and cannot work with multi step or origin bound sign in such as WebAuthn. User facing apps move to the code flow in the system browser; scripts move to client credentials.
Removed: tokens in the URL query and PKCE plain
RFC 6750 already said the access_token query parameter "SHOULD NOT be used." 2.1 forbids it and tells resource servers to ignore it, because URLs land in history, logs and Referer headers. Draft 16 also forbids PKCE plain. Its reasoning: 2.1 requires TLS 1.2 or later, which already needs SHA-256, so any 2.1 device can do S256, and a server that still accepts plain has a downgrade it cannot detect.
Mandatory: PKCE everywhere and exact redirect matching
PKCE was designed for native apps in 2015. 2.1 applies it to every client because "all kinds of OAuth clients, including web applications and other confidential clients, are susceptible to authorization code injection attacks." Servers must also block PKCE downgrade, accepting a code_verifier only when the request carried a code_challenge.
Servers must require the complete redirect URI, path included, and reject anything that is not an exact string match (RFC 3986 section 6.2.1). Wildcards and prefix rules are out. Loopback redirects such as http://127.0.0.1:51004/cb are the exception: any port must be allowed, because native apps get an ephemeral port at run time.
Changed: no redirect_uri in the token request
PKCE now does what the 2.0 token request's redirect_uri was for, so 2.1 drops it. A server supporting both versions must keep accepting and enforcing it from clients that send it, and can use client_id to decide. A pure 2.1 client will not send it, so it cannot work with a server that insists, one of the few real interoperability breaks.
The other rules at a glance
| Rule | Level in 2.1 | Source |
|---|---|---|
| HTTPS for every OAuth URL except loopback redirects | MUST | Draft 1.5 |
iss in the authorization response | REQUIRED from servers (draft 16) | Draft 4.1.2, RFC 9207 |
| Mix-up defense when using several authorization servers | MUST | Draft 2.3.4 |
CSRF protection: PKCE, nonce or one time state | MUST | Draft 2.3.3 |
| No open redirectors on clients or servers | MUST NOT | RFC 9700 4.11 |
| Sender constrained access tokens | SHOULD | Draft 1.4.3 |
| Native apps use the system browser, not an embedded view | MUST NOT embed | RFC 8252 8.12 |
Loopback IP literal rather than localhost | localhost NOT RECOMMENDED | Draft 8.4.2 |
How the OAuth 2.0 Security BCP (RFC 9700) relates
RFC 9700 (January 2025, BCP 240) updates RFC 6749, RFC 6750 and RFC 6819. It exists because attackers kept exploiting known anti-patterns, OAuth spread into open banking and eHealth, deployments became dynamic, and browsers changed how fragments survive redirects. RFC 9700 is the rulebook for OAuth 2.0 as deployed; 2.1 is the base spec rewritten with those rules built in. RFC 9700 says so itself: OAuth 2.1 "will incorporate security recommendations from this document." For audits and policies, cite RFC 9700 for the requirement and the 2.1 draft as the target design.
Its attacker model is referenced by number in 2.1:
| Attacker | Capability |
|---|---|
| A1, web attacker | Runs websites, servers and browsers and lures users to them |
| A2, network attacker | Reads, changes and spoofs anything not protected by TLS |
| A3 | Reads the authorization response (open redirector, log, Referer) |
| A4 | Reads the authorization request the same ways |
| A5 | Obtains an access token, for example from a compromised resource server |
PKCE plain fails against A4. Sender constrained tokens answer A5.
Security properties and weakness classes, as concepts
OAuth gives delegated, scoped, time limited authorization and relies on TLS for confidentiality and integrity. A bearer token proves nothing about who presents it: "any party in possession of the token" can use it. Replay protection exists only where the protocol adds it: one time codes, PKCE, refresh rotation and sender constraint. Availability is out of scope. The classes below come from the standards' security considerations.
Code interception and injection
Codes travel through the browser and can leak via a malicious app claiming the same URL scheme, a log or an open redirector. Interception means someone else redeems the code; injection means an attacker's code is planted in the victim's client, and the draft notes the client has no way to notice. PKCE answers both, because only the holder of the verifier can redeem.
Redirect URI manipulation and PKCE downgrade
Pattern based redirect matching lets anyone controlling a matching page, or an open redirector on the client's site, receive codes (RFC 9700 section 4.1). Exact matching closes it. A server that enforces PKCE only when a challenge appears gives an attacker an off switch, a CSRF variant in RFC 9700 section 4.8; requiring the challenge closes it.
Mix-up
When a client uses several authorization servers and one is malicious, a code from an honest server can be steered to the attacker's token endpoint. Storing the server URL alone is not enough, since an attacker can pair an honest authorization endpoint with their own token endpoint. The defense is checking iss against the stored issuer (RFC 9207), or a distinct redirect URI per issuer.
Token leakage
Tokens leak through history, Referer headers, logs, front channel delivery and compromised or counterfeit resource servers (RFC 9700 sections 4.2, 4.3, 4.9). The answers stack: no tokens in URLs, no implicit grant, audience restricted tokens, short lifetimes and sender constraint.
Consent phishing
New as a named section in draft 16. The attacker registers a client, builds a request for broad scopes and sends the link; the victim signs in at the real server and approves. The draft is blunt: "phishing-resistant authenticators do not protect against consent phishing." Its fixes are server side: control registration, restrict which scopes a client may request, verify developers for sensitive scopes, show client and scopes clearly, and let users and admins revoke grants. A related abuse is covered in device code phishing.
CSRF, clickjacking and embedded views
PKCE, a checked nonce or a one time state stops CSRF at the redirect endpoint. RFC 9700 section 4.16 points to frame restrictions such as CSP frame-ancestors against clickjacking. RFC 8252 forbids embedded web views for authorization because the host app can read what is typed.
Sender constrained tokens, PAR and metadata
DPoP (RFC 9449, September 2023) binds tokens to a client key pair at the HTTP layer. Each request carries a DPoP header holding a short JWT typed dpop+jwt, with a unique jti, the method (htm), the URL (htu), the time (iat) and, at resource servers, ath, a hash of the token. Servers can demand freshness with DPoP-Nonce. Tokens come back as token_type: DPoP, and dpop_jkt can bind the code itself to the key.
mTLS (RFC 8705, February 2020) binds the token to the client's TLS certificate, recorded as an x5t#S256 thumbprint in the cnf claim. It is strong but needs client certificates end to end. Our post on bearer tokens vs DPoP and mTLS compares them.
PAR (RFC 9126, September 2021) has the client POST the full authorization request to a PAR endpoint first and receive a short lived request_uri, which is all the browser carries. RFC 9126 calls this "a confidential and integrity-protected authorization request", suggests a lifetime between 5 and 600 seconds, and says request URIs should be one time use. require_pushed_authorization_requests makes it mandatory.
Authorization server metadata (RFC 8414, June 2018) lives at /.well-known/oauth-authorization-server, registered in the IANA Well-Known URIs registry. For migration, the key fields are code_challenge_methods_supported (if omitted, the server "does not support PKCE"), grant_types_supported, response_types_supported, token_endpoint_auth_methods_supported, plus extension fields for PAR, DPoP, mTLS and authorization_response_iss_parameter_supported.
Protected resource metadata (RFC 9728, April 2025) does the same for APIs at /.well-known/oauth-protected-resource, listing authorization_servers, bearer_methods_supported and dpop_bound_access_tokens_required, and can be pointed to from a 401 via the resource_metadata parameter of WWW-Authenticate. It is central to MCP.
Versions and history
The standards table above doubles as the timeline, from OAuth 2.0 in October 2012 and the first 2.1 draft on 30 July 2020 to draft 16 on 3 September 2026. In practice most deployments run OAuth 2.0 with some of those extensions, and the removed grants survive in older single page apps, first party mobile apps and internal scripts, which is why migration starts with an inventory.
Migrating from OAuth 2.0 to OAuth 2.1, step by step
Step 1: inventory every flow
List every client per authorization server: type, grant types, response types and redirect URIs. Search repositories you own for removed patterns with GNU grep:
grep -rnE "response_type=token|grant_type=password|code_challenge_method=plain|[?&]access_token=" \
--include='*.js' --include='*.ts' --include='*.py' --include='*.java' --include='*.go' --include='*.kt' --include='*.swift' .
Most code builds requests through libraries, so also record which OAuth libraries and versions each service ships; that often decides whether PKCE, DPoP or PAR is available.
Step 2: PKCE everywhere, then enforce
Make every client send S256 while the server still tolerates its absence, log requests without a code_challenge, fix those clients, then reject. Disable plain. Make sure metadata lists S256, since MCP clients refuse to start without it. For legacy clients that cannot do S256, the draft suggests issuing extended lifetime refresh tokens before cutting support, since the refresh grant does not need PKCE.
Step 3: tighten redirect URIs
Replace wildcards with full exact URIs; carry routing in state. Native apps use loopback IP literals and reverse domain private schemes; the draft says a private scheme without a period "SHOULD be rejected."
Step 4: retire implicit and password grants, and query tokens
Move single page apps to the code flow with PKCE, mobile apps to the system browser, service accounts to client credentials. Remove token and password from what each client may use. Make APIs ignore access_token in the query.
Step 5: refresh tokens, issuer checks and the redirect_uri change
Turn on rotation or DPoP binding for public client refresh tokens, with inactivity expiry. Return iss and have multi server clients check it. Keep accepting redirect_uri in token requests from older clients.
Step 6: plan sender constraint, PAR and metadata by risk
Sender constraint is a SHOULD, so start with APIs that move money or sensitive data: DPoP where client certificates are impractical, mTLS where you already run them. Add PAR for high value flows and publish RFC 8414 and RFC 9728 metadata.
Secure configuration, with defender-side check commands
Only run these against authorization servers, APIs and test clients you own or are authorised to test. They use curl and jq, with options taken from the official curl manual and jq manual. Use a dedicated test client and test account, never a production user's credentials. The variables are yours to set, for example AS=https://auth.example.com.
Check 1: read the authorization server's metadata
curl -s "$AS/.well-known/oauth-authorization-server" | jq '{
issuer,
code_challenge_methods_supported,
response_types_supported,
grant_types_supported,
token_endpoint_auth_methods_supported,
pushed_authorization_request_endpoint,
require_pushed_authorization_requests,
dpop_signing_alg_values_supported,
tls_client_certificate_bound_access_tokens,
authorization_response_iss_parameter_supported
}'
If you run OpenID Connect, also read $AS/.well-known/openid-configuration. What a 2.1 ready server should show: issuer identical to the URL you built the request from, code_challenge_methods_supported containing S256 and not plain, no token in response_types_supported, and no password or implicit in grant_types_supported. A quick filter for the removed values:
curl -s "$AS/.well-known/oauth-authorization-server" \
| jq '[.response_types_supported[]?, .grant_types_supported[]?, .code_challenge_methods_supported[]?]
| map(select(. == "token" or . == "password" or . == "implicit" or . == "plain"))'
An empty array is what you want. Metadata only describes what the server advertises, so the next checks confirm what it enforces.
Check 2: confirm PKCE is enforced, not just supported
Send an authorization request from your test client with no code_challenge and print the status and any redirect target:
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' \
"$AS_AUTHORIZE?response_type=code&client_id=$TEST_CLIENT&redirect_uri=$TEST_REDIRECT_ENC&state=pkcecheck1"
A 2.1 compliant server returns an error: either a redirect to your test redirect URI with error=invalid_request, or an error page. Some servers show the login page first and only fail after sign in. In that case, complete the flow in a browser with your test account and confirm that no code is issued. Repeat with code_challenge_method=plain and a dummy challenge; the server should refuse that too.
Check 3: confirm the removed grants are really off
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' \
"$AS_AUTHORIZE?response_type=token&client_id=$TEST_CLIENT&redirect_uri=$TEST_REDIRECT_ENC&state=implicitcheck1"
curl -s -u "$TEST_CLIENT:$TEST_CLIENT_SECRET" \
-d grant_type=password -d username=nobody -d password=invalid \
"$AS_TOKEN" | jq -r '.error'
Expect unsupported_response_type (or unauthorized_client) for the first and unsupported_grant_type (or unauthorized_client) for the second, both error codes defined in the 2.1 draft. Anything that looks like a token means the grant is still live for that client.
Check 4: confirm exact redirect URI matching
Using your own test client, send an otherwise valid authorization request whose redirect_uri adds one extra path segment to the registered value. The server must not redirect anywhere; the draft says it should show an error itself, usually with HTTP 400. If the second column of the curl output shows your altered URI as the redirect target, the server is matching loosely.
Check 5: confirm the API ignores tokens in the query string
curl -s -o /dev/null -w '%{http_code}\n' "$API/resource?access_token=$TEST_TOKEN"
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $TEST_TOKEN" "$API/resource"
The first should return 401, because the resource server must ignore a query token. The second should succeed. Look at the 401 headers too:
curl -s -i "$API/resource" | grep -i '^www-authenticate'
You should see a Bearer or DPoP challenge, and on RFC 9728 deployments a resource_metadata URL.
Check 6: read the protected resource metadata
curl -s "$API/.well-known/oauth-protected-resource" | jq '{resource, authorization_servers, bearer_methods_supported, dpop_bound_access_tokens_required}'
Confirm the listed authorization servers are the ones you expect and that bearer_methods_supported does not list query.
Check 7: confirm refresh token rotation for public clients
With a test public client, redeem a refresh token once, keep the old value, then try it again:
curl -s -d grant_type=refresh_token -d client_id=$TEST_PUBLIC_CLIENT -d refresh_token=$OLD_REFRESH_TOKEN "$AS_TOKEN" | jq -r '.error'
Expect invalid_grant, then confirm the server also revoked the newer refresh token, which is the rotation tripwire RFC 9700 describes. Skip this check if your public clients use DPoP bound refresh tokens instead.
When to use which flow and which extension
| Use case | 2.1 approach | Security properties | Operational cost |
|---|---|---|---|
| Server side web app acting for a user | Authorization code + PKCE, confidential client, asymmetric client auth | Code bound to verifier and client; secret never in browser | Low |
| Single page app | Authorization code + PKCE, public client, rotated or DPoP bound refresh tokens; or a backend that holds tokens | No tokens in the URL; refresh replay detected | Medium |
| Native mobile or desktop app | Authorization code + PKCE in the system browser, loopback or claimed https redirect | No embedded web view; code bound to verifier | Medium |
| Service to service, no user | Client credentials, confidential client, mTLS or private_key_jwt | No user credentials involved | Low to medium |
| TV, CLI or kiosk with no browser | Device authorization grant (RFC 8628) | Separate extension; known phishing risk | Medium; restrict who may use it |
| High value API (payments, health) | Add DPoP or mTLS, PAR, audience restricted tokens | Stolen tokens unusable without key; request integrity | Higher |
| Clients that discover APIs at run time (including MCP) | RFC 9728 + RFC 8414 discovery, RFC 8707 resource, iss checks | Tokens bound to one resource; mix-up defense | Medium |
Why MCP builds its authorization on OAuth 2.1
On the day we checked, modelcontextprotocol.io served revision 2026-07-28 as current. Its authorization spec covers HTTP transports only; authorization is optional, and stdio servers "SHOULD NOT" use it and take credentials from the environment instead. A protected MCP server is an OAuth 2.1 resource server, the MCP client is an OAuth 2.1 client, and a separate or co-hosted authorization server issues tokens. It builds on OAuth 2.1 draft 13, RFC 6750, RFC 8414, RFC 7591, RFC 8707, RFC 9728, RFC 9207, the Client ID Metadata Document draft and OpenID Connect Discovery.
OAuth 2.1 fits because MCP is the dynamic world RFC 9700 describes: clients meet servers at run time with no prior trust. MCP servers "MUST implement" RFC 9728 metadata, authorization servers must offer RFC 8414 or OpenID Connect discovery, and clients must reject metadata whose issuer does not match the URL fetched. Clients must send the RFC 8707 resource parameter, and servers accept only tokens issued for them, so a token cannot be replayed at another server or passed downstream. Tokens "MUST NOT" go in the query string.
MCP is stricter than OAuth 2.1 on PKCE discovery: clients "MUST verify PKCE support before proceeding," and if code_challenge_methods_supported is missing they "MUST refuse to proceed." That makes Check 1 above a prerequisite for any authorization server in front of an MCP server. Our post on the 2025-06-18 MCP spec and OAuth resource servers covers how that model arrived. Note the version gap: MCP cites draft 13 while the IETF draft is at 16; the changes since, removing plain and requiring iss, point the way MCP already goes.
Common mistakes
- Waiting for an RFC number. The rules come from RFC 9700, published January 2025.
- Advertising PKCE without enforcing it.
S256in metadata means support. Only a test shows enforcement, and conditional enforcement is open to downgrade. - Keeping
plainfor old clients. It fails against anyone who can read the request, and the server cannot tell real old clients from modified requests. - Wildcard redirect URIs for preview environments. Register each full URI.
- Leaving the password grant on for one internal tool. It stays live for that client and cannot work with phishing resistant sign in.
- Checking
statebut notissin multi provider clients.statestops CSRF, not mix-up. - Long lived bearer refresh tokens in browsers. Public clients need rotation or sender constraint.
- Using an access token as proof of identity. The draft calls that outside the standard; use OpenID Connect.
- Logging full redirect URLs. They contain authorization codes.
FAQ
What is the difference between OAuth 2.0 and OAuth 2.1?
OAuth 2.1 is OAuth 2.0 with the security best practices built in: mandatory S256 PKCE, exact redirect URI matching, no tokens in query strings and protected refresh tokens for public clients. It removes the implicit grant, the password grant and PKCE plain.
Is OAuth 2.1 an official standard?
Not yet. On 23 October 2026 it is draft-ietf-oauth-v2-1-16 (3 September 2026), an active working group document not yet sent to the IESG. Its rules come mostly from RFC 9700.
Is PKCE required in OAuth 2.1?
Yes, with S256, enforced by the server. The narrow exception is a confidential client the server trusts to check the OpenID Connect nonce, and PKCE is still recommended there.
Why was the implicit grant removed?
It returns access tokens in the browser redirect, where they can leak or be injected, and they cannot be sender constrained. RFC 9700 deprecated it.
What replaces the password grant?
The authorization code flow with PKCE in the system browser for user facing apps, and client credentials for machine to machine access.
What port does OAuth use?
None of its own. It runs over HTTPS, normally port 443, and 2.1 requires https for every endpoint except native app loopback redirects.
Is OAuth encrypted?
Not by itself. It relies on TLS, which 2.1 requires for all OAuth traffic. A signed JWT access token is signed, not encrypted.
Does MCP use OAuth 2.1?
Yes, for HTTP transports. In MCP 2026-07-28 the server is an OAuth 2.1 resource server that publishes RFC 9728 metadata, requires the RFC 8707 resource parameter and rejects tokens issued for anything else.
Where Safeguard fits
Safeguard does not run an authorization server or inspect OAuth traffic; the checks above are yours to run. The connection is the code that implements OAuth. Clients, APIs and authorization servers rely on open source OAuth, JWT and HTTP libraries, and whether a service can do S256 PKCE, DPoP or PAR often depends on the library version it ships.
Safeguard's software composition analysis, generally available, builds a full transitive dependency inventory from repositories, container images and SBOM files (SPDX and CycloneDX), so you can see which services carry which OAuth and JWT library versions before step 2. Vulnerability management with EPSS and KEV enrichment and reachability analysis, both generally available, rank advisories in those libraries by whether your code calls the affected path, and policies and gates, generally available, stop a known vulnerable version coming back. Safeguard's MCP server at mcp.safeguard.sh, listed in the Claude connector directory, uses OAuth when you connect Claude to it. MCP Guardrail, which issues proxy URLs for MCP servers, and the AI Gateway are in early access.
Fix it without waiting for upstream
OAuth weaknesses in practice usually live in the OAuth, JWT and HTTP libraries your clients and APIs depend on, so a library advisory has to reach every service that bundles it.
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
These demos show dependency and MCP connection workflows, not OAuth traffic inspection. You can download Safeguard and follow the CLI docs to try them on your own projects.
This demo connects Claude to the Safeguard MCP server with OAuth and lets you choose which tools Claude can use. It shows the MCP authorization model above from the user side: sign in, consent, then a scoped connection, with no password handed to the client.
Click through it at your own pace, or open it full screen.
This demo sets up the Safeguard runner on your own machine, approves it with its device code and then scans from the terminal. Pointed at a service, the same scan lists its OAuth, JWT and HTTP libraries with versions, the inventory you need before step 2.
Click through it at your own pace, or open it full screen.
This demo is a real CLI session: install, open the interactive terminal, ask it to fix the lodash CVE in a repository, and watch it read the finding and advisory, ask before each command, move lodash to 4.17.21, run npm install and npm test with approval, and print a summary without pushing anything. The same approve each step flow applies when the package to bump is an OAuth client library you need upgraded for S256 PKCE or DPoP support.
Click through it at your own pace, or open it full screen.
Quick check
Your authorization server lists S256 in code_challenge_methods_supported. Does that mean it enforces PKCE?
No. The metadata only says the server supports PKCE. OAuth 2.1 requires the server to reject authorization requests without a code challenge and to accept a code verifier only when the original request carried a challenge. Test it by sending a request without a challenge from your own test client and confirming no code is issued.
Why does OAuth 2.1 drop redirect_uri from the token request?
In OAuth 2.0 it guarded against authorization code injection. PKCE now does that job, because only the client holding the code verifier can redeem the code. Servers that support both versions must still accept and enforce redirect_uri from older clients that send it.
Sources
- draft-ietf-oauth-v2-1-16: The OAuth 2.1 Authorization Framework, 3 September 2026 (IETF datatracker)
- draft-ietf-oauth-v2-1-16, full text
- RFC 6749: The OAuth 2.0 Authorization Framework, October 2012
- RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage, October 2012
- RFC 7636: Proof Key for Code Exchange by OAuth Public Clients, September 2015
- RFC 8252: OAuth 2.0 for Native Apps, October 2017
- RFC 8414: OAuth 2.0 Authorization Server Metadata, June 2018
- RFC 8628: OAuth 2.0 Device Authorization Grant, August 2019
- RFC 8705: OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens, February 2020
- RFC 8707: Resource Indicators for OAuth 2.0, February 2020
- RFC 9126: OAuth 2.0 Pushed Authorization Requests, September 2021
- RFC 9207: OAuth 2.0 Authorization Server Issuer Identification, March 2022
- RFC 9449: OAuth 2.0 Demonstrating Proof of Possession (DPoP), September 2023
- RFC 9700: Best Current Practice for OAuth 2.0 Security, January 2025
- RFC 9728: OAuth 2.0 Protected Resource Metadata, April 2025
- Model Context Protocol specification 2026-07-28: Authorization
- Model Context Protocol specification 2026-07-28: Authorization server discovery
- Model Context Protocol specification 2026-07-28: Authorization security considerations
- curl manual page
- jq manual
- GNU grep manual
Last reviewed: 23 October 2026. OAuth 2.1 is still an Internet-Draft; check the datatracker for the current version before relying on draft specific details.