privileged.id

OAuth 2.0

OAuth security mistakes (RFC 9700)

A plain-English, section-by-section summary of the current OAuth security best practice, with the exact requirement strength of each rule.

Reviewed 11 min read 7 primary sources

RFC 9700, "Best Current Practice for OAuth 2.0 Security" (BCP 240, January 2025), is the document that says how OAuth should be deployed today. It updates RFC 6749, RFC 6750 and RFC 6819, deprecates the implicit and password grants, and turns a decade of real attacks into requirements. Its own introduction says following it "may break interoperability" with older deployments, and still recommends upgrading "as soon as feasible".

This page lists the mistakes RFC 9700 addresses, each with the section and the exact requirement strength. MUST and MUST NOT are absolute; SHOULD and SHOULD NOT mean you need a specific, documented reason to deviate; MAY is optional. The section numbers link to the RFC.

Summary table

#MistakeSectionRequirement
1Pattern or prefix matching of redirect URIs2.1, 4.1.3AS MUST use exact string matching
2Open redirectors on client or AS4.11MUST NOT expose
3No CSRF protection on the redirect endpoint4.7Clients MUST prevent CSRF
4No mix-up defense with several authorization servers4.4Defense REQUIRED; iss SHOULD
5Codes and state leaking through Referer4.2No third-party content SHOULD NOT
6Tokens in URLs and browser history4.3Not in query MUST NOT
7Loose postMessage origins4.17Exact origins MUST; no *
8No PKCE (code injection, code theft)2.1.1, 4.5Public MUST; confidential RECOMMENDED
9PKCE downgrade accepted4.8AS MUST mitigate
10Using the implicit grant2.1.2SHOULD NOT
11Using the password grant2.4MUST NOT
12Unconstrained (pure bearer) access tokens2.2.1Sender-constrain SHOULD
13Unprotected refresh tokens for public clients2.2.2, 4.14Rotate or sender-constrain MUST
14Over-privileged, all-audience access tokens2.3Restrict SHOULD; RS refuse MUST
15Resource servers mishandling tokens4.9Treat as secrets MUST
16Weak or no client authentication2.5Enforce SHOULD; asymmetric RECOMMENDED
17Client IDs that collide with user IDs4.15SHOULD NOT allow; else MUST disambiguate
18Authorization pages that can be framed4.16AS MUST prevent clickjacking
19307 redirect after a credential POST4.12MUST NOT use 307; 303 SHOULD
20Plain HTTP; trusting proxy headers2.6, 4.13No unencrypted responses MUST NOT; sanitize MUST
21CORS on the authorization endpoint; no metadata2.6CORS MUST NOT; metadata RECOMMENDED

RFC 9700 also states an attacker model (§3): web attackers, network attackers, attackers who can read the authorization response (A3) or the request (A4), and attackers who obtain an access token (A5). It says implementers MUST take into account all possible types of attackers in their environment. The mistakes below are what those attackers exploit.

Redirects and the browser

1. Loose redirect URI matching

When authorization servers allowed redirect URI patterns (wildcards in the host, prefix matches on the path), attackers found URIs that matched the pattern but pointed somewhere they could read, and harvested codes and tokens. §2.1: authorization servers MUST use exact string matching against pre-registered URIs. The single exception is the port number of loopback redirects for native apps, where the server MUST allow a variable port (§4.1.3, following RFC 8252 §7.3). If the origin and integrity of the request are verified, for example with PAR or signed request objects plus client authentication, the server MAY trust the redirect URI without further checks.

2. Open redirectors

A URL like https://client.example/go?to=https://attacker.example on the client's domain lets an attacker build a redirect URI that passes registration checks and then forwards the code (or, with fragments, the token) to them. §2.1 and §4.11: clients and authorization servers MUST NOT expose open redirectors. Authorization servers can become open redirectors too: a malicious client can send a deliberately invalid or declined request and have the user forwarded to its phishing page. The AS MUST take precautions, MUST authenticate the user before redirecting (except for silent authentication), and SHOULD only redirect automatically to URIs it trusts (§4.11.2).

3. No CSRF protection

An attacker sends the victim's browser to the client's redirect URI with the attacker's own code, so the victim's session ends up linked to the attacker's account (for example, saving data into it). §2.1: clients MUST prevent CSRF. Clients that have made sure the AS supports PKCE MAY rely on it; OpenID Connect's nonce also provides it; otherwise one-time CSRF tokens in state, bound to the user agent, MUST be used. Before relying on PKCE for this, clients MUST ensure the server supports it (§4.7.1). If state carries application data whose integrity matters, it MUST be protected against tampering and swapping.

4. No mix-up defense

A client that works with several authorization servers (say, "sign in with" several providers) can be tricked: the user picks the attacker's server, the attacker redirects to the honest server, and the client sends the honest server's code to the attacker's token endpoint. §2.1: when a client can interact with more than one AS, a mix-up defense is REQUIRED. Clients SHOULD use the iss authorization response parameter of RFC 9207 (or the iss in an ID token or JARM response, processed the same way); otherwise they MAY use a distinct redirect URI per issuer, which §4.4.2.2 says SHOULD only be used if other options are not available. Either way, the client MUST store which issuer each request was sent to, bound to the user agent, and MUST abort on a mismatch (§4.4.2).

