OAuth 2.0 is the protocol that powers "Sign in with Google," third-party API access, and delegated authorization across the modern web. It's also one of the most commonly misimplemented security protocols in production applications. Not because engineers don't understand what OAuth is — but because the specification is broad, the RFCs have evolved across multiple documents, and the difference between a secure implementation and an insecure one is often in choices that seem like minor details.
This isn't a tutorial on how OAuth works. It's a review of the authorization code flow mistakes we see most frequently in code reviews, and what to do about them.
Using the implicit flow in 2025
The implicit flow was designed for browser-only applications in an era before CORS and before the authorization code flow with PKCE was practical for single-page applications. In the implicit flow, the authorization server returns the access token directly in the URL fragment after the user authenticates — no authorization code exchange, no client secret verification.
The problems with this are well-documented: the token appears in browser history, in server logs (if the redirect URI has a server-side component), in referrer headers for outbound links, and is accessible to any JavaScript running on the page (which means an XSS vulnerability in any part of your application can steal it).
The OAuth 2.0 Security Best Current Practice (RFC 9700, replacing earlier security BCP drafts) explicitly recommends against the implicit flow and states that the authorization code flow with PKCE is the appropriate replacement for all public clients including SPAs. PKCE (Proof Key for Code Exchange, RFC 7636) allows the authorization code flow to be used without a client secret while preventing authorization code interception attacks.
If your SPA is using the implicit flow, migrate to authorization code + PKCE. The migration effort is modest; the security improvement is significant.
Missing or insufficient state parameter validation
The state parameter in the OAuth authorization request serves two purposes: it carries application state (so you can redirect the user to where they were going after auth), and it's a CSRF token for the OAuth flow itself.
When your application initiates an authorization request, it generates a random state value, stores it in the user's session, and includes it in the redirect to the authorization server. When the authorization server redirects back with the authorization code, it includes the same state value. Your application must verify that the returned state matches what was stored in the session before processing the authorization code.
If state validation is missing or uses a predictable value, an attacker can perform a CSRF attack on the OAuth flow: they initiate an OAuth flow as themselves, capture the redirect URI with their authorization code, and trick the victim's browser into completing that redirect. The result: the victim's account becomes linked to the attacker's authorization, or the victim is logged into the attacker's account — depending on how your application handles the token.
The fix is straightforward: generate a cryptographically random state value for each authorization request, store it in the session, and validate it exactly before processing the callback.
Insufficient redirect URI validation
The redirect URI in an OAuth flow is the URL the authorization server sends the authorization code to after authentication. If your OAuth client registration accepts wildcards or prefix matches for redirect URIs — or if your validation is done with substring matching rather than exact matching — an attacker may be able to redirect the authorization code to a URI they control.
For example, if your registered redirect URI is https://app.example.com/callback and your validation accepts any URI starting with https://app.example.com, an attacker can register a redirect URI of https://app.example.com.evil.com/callback and redirect the authorization code to their domain. Less obviously: open redirects within your own application domain can be chained with OAuth redirect URI validation to exfiltrate authorization codes.
The current OAuth 2.0 security BCP requires exact redirect URI matching. The authorization server is responsible for enforcing this, but your client registration should use exact URIs — no wildcards, no trailing slashes that might be inconsistently handled.
Treating access tokens as identity tokens
OAuth 2.0 is an authorization protocol, not an authentication protocol. An access token says "the bearer has been granted these permissions." It does not say "the bearer is this specific user." Using access tokens to make identity assertions — particularly using the fact that a token is valid as proof of identity — is a category error that leads to security issues.
OpenID Connect (OIDC) is the authentication layer built on top of OAuth 2.0 that provides identity tokens (id_token). If you're using OAuth for user authentication, you should be using OIDC and validating the id_token — not using the access token or the UserInfo endpoint as a substitute for proper identity verification.
We're not saying access tokens are useless for building user context. We're saying that "can I use this access token?" and "is this the user I think it is?" are different questions that require different cryptographic verification.
Not using PKCE for mobile and desktop clients
Mobile apps and desktop applications are "public clients" in OAuth terminology — they can't securely store a client secret because the application binary can be reverse-engineered by any user. Without PKCE, a public client using the authorization code flow is vulnerable to authorization code interception: another app on the same device can register the same redirect URI scheme and receive the authorization code.
PKCE (RFC 7636) prevents this by requiring the client to prove it generated the original authorization request. The client generates a random code_verifier, hashes it with SHA-256 to produce a code_challenge, includes the challenge in the authorization request, and then includes the original verifier when exchanging the authorization code for tokens. An interceptor who captures the authorization code doesn't have the verifier and can't complete the exchange.
PKCE is now recommended for all OAuth 2.0 clients, not just public clients. Using it for confidential clients (server-side applications with client secrets) provides defense in depth.
Refresh token rotation and reuse detection
Refresh tokens are long-lived credentials that allow clients to obtain new access tokens without requiring the user to re-authenticate. If a refresh token is stolen, the attacker can obtain access tokens indefinitely. Refresh token rotation — issuing a new refresh token with each access token refresh and invalidating the old one — limits the window of exposure from a stolen refresh token.
The more powerful addition is refresh token reuse detection: if an old (already-rotated) refresh token is presented, the authorization server detects that the token family has been compromised (either the legitimate client or an attacker is using a stale token) and invalidates all tokens in that family. This detection pattern is documented in RFC 9700 and is supported by most mature OAuth authorization server implementations.
If your authorization server supports refresh token rotation with reuse detection and you haven't enabled it, that's a configuration change worth making without rewriting any client code.