Safeguard
Supply Chain Security•

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

OAuth 2.1 folds PKCE, exact redirect matching and the RFC 9700 security rules into one spec and drops implicit and password grants. How to migrate.

Safeguard Security Team
35 min read

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 S256 method is required for the authorization code grant and servers must enforce it. The plain method 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 iss in 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

DocumentDateTrackWhat it does
RFC 6749, OAuth 2.0 Authorization FrameworkOctober 2012Standards TrackCore OAuth 2.0; obsoletes RFC 5849 (OAuth 1.0)
RFC 6750, Bearer Token UsageOctober 2012Standards TrackSending bearer tokens to a resource server
RFC 7636, PKCESeptember 2015Standards TrackBinds an authorization code to the client that asked for it
RFC 8252, OAuth 2.0 for Native AppsOctober 2017BCP 212, updates 6749External browser and loopback redirects
RFC 8414, Authorization Server MetadataJune 2018Standards Track/.well-known/oauth-authorization-server
RFC 8628, Device Authorization GrantAugust 2019Standards TrackSign in on input constrained devices
RFC 8705, Mutual TLS and certificate bound tokensFebruary 2020Standards TrackSender constraint through the TLS client certificate
RFC 8707, Resource IndicatorsFebruary 2020Standards TrackThe resource parameter naming the target API
RFC 9126, Pushed Authorization RequestsSeptember 2021Standards TrackSend the authorization request over a back channel
RFC 9207, Issuer IdentificationMarch 2022Standards TrackThe iss authorization response parameter
RFC 9449, DPoPSeptember 2023Standards TrackSender constraint at the application layer
RFC 9700, OAuth 2.0 Security BCPJanuary 2025BCP 240, updates 6749, 6750, 6819Modern security rules for OAuth 2.0
RFC 9728, Protected Resource MetadataApril 2025Standards Track/.well-known/oauth-protected-resource
draft-ietf-oauth-v2-1-163 September 2026Internet-DraftOAuth 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
ParameterRequired in 2.1?What it does
response_typeRequiredMust be code; token is no longer defined
client_idRequiredIdentifies the registered client
code_challengeRequired, one narrow exceptionHash of the verifier
code_challenge_methodRequiredS256, or a future extension method
redirect_uriRequired only if several are registeredMust exactly match a registered value
scope, stateOptionalRequested 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

RuleLevel in 2.1Source
HTTPS for every OAuth URL except loopback redirectsMUSTDraft 1.5
iss in the authorization responseREQUIRED from servers (draft 16)Draft 4.1.2, RFC 9207
Mix-up defense when using several authorization serversMUSTDraft 2.3.4
CSRF protection: PKCE, nonce or one time stateMUSTDraft 2.3.3
No open redirectors on clients or serversMUST NOTRFC 9700 4.11
Sender constrained access tokensSHOULDDraft 1.4.3
Native apps use the system browser, not an embedded viewMUST NOT embedRFC 8252 8.12
Loopback IP literal rather than localhostlocalhost NOT RECOMMENDEDDraft 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:

AttackerCapability
A1, web attackerRuns websites, servers and browsers and lures users to them
A2, network attackerReads, changes and spoofs anything not protected by TLS
A3Reads the authorization response (open redirector, log, Referer)
A4Reads the authorization request the same ways
A5Obtains 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 case2.1 approachSecurity propertiesOperational cost
Server side web app acting for a userAuthorization code + PKCE, confidential client, asymmetric client authCode bound to verifier and client; secret never in browserLow
Single page appAuthorization code + PKCE, public client, rotated or DPoP bound refresh tokens; or a backend that holds tokensNo tokens in the URL; refresh replay detectedMedium
Native mobile or desktop appAuthorization code + PKCE in the system browser, loopback or claimed https redirectNo embedded web view; code bound to verifierMedium
Service to service, no userClient credentials, confidential client, mTLS or private_key_jwtNo user credentials involvedLow to medium
TV, CLI or kiosk with no browserDevice authorization grant (RFC 8628)Separate extension; known phishing riskMedium; restrict who may use it
High value API (payments, health)Add DPoP or mTLS, PAR, audience restricted tokensStolen tokens unusable without key; request integrityHigher
Clients that discover APIs at run time (including MCP)RFC 9728 + RFC 8414 discovery, RFC 8707 resource, iss checksTokens bound to one resource; mix-up defenseMedium

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. S256 in metadata means support. Only a test shows enforcement, and conditional enforcement is open to downgrade.
  • Keeping plain for 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 state but not iss in multi provider clients. state stops 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 situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingThe Gold scan GitHub Action in CI, plus Safeguard policies and gates
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch (early access)
You want to start clean on new projectsPull 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

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.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.