HTTP/1.1 302 Found
Location: https://client.example/cb?code=x1848ZT64p4IirMPT0R-X3141MFPTuBX-VFL_cvaplMH58
  &state=ZWVlNDBlYzA1NjdkMDNhYjg3ZjUxZjAyNGQzMTM2NzI
  &iss=https%3A%2F%2Fhonest.as.example

Example from RFC 9207 §2.1. The client compares the decoded iss with the issuer it sent this request to, using simple string comparison.

5. Leaking codes and state through Referer

If the page at the redirect URI includes third-party images, scripts, ads, or links, the browser may send the full URL, with the code and state, in the Referer header. §4.2.4: the page rendered from the authorization response, and the authorization endpoint, SHOULD NOT include third-party resources or links to external sites. Further measures: a Referrer-Policy: no-referrer header, the code response type, PKCE or a confidential client so a leaked code is useless, single-use codes (RFC 6749 already says codes MUST be invalidated after first use), invalidating state after first use (SHOULD), and the form post response mode.

6. Tokens in URLs and browser history

URLs are stored in history, server logs and proxies. §4.3.2: clients MUST NOT pass access tokens in a URI query parameter in the way RFC 6750 §2.3 describes. Send them in the Authorization header. For codes in history, rely on replay prevention (single use, PKCE) or form post response mode (§4.3.1).

7. Loose postMessage origins

Some browser-based flows return the authorization response by postMessage instead of a redirect. §4.17.2: the AS MUST compare receiver origins by exact string matching and send messages only to the trusted client origin; wildcard origins like "*" MUST NOT be used; clients MUST check the sender's origin exactly; and all the protections of §2.1 MUST apply equally.

Codes, PKCE and grant types

8. Not using PKCE

In authorization code injection, an attacker who has stolen a code (through any of the leaks above) injects it into their own session with the legitimate client, which redeems it and logs the attacker in as the victim. Client authentication does not help, because the legitimate client does the redemption. §2.1.1: clients MUST prevent code injection. Public clients MUST use PKCE; for confidential clients PKCE is RECOMMENDED; confidential OpenID Connect clients MAY use the nonce instead, with the precautions of §4.5.3.2. The challenge or nonce MUST be transaction-specific and bound to the client and user agent. Clients SHOULD use a challenge method that does not expose the verifier: currently only S256. Authorization servers MUST support PKCE, MUST enforce the verifier when a challenge was sent, and MUST provide a way to detect PKCE support (code_challenge_methods_supported in metadata is RECOMMENDED). Details: authorization code with PKCE.

9. Accepting a PKCE downgrade

If PKCE is optional, an attacker removes code_challenge from a request on their own device, gets an unbound code, and injects it into the victim's session; the client sends its verifier, and a lax server ignores it. §4.8.2: authorization servers MUST mitigate this by rejecting a token request that contains code_verifier when the authorization request had no code_challenge. Servers that require PKCE for all clients get this for free.

10. Using the implicit grant

The implicit grant returns access tokens in the URL fragment, where they can leak (§4.1, 4.2, 4.3), be injected (§4.6), and cannot be sender-constrained, since there is no standard way to bind them at the authorization endpoint. §2.1.2: clients SHOULD NOT use the implicit grant or any response type that issues access tokens in the authorization response, unless token injection is prevented and the leakage vectors are mitigated; they SHOULD use the code response type (or another type that returns access tokens from the token endpoint, such as code id_token).

11. Using the resource owner password credentials grant

§2.4: it MUST NOT be used. It exposes the user's password to the client, widens where it can leak, trains users to type it into non-AS pages, and cannot support multi-step, two-factor or origin-bound (WebAuthn) authentication.

Tokens

12. Pure bearer access tokens

A bearer token works for anyone who holds it. §2.2.1: authorization and resource servers SHOULD use mechanisms for sender-constraining access tokens, such as mutual TLS (RFC 8705) or DPoP (RFC 9449). §4.10 notes the limit: if an attacker gets both the token and the key (for example via XSS in the client), sender-constraining is undermined, though keys held in a hardware or non-extractable store still stop use once the client is offline. See sender-constrained tokens.

13. Unprotected refresh tokens

§2.2.2: refresh tokens for public clients MUST be sender-constrained or use refresh token rotation. §4.14.2 adds that the AS MUST decide by risk assessment whether to issue refresh tokens at all, that refresh tokens MUST be bound to the consented scope and resource servers, that they SHOULD expire after client inactivity, and that the AS MAY revoke them on events such as password change or logout. If the grant is encoded inside the refresh token, its integrity MUST be protected.

14. Over-privileged access tokens

