Implement OAuth 2.0 securely in 2026

Fri Sep 04 2026

Implement OAuth 2.0 securely in 2026

The common OAuth review ends when the happy-path login works. And that’s the wrong stopping point. The failures that matter arrive with a forged callback, a wrong issuer, or a token accepted by the wrong API.

RFC 9700, published as BCP 240 in January 2025, updates OAuth’s threat model with practical experience and newer threats. It updates RFCs 6749, 6750, and 6819. OAuth 2.1 remains in development and is expected to incorporate these recommendations.

The June 24, 2026 draft-ietf-oauth-security-topics-update-02 extends that work to threats identified since RFC 9700. And it belongs in backlog review, not in your list of current RFC MUST requirements.

Your auth-gap tickets should begin with exact redirects, PKCE, callback CSRF binding, issuer and mix-up defense, protected storage and audience checks. And treat DPoP, mTLS and PAR as risk-based tickets. So this guide chooses the flow first, hardens the callback and tokens next, then gives you a migration and test plan.

In this article

OAuth is “working” long before it is secure

A successful login proves only that one path works. It says little about authorization-code injection, redirect abuse, issuer confusion, token replay, or leakage through browser and proxy behavior.

The standard now reflects that broader problem. But following it may break interoperability with older ecosystems. A live deployment needs staged enforcement rather than a flag-day rewrite. Provider documentation explains integration mechanics; RFC 9700 sets the security baseline.

The standard defines a fixed set of controls, but OAuth configuration alone cannot tell you whether your token replay risk justifies DPoP. Measure the data exposed, key custody, and incident-response capacity before making that call.

Refuse new integrations that use the Implicit or Password grant, wildcard redirect URIs, arbitrary callback URLs or a token exchange that permits PKCE omission. And the happy path has had its turn.

Start with the flow, not the library

For interactive users, choose Authorization Code with PKCE. Public clients MUST use PKCE; confidential clients are RECOMMENDED to use it. “Confidential” describes where a client secret can be kept. Confidential clients still benefit from PKCE.

A client secret in a mobile binary is decoration, not authentication.

Client/workloadUseMinimum protectionsDo not use
Server-side web applicationAuthorization Code + PKCEExact redirect URI, state, issuer checks, protected server-side token storageImplicit or Password grant
Native app or public clientAuthorization Code + PKCEPKCE enforcement, exact redirect rules, state, platform token storageEmbedded webviews or client secrets treated as proof
Browser applicationAuthorization Code + PKCEPKCE, callback protection, limited token exposureTokens in URLs or casual browser-readable storage
Machine-to-machine serviceClient CredentialsClient authentication, audience-restricted tokens, protected credentialsInteractive user grants
Existing legacy integrationMigrate toward the applicable rowTelemetry, staged enforcement, compatibility reviewAnother permanent exception

For a multi-tenant SaaS calendar integration, the web application is an interactive authorization-code client whether it connects to Google or Microsoft. Review its high-value API separately. A scheduled backend process calling an API without a user belongs to the Client Credentials row.

For machine-to-machine tickets, specify the authentication method your authorization server supports, require an audience-restricted token, and store the credential securely. Client Credentials identifies the grant; it doesn’t solve service authentication by itself.

Google’s web-server documentation and Microsoft’s authorization-code documentation cover provider-specific mechanics. Use those details without letting provider convenience override the security baseline.

The flow on the wire must bind the code to the client

Create a fresh, high-entropy code_verifier for every authorization request. Derive its challenge with SHA-256 and base64url encoding:

code_challenge = BASE64URL(SHA256(code_verifier))

For the calendar connection, the request sequence looks like this:

# Authorization request
GET /authorize?
  response_type=code&
  client_id=calendar-web&
  redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fcallback%2Fgoogle&
  scope=calendar.read&
  state=<random-state>&
  code_challenge=<base64url-sha256-verifier>&
  code_challenge_method=S256

# Token exchange over the back channel
POST /token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
client_id=calendar-web&
redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fcallback%2Fgoogle&
code=<authorization-code>&
code_verifier=<original-verifier>

The browser carries the authorization request and the resulting short-lived code. Your backend exchanges that code over the back channel. If the server requires redirect_uri at both stages, send the same registered value. Reject mismatches.

When the authorization request includes a PKCE challenge, the authorization server MUST reject a token request without the corresponding verifier. That closes a downgrade in which an intermediary strips PKCE from the exchange.

Keep the verifier with the pending server-side transaction. Delete it after a successful or failed exchange. Never accept a verifier from an unrelated browser session.

