OAuth security best practice (RFC 9700)

OAuth, OIDC & JWT · Expert · 15 min

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 iss parameter 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.com

Remember

  • Exact redirect URI matching, PKCE S256, no implicit or password grants
  • Defend against mix-up with the iss response 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.

Check yourself

  1. What does checking the `iss` parameter on the authorization response defend against?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.rfc-editor.org/rfc/rfc9700