§2.3: privileges SHOULD be restricted to the minimum required; tokens SHOULD be audience-restricted to one resource server or a small set; and a resource server receiving a token not meant for it MUST refuse the request. Tokens SHOULD also be restricted to specific resources and actions. The aud claim (RFC 9068), the resource parameter (RFC 8707), scope, and authorization_details (RFC 9396) are the tools. See scopes and consent.

15. Token handling at the resource server

A counterfeit resource server can phish tokens from clients that learn API URLs at runtime, and a compromised one can replay what it receives (§4.9). §4.9.3: sender-constrained tokens and audience restriction SHOULD be used to stop replay elsewhere, and the resource server MUST treat access tokens like other sensitive secrets, not storing or transferring them in plaintext. Log token hashes or IDs, never tokens.

Clients and authorization servers

16. Weak client authentication

§2.5: authorization servers SHOULD enforce client authentication where it is feasible to issue and protect client credentials, and asymmetric methods (mutual TLS, or private_key_jwt per RFC 7523) are RECOMMENDED, so the server holds no symmetric secrets that could leak. See client authentication methods.

17. Client identifiers that look like users

If a resource server cannot distinguish a token issued to a client on its own behalf from one issued for a user, and clients can choose their client_id, a client can register an ID equal to some user's sub and access that user's resources. §4.15.1: authorization servers SHOULD NOT let clients influence their client_id or any claim that could be confused with a genuine resource owner, where client IDs and user identifiers share a namespace; where that cannot be avoided, they MUST give resource servers another way to tell the token types apart.

18. Clickjacking the authorization page

An attacker frames the consent page invisibly and tricks the user into clicking "Allow", or overlays a fake password field. §4.16: authorization servers MUST prevent clickjacking and SHOULD use CSP level 2 or later (frame-ancestors) on the authorization endpoint and every page used to authenticate the user or authorize the client, including device authorization, login and error pages. Because older browsers lack CSP, it SHOULD be combined with other techniques such as X-Frame-Options unless such browsers are explicitly unsupported.

19. Redirecting with 307 after a credential POST

A 307 redirect tells the browser to repeat the POST, form body and all, at the new location. If the AS answers a login form POST with a 307 to the client, the client receives the user's password. §4.12: authorization servers that redirect a request that may contain user credentials MUST NOT use 307, and if they use an HTTP redirect they SHOULD use 303 (See Other), the only status code that unambiguously forces a switch to GET.

Transport and infrastructure

20. Plain HTTP and trusting proxy headers

§2.6: authorization responses MUST NOT be sent over unencrypted connections, so authorization servers MUST NOT allow http redirect URIs except loopback redirects for native clients (RFC 8252 §7.3). End-to-end TLS between client and resource server is RECOMMENDED. Where TLS terminates at a reverse proxy (§4.13), the proxy MUST sanitize inbound requests so security-relevant headers (client IP, client certificate) cannot be injected from outside, and the link from proxy to application MUST be protected against eavesdropping, injection and replay. A proxy that passes through an attacker-supplied client-certificate header defeats certificate-bound tokens entirely.

21. CORS in the wrong place, and no metadata

§2.6: endpoints browser clients call directly (token, metadata, JWKS, registration) MAY support CORS, but CORS MUST NOT be supported at the authorization endpoint, which the client never calls directly. Publishing RFC 8414 authorization server metadata, and having clients use it, is RECOMMENDED: it enables security features automatically, avoids misconfigured endpoint URLs, and eases key rotation.

What to do

If you only do six things:

  1. Authorization code with PKCE (S256) for every user-facing client; turn off implicit and password grants.
  2. Exact redirect URI matching; no open redirectors anywhere on client or AS domains.
  3. Reject code_verifier without a prior challenge; use state anyway.
  4. Audience-restricted, least-privilege access tokens, checked by every API.
  5. Rotate or sender-constrain public clients' refresh tokens; sender-constrain access tokens where you can.
  6. Check iss (RFC 9207) if a client talks to more than one authorization server.

Then work through the table above. The OAuth flow explorer shows where in each flow these checks sit.

Sources

  1. RFC 9700: Best Current Practice for OAuth 2.0 Security, IETF. IETF Trust Legal Provisions (brief quotations with attribution).
  2. RFC 6749: The OAuth 2.0 Authorization Framework, IETF. IETF Trust Legal Provisions (brief quotations with attribution).
  3. RFC 7636: Proof Key for Code Exchange by OAuth Public Clients, IETF. IETF Trust Legal Provisions (brief quotations with attribution).
  4. RFC 9207: OAuth 2.0 Authorization Server Issuer Identification, IETF. IETF Trust Legal Provisions (brief quotations with attribution).
  5. RFC 8705: OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens, IETF. IETF Trust Legal Provisions (brief quotations with attribution).
  6. RFC 9449: OAuth 2.0 Demonstrating Proof of Possession (DPoP), IETF. IETF Trust Legal Provisions (brief quotations with attribution).
  7. RFC 8414: OAuth 2.0 Authorization Server Metadata, IETF. IETF Trust Legal Provisions (brief quotations with attribution).