Google’s PKCE and DPoP guidance explains how the two controls cover different parts of the flow.

Redirect URIs are an allowlist, not a pattern

Register complete redirect URIs and compare the complete value. RFC 9700 says:

“Authorization servers MUST compare client redirection URIs exactly, except for port numbers in localhost redirection URIs of native apps.”

For example, if you register:

https://app.example.com/oauth/callback/google

the server must reject:

https://app.example.com/oauth/callback/google/tenant-admin

A wildcard redirect URI is an allowlist someone forgot to finish. Prefix and loose path matching can send a code to an unintended endpoint. PKCE binds the exchange to a verifier; redirect validation remains a separate control.

Define the comparison rule explicitly, and test every scheme, host, port, path, and query difference. Apply the localhost port exception only to native-app localhost redirects. Never accept a dynamically supplied callback URL.

An open redirect near the callback path creates a relay for sensitive responses. An endpoint accepting ?next= and forwarding anywhere has no place in the transaction path.

Peer-reviewed research on redirect-URI validation found weaknesses between intended policy and deployed validation. Use that warning to test your own implementation.

  • Compare the exact registered string. Scheme, host, port, path, and query all matter.
  • Reject wildcards and prefixes. “Starts with our domain” is not a security rule.
  • Select from registered values. Don’t let request parameters create callbacks.
  • Remove open redirects. A callback should complete the transaction and keep the user in the transaction flow.

Treat the callback as an untrusted security boundary

Your callback receives attacker-controlled input. Handle it like an API endpoint that happens to arrive through a browser.

Is PKCE enough?

Only conditionally. PKCE may substitute for state-based CSRF protection when you have confirmed that the authorization server supports PKCE. For a multi-provider deployment, state remains the safer default because it binds the response to the initiating browser transaction.

Generate a unique, non-guessable state. Bind it to the user session, compare it exactly, then consume it once. Auth0’s state guidance describes this transaction-binding purpose.

pending = load_transaction(returned_state)

if pending is missing or expired:
    reject

if pending.session_id != current_session.id:
    reject and consume transaction

if returned_state != pending.state:
    reject and consume transaction

if returned_issuer != pending.expected_issuer:
    reject and consume transaction

exchange code using:
    pending.client_id
    pending.registered_redirect_uri
    pending.code_verifier

validate issuer and audience wherever the received token format exposes
and supports those claims; otherwise use the provider's documented
introspection or validation mechanism

map identity by (issuer, subject)
consume transaction
create application session

Supporting Google and Microsoft creates a required mix-up-defense problem. Use the iss parameter defined by RFC 9207 or distinct redirect URIs for each authorization server:

/oauth/callback/google
/oauth/callback/microsoft

Persist identity as (issuer, subject). A subject value without its issuer is ambiguous.

For OIDC, verify nonce in the ID token as an additional replay and response-binding check; retain the callback protections required by your flow. state, PKCE, issuer checks, and nonce serve different jobs.

Keep codes and tokens out of places that remember or repeat them

Authorization codes can leak through browser-visible channels; access and refresh tokens introduce replay risk after issuance. RFC 9700 identifies referrer headers, browser history, resource-server leakage, and accidental credential forwarding through HTTP 307 redirects.

Keep credentials out of query strings and URLs generally. Set an appropriate Referrer-Policy. Clear sensitive callback state promptly. Inspect reverse-proxy behavior around redirects.

EnvironmentStorage approachMain boundary
Server-side web appProtected server-side datastore; client credentials in a secret managerKeep tokens out of browser responses and logs
Native mobile appAndroid Keystore, iOS Keychain, or Windows Credential LockerUse a full browser or platform OAuth library, not Android WebView or iOS WKWebView
Browser applicationPrefer a backend-for-frontend and server sessionIf tokens remain in the browser, document XSS exposure and use the provider’s supported authorization-code pattern

Avoid putting refresh or access tokens in localStorage: JavaScript running through an XSS flaw can read them. For a server-side connector, store refresh tokens in a protected datastore, restrict decryption access where encryption is used, redact tokens from logs, and revoke and delete them on disconnect.

For refresh tokens, implement rotation where supported, detect reuse, revoke on suspected compromise, and delete them when no longer needed.

Disable redirects from token requests where possible. Otherwise ensure the HTTP client cannot forward credential-bearing requests to another origin.

Decide when bearer tokens are not good enough

A bearer access token works for whoever presents it. Audience restriction narrows where a token is accepted. Sender constraint requires possession of an associated client key or certificate.

