OAuth Security in 2026: RFC 9700 Checklist
OAuth 2.0 is not one frozen protocol profile. Years of deployment attacks changed what a secure implementation looks like. In January 2025, the IETF published RFC 9700, the OAuth 2.0 Security Best Current Practice, as BCP 240.
This checklist turns that document into reviewable engineering controls. It is for teams implementing an authorization server, integrating an OAuth client, or auditing an existing deployment.
Baseline profile
Start with these non-negotiable decisions:
- Use the authorization code grant.
- Require PKCE with
S256for public clients and adopt it for confidential clients. - Do not offer the implicit grant.
- Do not offer the resource owner password credentials grant.
- Match registered redirect URIs exactly.
- Prevent open redirects on authorization-server and client endpoints.
- Restrict tokens by audience and minimum scope.
- Rotate or sender-constrain refresh tokens for public clients.
If a legacy integration cannot meet this profile, document the exception, its threat model, owner, and removal date. “OAuth supports it” is not a security justification.
This baseline is intentionally stricter than the minimum wording in places. RFC 9700 says public clients MUST use PKCE and confidential clients SHOULD use it. A platform can simplify enforcement by requiring S256 PKCE for both, as NamoID does. When this article says “require” in the implementation checklist, it names that secure deployment profile; the linked RFC remains authoritative about its exact normative language.
Turn the BCP into release evidence
A standards checklist is useful only when each item maps to an enforcement point and a negative test.
| Control | Authorization server evidence | Client/resource evidence |
|---|---|---|
| Exact redirects | Registered URI comparison | Callback is fixed and not an open redirect |
| PKCE | Challenge stored with one-time code; S256 verifier checked | Fresh verifier per transaction |
| Issuer binding | iss returned and metadata consistent | Expected issuer stored and compared |
| Audience restriction | Token minted for requested resource | Resource rejects wrong aud |
| Refresh replay | Token family and reuse revocation | Rotated token stored atomically |
| Deprecated grants absent | Discovery and token endpoint reject them | Library configured for code flow |
Keep this matrix in the threat model or release checklist. “We follow OAuth best practices” is not testable; “wrong-audience access tokens receive 401 at every resource server” is.
Redirect URI checklist
Authorization servers must compare redirect URIs using exact string matching, except the narrow native-app localhost port allowance described by RFC 9700.
registered: https://app.example.com/oauth/callback
accepted: https://app.example.com/oauth/callback
rejected: https://app.example.com/oauth/callback/
rejected: https://app.example.com/oauth/callback?next=...
rejected: https://evil.example/callbackDo not allow wildcard subdomains, path prefixes, or arbitrary return_to URLs. Both the authorization server and OAuth client must avoid open redirectors because they can exfiltrate codes and tokens or create convincing phishing links.
Behind a proxy, sanitize inbound forwarding headers and reconstruct redirects from trusted configuration. An attacker-controlled X-Forwarded-Host must not decide where credentials or codes are sent.
Apply the same exact-match rule to post-logout redirects and native hosted-auth return URLs, even when those are not the OAuth authorization redirect itself. Separate allowlists by purpose. A login callback, logout destination, browser origin, and webhook URL are not interchangeable just because each is a URL.
PKCE checklist
Public clients must use PKCE. RFC 9700 recommends it for confidential clients too, and notes that the advice applies to web applications—not only native apps.
- Generate a fresh high-entropy verifier for every transaction.
- Send only its
S256challenge in the authorization request. - Bind the challenge to the authorization code.
- Require the verifier at the token endpoint.
- Reject
plain, missing, replayed, and downgraded verifiers. - Advertise only supported methods in authorization-server metadata.
PKCE protects code interception and injection. It can also provide CSRF protection when the client has ensured the authorization server supports it. When using state, bind the value to the browser transaction and make it one-time.
Do not treat PKCE as a client authentication method. A public client remains public, and a copied client_id remains expected. PKCE proves that the party redeeming this authorization code holds the transaction-specific verifier; it does not make a browser capable of keeping a shared client secret.
Read how PKCE works for the full exchange.
Issuer and mix-up checklist
A client that talks to more than one authorization server must defend against authorization-server mix-up. RFC 9700 recommends the iss authorization response parameter defined by RFC 9207, or another issuer value with equivalent validation.
- Record which issuer started the transaction.
- Require the returned issuer to match exactly.
- Fetch tokens from that issuer's configured token endpoint.
- Validate ID-token and access-token issuer claims.
- Never choose the token endpoint from untrusted callback parameters.
NamoID's OAuth mix-up attack guide walks through the failure mode.
Discovery must not weaken the issuer anchor. Begin with a trusted issuer identifier, fetch its metadata over TLS, and verify that the returned issuer is exactly the expected value. Do not accept a callback parameter that tells the client which discovery document or token endpoint to use.
Token restriction checklist
An access token should carry no more authority than its caller needs:
- Restrict
audto one resource server or a small explicit set. - Restrict scopes to the authorized operation.
- Keep access-token lifetime short.
- Validate issuer, audience, expiry, signature, and token type at every resource server.
- Never accept an ID token where an access token is required.
- Do not place access tokens in URLs.
Audience checks stop a token issued for one API from being replayed at another. Follow the complete JWT validation order when tokens are self-contained JWTs.
RFC 9700 recommends sender-constrained access tokens such as mTLS or DPoP where replay risk justifies them. Sender constraint is a stronger replay control, not a substitute for correct issuer, audience, signature, expiry, scope, and token-type validation.
When the client uses the resource parameter from RFC 8707, validate it as an absolute resource identifier and bind the resulting access token to that audience. Never let an untrusted resource value become an arbitrary network destination for server-side requests.
Refresh token checklist
For public clients, refresh tokens must be sender-constrained or rotated. With rotation:
- Every refresh returns a new refresh token.
- The previous token becomes invalid.
- The server retains the token-family relationship.
- Reuse of an invalidated token revokes the active family.
- The legitimate user must authenticate again.
Also bind refresh tokens to the consented scope and resources, expire inactive tokens, and consider revocation after password changes, logout, or other security events.
Rotation does not identify whether the attacker or legitimate client presented the reused token. It contains the breach by invalidating the family. The refresh-token rotation guide covers the data model and race conditions.
Handle legitimate concurrency explicitly. Two browser tabs can refresh at nearly the same time. Your design may use a very narrow, well-defined overlap or a single-flight client refresh, but it must not silently permit unlimited reuse. Record the family, parent, rotation time, and revocation reason without logging the token itself.
Client authentication checklist
Never treat a secret embedded in a browser, mobile binary, desktop package, or distributed CLI as confidential. Public clients cannot keep a shared secret.
For confidential clients:
- Use a strong credential appropriate to the deployment.
- Bind issued credentials to one client.
- Rotate secrets without downtime.
- Do not log credentials or place them in URLs.
- Prefer asymmetric client authentication when operationally justified.
Publishable browser keys are identifiers with limited authority, not OAuth client secrets. If a value ships inside JavaScript, a mobile binary, or a distributed CLI, design as though an attacker already has it.
Authorization request checklist
- Request only necessary scopes and resources.
- Bind transaction data to the user agent.
- Reject duplicate security-sensitive parameters.
- Prevent credentials entered at the authorization endpoint from leaking through redirects or referrers.
- Use pushed authorization requests or signed request objects when the threat model requires request integrity and confidentiality.
- Require user consent where appropriate and make the requested authority understandable.
Do not let convenience parameters silently expand scope, audience, or redirect authority.
Reject duplicate security-sensitive parameters rather than choosing the first or last value. Different parsers may resolve duplicates differently, enabling an attacker to make the authorization server validate one value while a downstream component acts on another.
Use PAR or signed request objects when your threat model needs stronger request integrity, confidentiality, or policy enforcement. They reduce front-channel tampering, but the server still needs client authentication, redirect validation, consent, and token controls.
Browser and response checklist
- Use TLS everywhere outside explicit local development.
- Set authorization responses and token-bearing pages to
Cache-Control: no-store. - Keep tokens out of browser-readable storage when a backend session is available.
- Apply a restrictive CSP to hosted authorization pages.
- Use
HttpOnly,Secure, and deliberateSameSitesettings for cookies. - Render safe recovery pages for expired or malformed browser flows; keep diagnostic details in server logs.
Security errors should fail closed without becoming account-enumeration or infrastructure-disclosure channels.
RFC 9700 also calls out redirect status codes when credentials may be present. A 307 preserves the HTTP method and body, which can forward submitted credentials to the next location. Use a redirect status that changes the follow-up to GET, such as 303, where the authorization flow redirects after credential submission.
At a TLS-terminating reverse proxy, strip untrusted inbound forwarding headers and set trusted values yourself. Protect the internal hop, authenticate the proxy where appropriate, and ensure the application cannot be reached through an alternate path that bypasses those controls.
In-browser messaging needs exact origins
If a popup, iframe, or silent flow uses postMessage, pin the receiver origin exactly. Never send security responses with targetOrigin="*". On receipt, validate both the sender origin and the expected message structure before using any authorization result.
Avoid URI prefix checks and substring checks. Parse the origin and compare scheme, host, and port to the exact registered value. Do not trust a claimed origin inside the message payload over the browser-provided event origin.
The simplest secure option is often to avoid complex in-browser communication and use a normal top-level redirect plus backend callback.
Test the attacks, not only the flow
Your automated suite should cover:
- Missing,
plain, wrong, reused, and downgraded PKCE verifiers. - Redirect URI suffix, query, encoding, and wildcard bypasses.
- Swapped issuer and token endpoints.
- Replayed authorization codes.
- ID-token-as-access-token confusion.
- Wrong audience and excessive scope.
- Refresh-token reuse and concurrent rotation.
- Open redirect attempts.
- Forged forwarding headers behind the production proxy topology.
- Unsupported implicit and password grants.
- Credential-preserving
307redirects. - Duplicate security-sensitive parameters.
postMessagewith wrong sender or receiver origin.- Client-generated subject claims that impersonate a resource owner.
Run these tests against the deployed proxy topology, not only a direct application test client. Redirect reconstruction, TLS termination, caching, and header sanitisation often live outside the framework process.
A successful login proves only that the happy path works. These negative tests prove the security profile.
NamoID's baseline
NamoID's current OAuth/OIDC profile uses authorization code flow, S256 PKCE, exact redirect registration, issuer identification, audience-aware access tokens, and refresh-token rotation with family replay revocation. Advanced sender-constrained tokens remain roadmap work.
Treat RFC 9700 as a living release gate: map each applicable requirement to code, a negative test, public documentation, and an owner. That turns a long standards document into something an engineering team can keep true after the first launch.
FAQ
Is RFC 9700 the same as OAuth 2.1?
No. RFC 9700 is the final OAuth 2.0 Security Best Current Practice, published as BCP 240 in January 2025. OAuth 2.1 is a protocol revision that incorporates many modern security recommendations. Implementations should use the final RFC 9700 as the security baseline rather than waiting for another specification.
Does RFC 9700 require PKCE for confidential clients?
It requires PKCE for public clients and recommends it for confidential clients. A platform may enforce a stricter profile and require S256 PKCE for every authorization-code flow, reducing configuration branches and protecting against code injection.
Does PKCE replace state and nonce?
Not universally. RFC 9700 explains when transaction-bound PKCE can provide CSRF protection, while OpenID Connect uses nonce and OAuth clients can use one-time state bound to the user agent. Follow the guarantees of your library and profile instead of deleting parameters mechanically.
Is refresh-token rotation enough to stop token theft?
Rotation detects reuse and can revoke the active token family, limiting persistence. It does not prevent the first use of a stolen bearer token. Sender-constrained tokens such as DPoP or mTLS address replay more directly, alongside correct token validation and secure storage.
Can legacy implicit-flow clients remain temporarily?
Sometimes migration takes time, but treat that as a documented exception with compensating controls, an owner, monitoring, and a removal date. New clients should use authorization code flow and modern browser/session handling.