OAuth 2.0 is the most misimplemented protocol in modern web development. Not because the spec is unclear — because it's flexible, and flexibility means developers make choices without realizing those choices carry security consequences. Misconfigured redirect URIs, tokens stored in localStorage, missing PKCE — these aren't theoretical issues. They're the top causes of account takeover vulnerabilities in apps that use "industry standard" authentication.
This isn't a spec walkthrough. It's a list of the specific things that go wrong in production OAuth implementations, and how to fix each one.
Always Use PKCE — Even for Confidential Clients
Proof Key for Code Exchange (PKCE) was originally designed for public clients like mobile and single-page apps that can't safely store a client secret. The updated guidance (OAuth 2.1 effectively mandates this) is to use PKCE for every authorization code flow, regardless of client type.
Why it matters
- PKCE prevents authorization code interception attacks, where a malicious app on the same device intercepts the redirect and steals the code.
- It closes the gap even if your client secret is somehow exposed, because the attacker still needs the original code verifier.
- It costs almost nothing to implement — most modern OAuth libraries (Auth0 SDKs, oauth4webapi, AppAuth) support it natively.
Implementation checklist
- Generate a cryptographically random
code_verifier(43–128 characters). - Hash it with SHA-256 to produce the
code_challenge. - Send
code_challengeandcode_challenge_method=S256in the authorization request — never use the deprecatedplainmethod. - Verify the server-side library actually validates the challenge on token exchange, not just accepts it silently.
Lock Down Redirect URIs — Exact Match, No Wildcards
Open redirect via loose redirect_uri validation is one of the most common OAuth vulnerabilities found in bug bounty reports. Attackers register a similar domain or subdomain and trick the authorization server into sending the code or token there.
- Never use wildcard or prefix matching for redirect URIs (e.g., allowing anything under
*.yourapp.com). - Register the full, exact redirect URI including path — no partial matches.
- Reject any request where the
redirect_uridoesn't exactly match a pre-registered value, including trailing slashes. - If you support multiple environments (dev, staging, prod), register each URI explicitly rather than relying on pattern rules.
Never Use the Implicit Grant Flow
The implicit flow (response_type=token) returns access tokens directly in the URL fragment. It's deprecated for good reason:
- Tokens end up in browser history, server logs (via referrer headers), and proxy logs.
- There's no way to verify the token wasn't intercepted in transit through a redirect chain.
- It offers no refresh token support, forcing awkward workarounds.
Replace it with the Authorization Code flow + PKCE, even for single-page applications. If you're maintaining a legacy app still using implicit flow, migrating it should be a near-term priority, not a "someday" item.
Token Storage: Where You Put Tokens Matters More Than the Token Format
Don't store tokens in localStorage or sessionStorage
Any XSS vulnerability on your domain — even in a third-party script you didn't write — can read localStorage and exfiltrate tokens. This is one of the most avoidable OAuth mistakes.
Preferred storage patterns
- HttpOnly, Secure, SameSite=Strict cookies for session tokens, so JavaScript can't read them at all.
- For SPAs calling APIs directly, use the Backend-for-Frontend (BFF) pattern: the browser never sees the access token — it holds a session cookie, and your backend proxies API calls with the real token attached server-side.
- Keep access token lifetimes short (5–15 minutes) and rely on refresh token rotation for longevity.
Refresh Token Rotation Is Non-Negotiable
If a refresh token is long-lived and reusable indefinitely, a single leak gives an attacker permanent access. Rotation solves this:
- Every time a refresh token is used, issue a new one and invalidate the old.
- If an already-used (rotated-out) refresh token is presented again, treat it as a signal of theft — revoke the entire token family immediately.
- Bind refresh tokens to client metadata (device fingerprint, IP range) where feasible, and flag anomalies.
Validate Scopes on Every Request — Not Just at Consent
A common mistake: scopes are checked when the user grants consent, but never re-validated when the token is actually used to call an API. This lets a token minted for read:profile quietly get used against a write:billing endpoint if the backend doesn't enforce it.
- Enforce scope checks at the resource server, not just the authorization server.
- Apply the principle of least privilege — request the narrowest scope that satisfies the use case, not broad "all access" scopes for convenience.
- Audit your scope definitions quarterly; unused broad scopes are a liability that accumulates silently.
State and Nonce Parameters Prevent CSRF and Replay
- Always include a random, unguessable
stateparameter in the authorization request, and verify it matches on callback — this prevents CSRF attacks against the OAuth flow itself. - If you're using OpenID Connect, also validate the
nonceclaim in the ID token to prevent replay attacks. - Never reuse a
statevalue across sessions or requests.
Secure the Transport Layer Around OAuth, Not Just the Protocol
OAuth security doesn't exist in isolation — it depends on the surrounding HTTP security posture. A textbook-perfect OAuth implementation is still vulnerable if:
- Your TLS configuration accepts outdated ciphers or expired certificates, letting attackers intercept the authorization code in transit.
- Your CORS policy is too permissive, allowing malicious origins to make token requests directly from the browser.
- Your cookies lack the
SecureandSameSiteattributes, undermining session protection even with rotation in place. - Missing security headers (like a strict
Content-Security-Policy) leave the door open for the XSS that makes token theft possible in the first place.
This is where a lot of teams miss the bigger picture — they harden the OAuth flow itself but never check whether the surrounding infrastructure has the same rigor. Running a scan through WebSentry is a fast way to catch exactly this class of gap: it grades your SSL/TLS setup, security headers, CSP, cookie flags, and CORS configuration, all of which directly affect how exploitable an OAuth flaw actually is in practice.
A Quick Pre-Launch OAuth Checklist
- Authorization Code flow with PKCE — no implicit grant, anywhere.
- Exact-match redirect URI validation, no wildcards.
- Access tokens stored server-side or in HttpOnly cookies, never in localStorage.
- Refresh token rotation with reuse detection.
- Scopes enforced at the resource server, reviewed regularly.
- State and nonce parameters validated on every callback.
- TLS, CORS, cookie flags, and CSP audited alongside the OAuth flow itself.
Most OAuth breaches don't come from someone breaking the cryptography — they come from a missed validation step, a loose CORS rule, or a cookie without the right flag. If you want a fast way to check whether your site's surrounding security posture is solid enough to support a hardened OAuth implementation, run a free scan at websentry.dev and get your A–F grade across SSL, headers, CSP, cookies, and CORS in minutes.
Check your own site
Run a free security scan and see if your site has the issues covered in this article. Results in under 30 seconds.