DPoP binds a token to a client-held key pair. It works only when the authorization server issues sender-constrained tokens and the resource server verifies the proofs. That means generating keys and protecting their storage. You also need signed request proofs, clock handling, replay detection, rotation and incident response.

Use this rule:

  • Ordinary, low-impact internal API: implement the baseline and audience restriction first.
  • Public client or high-value API: strongly consider DPoP.
  • Regulated or infrastructure-bound environment: evaluate DPoP against mTLS and your deployment constraints.

For the SaaS application’s high-value API, DPoP may justify its operational cost if bearer-token replay would expose sensitive tenant data. Hold the DPoP key in the backend’s protected key store. A browser-held key has a different compromise model and deserves a separate review.

PAR and sender constraint are useful security plumbing, but deploying either everywhere would turn a conditional control into ceremony.

Request less access, then survive a “no”

Requesting every permission at first login gives the client access it may not need and increases user friction. Request scopes when the user reaches the feature that needs them.

  1. Map each feature to the scopes it requires.
  2. Request baseline scopes during the initial connection.
  3. Request an elevated scope when the user activates the relevant feature.
  4. If the user denies it, disable that feature and keep the rest usable.
  5. Explain the reason before prompting again.
  6. Remove unused clients and credentials.

A calendar-read feature can remain available when calendar-write permission is refused. Don’t turn one declined scope into a failed login for the whole application.

Google says it auto-deletes OAuth clients inactive for six months. That is Google-specific operational guidance, so remove unused clients proactively rather than relying on the policy.

Add PAR where the authorization request itself deserves protection

Pushed Authorization Requests move authorization parameters through a back channel before the browser reaches the authorization endpoint. The browser then carries a short request reference instead of the full authorization payload.

Use PAR when your authorization server and client support it, and verify that the authorization request used at the front channel matches the pushed request. oauth.net identifies PAR as preferred for high-security deployments.

Treat PAR as a workload-specific control. It earns its place when request integrity, parameter exposure, or assurance requirements justify another endpoint and another operational path.

Migrate the auth gap without taking production down

Inventory first, because enforcement will expose clients that rely on deprecated behavior. Record each client, authorization server, redirect URI, grant and scope. Also record token lifetime, storage location and resource-server audience. Then classify clients as public, confidential, native, browser, or machine-to-machine.

Baseline enforcement now

Reject new Implicit and Password integrations. For existing clients, introduce exact redirect registration, PKCE, verifier enforcement, state, issuer validation, and mix-up defenses. Move secrets and tokens into protected storage, then add audience restriction.

Give every change an owner, failure metric, rollback switch, and retirement date. Enforce first for new clients, then migrate existing clients by cohort.

Conditional hardening next

Evaluate DPoP or mTLS for sender-constrained tokens. Add PAR where the authorization request warrants protection. These controls depend on the workload, provider support, key custody, and the resource server’s ability to verify what it receives.

Evidence before enforcement

Stage each rule with telemetry for legacy failures. A client that fails after exact redirect enforcement needs an owner and migration path. Silent exceptions become debt. Compatibility that requires accepting an attack is debt with an uptime budget.

The June 2026 -02 draft belongs in this backlog review. It is an IETF draft, not a final RFC. Use it to prepare tests and future tickets, not to present its proposals as current mandatory requirements.

Test the negative paths and the login button

Build the review from the attack-and-mitigation pairs in the standard. Test:

  • altered or unregistered redirect URI;
  • prefix, wildcard, and path-confusion matching;
  • callback open redirect;
  • missing, mismatched, reused, or guessed state;
  • wrong or absent issuer;
  • authorization-code exchange without a verifier;
  • wrong verifier and verifier reuse;
  • swapped authorization code;
  • token with the wrong audience;
  • tokens and codes appearing in query strings;
  • callback responses lacking an appropriate Referrer-Policy;
  • refresh-token replay and reuse detection;
  • token-endpoint redirects that forward credentials through a 307;
  • multi-authorization-server mix-up;
  • public-client PKCE bypass.

For the redirect test, expect rejection before code delivery. The callback test should fail before session creation. Token tests should make the server or resource server reject the value and record the event without logging the credential.

Review the June 24, 2026 IETF draft update for emerging threats. It remains a draft.

Before rollout, require recorded failures for an unregistered redirect, a missing or wrong verifier, mismatched state, wrong issuer or audience, and a replayed refresh token. Block release until those five tests fail in CI or an equivalent security-test environment.