# OAuth redirect flow mistakes that leak logins

> Most OAuth sign-in breaches happen at the redirect, not in the token. Prefix matching, a missing state check, no PKCE and open redirects, each with a test.

Published 2026-10-27 · https://aiskills402.com/blog/oauth-redirect-flow-mistakes

When someone signs in to your app with an OAuth provider, the browser leaves for the provider and comes back to your redirect address carrying a one-time code. Nearly every serious attack on that exchange targets the trip back: the code is sent to the wrong place, accepted from the wrong person, or read by someone who should not see it. The fixes are small and well documented in [RFC 9700](https://www.rfc-editor.org/rfc/rfc9700), the current security guidance for OAuth. This guide walks through the mistakes that still turn up in login code and gives each one a quick test.

## How the trip back is meant to work

Your app sends the browser to the provider with a client identifier, a redirect address and some extra values. The user signs in. The provider sends the browser back to the redirect address with a short-lived code. Your server then swaps that code for tokens by calling the provider directly. Three questions decide whether this is safe: does the provider return the code only to an address you control, can an attacker feed your app a code that was not meant for this browser, and can anyone who sees the code redeem it?

Each mistake below breaks one of those three answers.

## Mistake 1: matching the redirect address loosely

The provider checks the redirect address in each request against what you registered. The guidance says the provider must compare by exact string, with one allowance: native apps may differ in the port number on a localhost address ([RFC 9700, section 2.1](https://www.rfc-editor.org/rfc/rfc9700)). The reason is that pattern matching has been attacked successfully in the wild. A registration like `https://*.example.com/*`, or a check that only tests whether the address starts with your domain, lets an attacker find any page under that pattern that they control or can abuse, and receive the code there.

Where your own code does the comparing, for example in a gateway or a custom login server, the common slips are `startsWith`, `includes`, a regular expression without anchors, and a check on the host only while ignoring the path. A prefix like `https://app.example.com` also matches `https://app.example.com.attacker.test`. The test is easy to run: send a request whose redirect address differs from the registered one by one added path segment, a different subdomain, and a lookalike host suffix. All three must be refused.

## Mistake 2: leaving an open redirect on your own site

Your client may be strictly matched and still leak. If any page on your site forwards the browser to an address taken from a query parameter, an attacker can register nothing and still use it: the registered address is the page on your site, and that page bounces the code onward. RFC 9700 says clients must not expose such open redirectors, because they can be used to exfiltrate codes and tokens, and also to build links that look like yours for phishing ([section 4.11](https://www.rfc-editor.org/rfc/rfc9700)). The advice is to redirect only to targets on a list you maintain.

Check by searching your code for parameters named `next`, `return`, `url` or `redirect`, and try each with an outside address. The login callback itself is the worst place for one.

## Mistake 3: a state value that is not checked

The `state` value exists to stop forged requests to your callback. Your app creates an unguessable value, ties it to the browser session that started the login, and compares it when the browser returns. The guidance requires clients to prevent this class of request forgery, and names one-time values in `state` bound to the user agent as the baseline ([section 2.1](https://www.rfc-editor.org/rfc/rfc9700)). Clients may lean on PKCE for the same protection only when they have confirmed that the provider supports it, and OpenID Connect flows can use the `nonce` instead.

Typical failures are generating `state` and never comparing it, comparing it with a constant, using a value anyone can predict such as a timestamp, or storing it somewhere that is not tied to the visitor's browser, such as a global variable on the server. To test, begin a login in one browser, copy the callback address, and open it in another browser with a different session. A correct app refuses it. The attacker's plan, in the forged-request case, is to have the victim finish a login that the attacker started, so the victim ends up inside the attacker's account.

## Mistake 4: no PKCE on a public client

PKCE, defined in [RFC 7636](https://www.rfc-editor.org/rfc/rfc7636), adds a secret that only the starting client knows. The client creates a random verifier, sends a transformed form of it, called the challenge, with the first request, and shows the verifier itself when it redeems the code. A stolen code is then useless without the verifier. The verifier is a string between 43 and 128 characters, and the client is told to use the `S256` transformation whenever it can, with `plain` kept for constrained cases only ([RFC 7636](https://www.rfc-editor.org/rfc/rfc7636)).

RFC 9700 says public clients, which means browser apps and mobile apps that cannot hold a secret, must use PKCE, and recommends it for confidential clients too ([section 2.1.1](https://www.rfc-editor.org/rfc/rfc9700)). It matters because of code injection: an attacker who has obtained a code starts their own login with your app, swaps the new code for the stolen one in the response, and your app, doing everything correctly otherwise, binds the victim's identity to the attacker's session. With PKCE, the provider notices that the verifier does not match the challenge it stored.

To test: look at the authorization request your app builds. If it contains no `code_challenge`, or the method is `plain` when `S256` is possible, that is the gap. Also make sure the verifier is generated fresh for every login and is not a fixed string.

## Mistake 5: tokens in the address

The implicit flow returns the access token directly in the address fragment. RFC 9700 advises clients not to use it, because tokens in an address are exposed to browser history, referrer headers and injection, and recommends the code flow instead ([section 2.1.2](https://www.rfc-editor.org/rfc/rfc9700)). The related habit is passing an access token to an API as a query parameter, which also lands in history and logs. Tokens belong in a header.

## A check that can fail

Pick one real login flow and run the five tests above against a staging copy, noting your expectation and the result for every test. Tests only count if they can fail, so include a deliberate break: temporarily remove the `state` comparison and confirm that the cross-browser callback test now succeeds. If it keeps failing, your test was never measuring the state check. Our piece on the [dangers of installing unchecked skills](/blog/agent-skills-security-risks) follows the same habit of inspecting a file before trusting it.

## When this does not apply

The advice here concerns browser-based redirect logins. A machine-to-machine flow with no user and no redirect has other risks. A provider that you do not control may already enforce exact matching and PKCE for you, in which case your remaining work is on the client side only. Some native app flows legitimately vary the port, as the guidance allows. The guidance is a best current practice and changes over time, so check the latest text before relying on this page. Token storage after sign-in, refresh token handling and token verification are separate reviews and are not covered.

## Where a review helps

Reviewing a sign-in flow comes down to a handful of concrete jobs. Read the callback and the code that kicks off the login, then report whether `state` is created, bound to the session and compared; whether a public client sends a PKCE challenge; whether the redirect address is matched exactly or by prefix; and whether any token travels in an address. Safe variants should not be reported, for instance a cookie-held `state` that is verified on return, a strict equality match, a backend client holding a secret, or a token carried in a header.

That narrow review is what one of our skills does: [OAuth Redirect Flow Review](https://aiskills402.com/skills/oauth-redirect-flow-review). Hand it the source of a single sign-in flow and it returns the genuine problems, each naming the function, the damage and the repair, or states that nothing was found. You can buy it in the store at https://aiskills402.com.
