Is Your OAuth Callback Safe? Audit Redirect URIs, PKCE, and Login State
A working Google, Microsoft, or SSO login does not prove a secure OAuth/OIDC integration. Audit redirect URIs, PKCE, state/nonce, issuer binding, and preview environments.
The login works: the identity provider sends the browser back, the application exchanges a code, and a session is created. Functionally, everything is green. The more important security question is: Does the callback accept only the response that belongs to this exact login transaction?
RFC 9700, the current OAuth 2.0 Security Best Current Practice, deliberately treats redirect flows strictly. Redirect URIs should match pre-registered values exactly, neither clients nor authorization servers should expose open redirectors, PKCE is required for public clients and recommended for confidential clients, and multi-provider clients need a reliable way to identify which authorization server actually issued a response.
1. Redirect URIs are not a wildcard convenience zone
Configurations such as https://*.example.com/auth/callback, https://example.com/*, or “all preview URLs at our host” look convenient. They also enlarge the authentication trust zone.
RFC 9700 requires exact string matching for registered redirect URIs. The relevant exception covers variable ports on localhost redirects for native apps. For normal web applications, register concrete HTTPS callback URLs rather than broad patterns.
This matters especially for preview deployments. A cleaner model separates production, staging, and preview into different OAuth clients, or gives previews a stable server-controlled test callback. Short-lived pull-request domains should not automatically inherit the production authentication boundary.
2. The callback must not become an open redirect
Applications commonly store a post-login destination in returnTo, next, or continue. That is normal. The danger begins when a successful login can redirect to an arbitrary URL without validation.
RFC 9700 prohibits open redirectors on OAuth clients and authorization servers. A safer pattern binds the destination to the transaction before authentication starts and permits only relative paths or explicitly allowed origins.
Test values such as https://evil.example, //evil.example, and URL-parsing edge cases. They should not become valid post-login destinations.
3. PKCE matters for web applications too
PKCE was originally designed for public clients. Current guidance is broader. RFC 9700 requires PKCE for public clients and recommends it for confidential clients, explicitly noting that the guidance applies to web applications as well.
For each login, the client generates a random code_verifier, derives a code_challenge, and sends the challenge with the authorization request. The matching verifier must be presented when the authorization code is exchanged. An intercepted authorization code is therefore not useful by itself.
RFC 9700 recommends challenge methods that do not expose the verifier in the authorization request; S256 is currently the method with that property. Challenge and verifier also need to be transaction-specific. A constant value is not meaningful PKCE protection.
4. state and nonce need validation, not mere presence
An authentication library may generate state and nonce automatically. What matters is whether the callback actually validates them against the transaction that was started.
RFC 9700 requires protection against foreign or manipulated callback requests. Depending on the flow, PKCE can contribute to that protection; OpenID Connect uses nonce, while other cases use one-time CSRF tokens in state that are securely bound to the browser and transaction.
Red flags include:
- generating
statebut never comparing it, - allowing values to remain reusable after login,
- parallel login attempts overwriting each other,
- failing to validate
nonceagainst the ID Token, - placing post-login destinations in unprotected client-controlled state.
The callback should not ask “Was some value supplied?” It should ask “Does this exact value belong to an open transaction started in this browser?”
5. Multiple providers require issuer binding
Once an application supports Google, Microsoft, and perhaps enterprise SSO, it interacts with several authorization servers. It must therefore know which provider actually issued the response.
RFC 9700 requires a defense against mix-up attacks. RFC 9207 defines the iss authorization-response parameter so the client can compare the issuer with the one expected for the transaction. OpenID Connect also carries issuer information in the ID Token.
A code from provider A must never accidentally be sent to provider B's token endpoint. Token, JWKS, and UserInfo endpoints should come from trusted provider metadata, not from freely editable request values.
6. A token becomes an identity only after validation
With OpenID Connect, a JSON response from the token endpoint is not the end of the security check. Before an external identity becomes a local session, the application needs to validate the properties required by its flow, including the signature, expected issuer, audience or client ID, time validity, and nonce where applicable.
A mature authentication library can handle much of this correctly. An audit should still verify that the library is current and correctly configured, and that custom glue code has not bypassed its validation path.
A practical callback audit
Inventory each provider's client ID, environment, redirect and logout URIs, library, and trusted metadata source. Explicitly look for obsolete staging and preview callbacks.
Then test negative cases: another subdomain, extra path, different port, http instead of https, an expired preview domain, and a deceptively similar hostname should all be rejected.
Start two login flows in parallel, alter state, replay an old callback, and test whether codes or transaction values can be reused. With multiple providers, verify that every response remains bound to the expected issuer.
What Website-Pflichtencheck would inspect
An OAuth/OIDC review can inspect registered redirect and logout URIs, wildcards and stale preview destinations, HTTPS and exact redirect matching, PKCE with S256, actual state and nonce validation, open redirects, separation of production and test environments, provider and issuer binding, OIDC token validation, and error paths for code or token leakage.
The goal is not to rebuild OAuth yourself. The less custom logic that sits between a reputable provider, a maintained protocol library, and the local session, the easier the security boundary is to understand and test.
A login that works is a functional test. A login that accepts only the exact transaction that was started, the expected provider, and the allowed return destination is a stronger architecture.