Many OAuth deployments still follow the 2012 rules of RFC 6749, which allowed patterns now known to leak codes and tokens, from loose redirect matching to the implicit grant. RFC 9700, published in January 2025 as BCP 240, is the IETF's consolidated security advice for OAuth 2.0. It updates RFC 6749, RFC 6750 and RFC 6819, deprecates the least secure modes and describes 17 classes of attack with countermeasures. This guide walks through what changed, turns Section 2 into an audit table, explains the attacks practitioners most often miss (mix-up, code injection, PKCE downgrade) and gives a plan to bring an estate into line. It is for identity architects, authorisation server owners and application teams who need to audit an OAuth deployment against the current standard.
Key takeaways
- Exact redirect URI matching is mandatory. Authorisation servers MUST use exact string matching, with one exception for the port of localhost redirects in native apps.
- PKCE is for everyone. Public clients MUST use it, confidential clients are recommended to, and servers MUST support it and block the PKCE downgrade.
- Implicit and password grants are out. Clients SHOULD NOT use the implicit grant, and the resource owner password credentials grant MUST NOT be used.
- Bearer tokens should be sender-constrained. Mutual TLS (RFC 8705) or DPoP (RFC 9449) stop a stolen access token working elsewhere; public clients must also protect refresh tokens.
- Clients talking to several servers need mix-up defence. The
issresponse parameter (RFC 9207) is the preferred fix and is cheap to deploy.
What RFC 9700 is and what changed
RFC 9700 is a Best Current Practice, not a new protocol. It does not replace RFC 6749; it narrows the choices RFC 6749 left open. The OAuth 2.0 and OpenID Connect Guide explains the protocol; the OAuth 2.1, GNAP and FAPI Guide covers OAuth 2.1, which folds much of this advice into the core specification.
| Area | RFC 6749 era practice | RFC 9700 position |
|---|---|---|
| Redirect URIs | Prefix or pattern matching common | Exact string matching required (Section 2.1) |
| PKCE | Mobile apps only | Public clients MUST, confidential clients recommended, S256 (Section 2.1.1) |
| Implicit grant | Standard for single-page apps | SHOULD NOT be used (Section 2.1.2) |
| Password grant | Allowed for trusted clients | MUST NOT be used (Section 2.4) |
| Access tokens | Bearer | Sender-constrained with mTLS or DPoP recommended (Section 2.2) |
| Refresh tokens for public clients | Long-lived bearer | Sender-constrained or rotated (Section 2.2) |
| Token privileges | Broad scopes | Audience and privilege restriction (Section 2.3) |
| Client authentication | Client secrets | Asymmetric methods recommended (Section 2.5) |
| Discovery | Manual configuration | Authorisation server metadata (RFC 8414) recommended (Section 2.6) |
Section 2 as an audit table
Section 2 is the normative summary; Section 4 explains the attacks behind each rule. Use the table below as the audit worksheet, one row per check, recording pass, fail or not applicable for each client and authorisation server.
| Check | Requirement | Who | How to test |
|---|---|---|---|
| Redirect URI matching | Exact string match (2.1) | AS | Request with an added path segment, query or different case; must be refused |
| Open redirectors | None on client or AS (2.1, 4.11) | Both | Review redirect endpoints and the AS behaviour on invalid client_id or redirect_uri |
| CSRF | PKCE, nonce or one-time state (2.1, 4.7) | Client | Replay an authorisation response in a different browser session |
| Mix-up | Issuer check when using more than one AS (2.1, 4.4) | Client | Confirm the client stores the expected issuer per request and checks iss |
| PKCE | S256 for public clients, recommended for confidential (2.1.1) | Both | Send plain; send no challenge; check the AS rejects as configured |
| PKCE downgrade | Reject code_verifier if no challenge was sent (4.8) | AS | Strip the challenge from the authorisation request, then present a verifier |
| Implicit grant | Not used (2.1.2) | Client and AS | Look for response_type=token or id_token token in configs and logs |
| Sender-constrained access tokens | mTLS or DPoP (2.2) | AS and RS | Replay a captured token from another client; must fail |
| Refresh token protection | Rotation or sender constraint for public clients (2.2, 4.14) | AS | Use an old refresh token twice; the family should be revoked |
| Audience restriction | Tokens bound to a resource (2.3, 4.10.2) | AS and RS | Present a token for API A to API B |
| Password grant | Disabled (2.4) | AS | Attempt grant_type=password |
| Client authentication | Asymmetric where feasible (2.5) | AS | List clients still using secrets |
| Metadata | Published and used (2.6) | Both | Fetch /.well-known/oauth-authorization-server |
| Transport | No http redirect URIs except native loopback (2.6) | AS | Search registrations for http:// |
| CORS | Not supported at the authorisation endpoint (2.6) | AS | Send a preflight to the authorisation endpoint |
Redirect-based flow attacks in depth
Insufficient redirect URI validation (Section 4.1)
- The attack: a server that accepts any URI under a registered prefix lets an attacker register or find a page on that host and receive the code or token.
- The fix: exact string matching. Native apps may vary only the port of a localhost redirect, as RFC 8252 allows. Section 4.1.3 also suggests appending a fragment such as
#_to the Location header so browsers do not reattach an attacker's fragment. - Where it bites: wildcard redirect URIs for preview environments, and multi-tenant SaaS hosts where tenants control content.
Credential leakage via Referer and browser history (Sections 4.2 and 4.3)
- The attack: a code or token in a URL leaks through the Referer header to third-party resources on the landing page, or sits in browser history.
- The fix: no third-party resources on the redirect landing page or authorisation endpoint, a
Referrer-Policysuch asno-referrer, the form post response mode, one-time codes, and revocation of tokens if a code is redeemed twice.
Mix-up attacks (Section 4.4)
A client that works with several authorisation servers (a SaaS app supporting many enterprise IdPs, an aggregator, an AI agent platform) can be tricked into sending a code issued by an honest server to an attacker-controlled server. Two defences exist:
- Issuer identification (preferred): the server returns
issin the authorisation response (RFC 9207) or in an ID Token. The client stores the expected issuer per request, bound to the browser session, and aborts on mismatch. RFC 9207 also requires clients to reject responses withoutissfrom servers that advertiseauthorization_response_iss_parameter_supported. - Distinct redirect URIs: one redirect URI per issuer. RFC 9700 says to use this only when other options are unavailable, because an attacker may register a client with the honest server using the attacker's redirect URI.
Authorisation code injection (Section 4.5)
An attacker who has stolen a code injects it into their own session with the legitimate client, so the client redeems it and logs the attacker in as the victim. PKCE binds the code to the browser that started the flow; the OpenID Connect nonce does the same for confidential clients, but Section 4.5.3.2 notes it does not protect public clients. Section 4.5.4 adds that an attacker who can alter the challenge or nonce in the victim's request can bypass both, so protecting authorisation responses still matters.
PKCE downgrade (Section 4.8)
If PKCE is optional, an attacker removes code_challenge from the request so the code is issued without binding. The server must reject a token request containing a code_verifier when the original authorisation request had no challenge. Servers that require PKCE for every client meet this automatically, which is the simplest audit outcome.
Other front-channel issues
- CSRF (4.7): clients must confirm the server supports PKCE before relying on it for CSRF protection; otherwise use one-time
stateornonce, and protect any data carried instateagainst tampering. - Open redirection (4.11): the server must authenticate the user before redirecting and must not redirect automatically on an invalid client_id and redirect_uri combination, which attackers use to bounce victims to phishing pages (MITRE ATT&CK T1566.002 Spearphishing Link).
- 307 redirects (4.12): the server MUST NOT use HTTP 307 when the request may carry user credentials, because the browser would re-post the form to the client. Use 303.
- Clickjacking (4.16): authorisation servers MUST prevent clickjacking, with CSP
frame-ancestorson the authorisation endpoint andX-Frame-Optionsfor older browsers. - In-browser messaging (4.17): flows using
postMessagemust match origins exactly and never use the*wildcard.
Token-side protections
Sender-constrained tokens (Sections 2.2 and 4.10.1)
A bearer token works for anyone who holds it, which is why token theft is so effective (ATT&CK T1528 Steal Application Access Token, then T1550.001 Application Access Token). Sender-constraining binds the token to a key the client holds.
| Mechanism | Binding | Best fit | Watch out for |
|---|---|---|---|
| Mutual TLS (RFC 8705) | Token cnf carries x5t#S256 of the client certificate | Server-to-server, existing PKI, open banking | TLS terminating proxies must pass the certificate securely (Section 4.13) |
| DPoP (RFC 9449) | Token bound to a key; client signs a proof per request | Browser, mobile and cases where mTLS is impractical | Nonce handling, proof replay windows, library maturity |
| Audience restriction only | Token valid at one resource | Everywhere, as a baseline | Does not stop replay at that same resource |
See the DPoP Guide and the mTLS Guide. The Salesloft Drift breach shows the cost of bearer tokens: stolen OAuth access and refresh tokens for an integration were used from anonymising infrastructure against hundreds of Salesforce tenants. Sender-constraining is one of the controls that limits that kind of replay; the breach page itself focuses on IP restrictions, scopes and revocation.
Audience and privilege restriction (Sections 2.3 and 4.10.2)
- Audience: resource servers should check the audience and reject mismatches; the audience should be the URL the client actually calls. Clients signal it with the
resourceparameter (RFC 8707), and servers returninvalid_targetfor unknown resources. - Privilege: limit tokens with
scopeorauthorization_details(RFC 9396). See the OAuth Scope Design Guide.
Refresh tokens (Section 4.14)
- The server MUST decide, based on a risk assessment, whether to issue refresh tokens at all.
- Refresh tokens MUST be bound to the scope and resource servers the user consented to.
- For public clients the server MUST detect replay, through sender-constraining or rotation. With rotation, reuse of an old token revokes the active one.
- Servers may revoke refresh tokens on events such as password change or logout, and should expire them after inactivity. See the OAuth Refresh Token Rotation and Expiry Guide.
Client impersonating a resource owner (Section 4.15)
If a resource server cannot tell a client credentials token from a user token, a client that chooses a client_id resembling a user identifier may reach that user's data. Servers SHOULD NOT let clients influence their client_id or claims that could be confused with a user, and otherwise MUST give resource servers another way to distinguish the two. This matters for machine-to-machine and agent clients that register themselves.
Applying the BCP: a phased plan
- Inventory: list every authorisation server, client, grant type, redirect URI and authentication method. Most findings come from this step alone: implicit flows, password grants and wildcard redirects.
- Quick wins on the server (weeks 1 to 4): disable the password grant, enforce exact redirect matching, require PKCE with S256 for public clients, publish metadata and return
iss. Run each change in report-only or logging mode first where the product allows, then enforce for a pilot ring of clients. - Client remediation (months 1 to 3): move single-page apps from implicit to code with PKCE, preferably behind a backend for frontend (see the OAuth for Browser-Based Applications and the BFF Pattern Guide); add
isschecks in multi-issuer clients; fix open redirectors. - Token hardening (months 3 to 9): introduce audience restriction with resource indicators, refresh token rotation for public clients, and sender-constrained tokens for high-value APIs. Pilot DPoP or mTLS with one API before scaling.
- Client authentication (ongoing): migrate confidential clients to
private_key_jwtor mTLS. See the JWT Client Authentication Guide. - Rollback: keep each enforcement change behind a per-client toggle so a broken integration can be exempted temporarily, with an expiry date, rather than reverting the control globally.
Decision guidance: a small organisation using a hosted IdP mostly needs to configure it correctly (disable legacy grants, enforce PKCE) and fix its own clients. A large organisation running its own authorisation servers owns every row of the audit table and should treat the BCP as a release gate. Regulated sectors that need more than the BCP offers should adopt a profile that turns the recommendations into requirements, such as the FAPI 2.0 Security Profile Implementation Guide.
Detection and measurement
- Authorisation server logs: redirect URI mismatches, requests without PKCE,
plainchallenges, code redemption failures and double redemptions, refresh token reuse events, and any attempt atgrant_type=password. - Resource server logs: audience mismatches, DPoP proof failures, certificate binding failures and tokens presented from unexpected networks.
- Client telemetry:
issmismatches andstatefailures, which can indicate active mix-up or CSRF attempts.
| Metric | Target |
|---|---|
| Clients using implicit or password grants | Zero |
| Redirect URIs registered with wildcards or http | Zero (native loopback excepted) |
| Authorisation requests with PKCE S256 | 100% of public clients, rising for confidential |
| High-value APIs accepting only sender-constrained tokens | Rising to 100% |
| Refresh token reuse events investigated | 100% within one working day |
| Audit table rows passed per authorisation server | All rows, reviewed yearly |
Framework mapping
| Framework | Reference | RFC 9700 area |
|---|---|---|
| OWASP ASVS 5.0 | V10.4.1, V10.4.2, V10.4.6 | Exact redirect matching, one-time codes, PKCE |
| OWASP ASVS 5.0 | V10.2.1, V10.2.2 | Client CSRF and mix-up defences |
| OWASP ASVS 5.0 | V10.3.1, V10.3.5, V10.4.5 | Audience, sender-constrained tokens, refresh token replay |
| OWASP ASVS 5.0 | V3.4.6 | CSP frame-ancestors (clickjacking) |
| NIST SP 800-53 Rev. 5 | SC-23 Session Authenticity; SC-8 Transmission Confidentiality and Integrity | Binding of codes and tokens; transport |
| NIST SP 800-53 Rev. 5 | AC-6 Least Privilege; IA-9 Service Identification and Authentication | Token privilege restriction; client authentication |
| NIST CSF 2.0 | PR.AA-03, PR.DS-02 | Authentication of services; data in transit |
| MITRE ATT&CK | T1528, T1550.001, T1566.002 | Token theft and replay; phishing via open redirectors |
Common failure patterns
- Wildcard redirects for convenience: a
https://*.example.com/*registration for preview builds becomes the code theft path once any subdomain can host attacker content. - PKCE optional on the server: clients send a challenge, but the server does not require one, which leaves the downgrade open.
- Single-page apps still on implicit: tokens in URL fragments, in history and visible to every script on the page.
- Multi-issuer clients without iss checks: SaaS platforms that let customers bring their own IdP are the classic mix-up target.
- Long-lived bearer refresh tokens in integrations: theft from one vendor yields durable access to every customer tenant, as in the Salesloft Drift case.
- Password grant kept for a legacy tool: it trains users and systems to hand passwords to clients and bypasses MFA at the authorisation server.
Practitioner checklist
- Inventory authorisation servers, clients, grants, redirect URIs and authentication methods.
- Enforce exact redirect URI matching and remove wildcard and http registrations.
- Require PKCE with S256 for every public client and reject verifiers without a prior challenge.
- Disable the implicit and password grants at the server.
- Return
issin authorisation responses and check it in every multi-issuer client. - Restrict token audience with resource indicators and validate it at every API.
- Rotate or sender-constrain refresh tokens for public clients; revoke on reuse.
- Pilot DPoP or mTLS for high-value APIs, then make it the default.
- Set CSP
frame-ancestors, a strict Referrer-Policy and 303 redirects on the authorisation server. - Rerun the Section 2 audit table yearly and on every new client type.
Standards and references
- IETF: RFC 9700 Best Current Practice for OAuth 2.0 Security
- IETF: RFC 9207 OAuth 2.0 Authorization Server Issuer Identification
- IETF: RFC 8707 Resource Indicators for OAuth 2.0
- IETF: RFC 8705 OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens
- IETF: RFC 8414 OAuth 2.0 Authorization Server Metadata
- OpenID Foundation: FAPI 2.0 Security Profile
- OWASP: Application Security Verification Standard 5.0
Related NHI Mgmt Group resources: OAuth 2.0 and OpenID Connect Guide · OAuth 2.1, GNAP and FAPI Guide · DPoP Guide · OAuth Token Theft Incident Playbook · FAPI 2.0 Security Profile Implementation Guide