Most OAuth breaches exploit known patterns that RFC 9700 tells you how to close.
RFC 9700 (OAuth 2.0 Security Best Current Practice, January 2025) is the checklist for any serious deployment. The highlights:
- Exact redirect URI matching. No wildcards or prefix matching: open redirectors and lax matching have leaked codes and tokens countless times.
- PKCE everywhere with
S256, and ASes must reject PKCE downgrade attempts. - No implicit grant, no resource owner password grant. Use the authorization code flow.
- Mix-up attacks: a client talking to several ASes can be tricked into sending a code to the wrong one. Defend with the
issparameter in authorization responses (RFC 9207) or distinct redirect URIs per AS. - Sender-constrain or rotate refresh tokens for public clients; prefer sender-constrained access tokens.
- Restrict token privileges: audience-restricted, minimal-scope access tokens.
- Prevent clickjacking of authorization pages and CSRF with PKCE or
state.
Beyond the BCP:
- PAR (Pushed Authorization Requests, RFC 9126): the client POSTs the authorization parameters to the AS first and gets a
request_uri, so they cannot be tampered with in the browser and long requests do not hit URL limits. - RAR (Rich Authorization Requests, RFC 9396) expresses detailed permissions like "pay 45 EUR to this account" via
authorization_details. - OAuth 2.1 (draft) folds all of this into one document.
- FAPI 2.0 profiles combine PAR, PKCE, sender-constrained tokens and strict client authentication for banking-grade APIs.
Example
# Pushed Authorization Request (RFC 9126): parameters go server-to-server
curl -s https://auth.example.com/oauth/par \
-u "web-app:$CLIENT_SECRET" \
-d response_type=code \
-d client_id=web-app \
-d redirect_uri=https://app.example.com/callback \
-d scope="openid payments" \
-d state=af0ifjsldkj \
-d code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM \
-d code_challenge_method=S256
# {"request_uri":"urn:ietf:params:oauth:request_uri:6esc_11ACC5bwc014ltc14eY22c","expires_in":60}
# The browser redirect then only carries client_id and request_uri:
# https://auth.example.com/authorize?client_id=web-app&request_uri=urn:ietf:params:oauth:request_uri:6esc_11ACC5bwc014ltc14eY22c
# Callback with RFC 9207 issuer identification (check iss before redeeming the code):
# https://app.example.com/callback?code=...&state=af0ifjsldkj&iss=https%3A%2F%2Fauth.example.comRemember
- Exact redirect URI matching, PKCE S256, no implicit or password grants
- Defend against mix-up with the
issresponse parameter - PAR moves authorization parameters to the back channel
- OAuth 2.1 and FAPI 2.0 package these practices
Watch out for
- Allowing wildcard redirect URIs for convenience in production.