notes 2026-08-21 · 5 min read

OAuth Account Takeover Chains: state, PKCE and Linking

OAuth account takeover chains: redirect URI matching, state and PKCE bindings, account linking abuse, email as identity and Mix-Up attacks.

OAuth Account Takeover Chains: state, PKCE and Linking

OAuth rarely fails because someone guessed an access token. The failure usually starts when an application trusts the right response inside the wrong session.

Authorization Code Flow contains several moving parts.

Client.

Authorization Server.

Resource Server.

Browser.

Redirect URI.

state.

PKCE.

Issuer.

Local account.

Account takeover appears when two or more of those relationships are no longer bound to the same transaction.

The examples below assume a lab or an authorized penetration test.

Parts of an OAuth flow

Authorization Code is a transition credential

A simplified flow begins when the client redirects the browser to the Authorization Server.

After authentication, the server returns an authorization code to the redirect URI.

The client exchanges that code for tokens.

1
2
3
4
5
6
7
Browser -> Authorization Server
            |
            v
        authorization code
            |
            v
Client -> Token Endpoint -> access token

The code should remain bound to the correct client, redirect URI, and transaction.

When those bindings fail, code injection and session confusion become possible.

Redirect URI matching should be exact

RFC 9700 recommends exact string matching for pre-registered redirect URIs, with a specific exception for localhost ports used by native apps.

Broad pattern matching creates dangerous combinations.

Consider a client registered with something equivalent to:

1
https://app.example.com/*

The application also has:

1
https://app.example.com/redirect?next=https://attacker.example

The Authorization Server may accept a URI that lands on an open redirect.

The code reaches the legitimate domain.

Then it is forwarded outside.

The attack chain comes from loose validation combined with the redirector.

state binds browser and transaction

state is commonly used as CSRF protection on the OAuth callback.

The client generates an unpredictable value.

It stores that value in the session.

It sends the value in the authorization request.

Then it requires the same value on the callback.

If the client accepts callbacks without state, or fails to bind it to the correct session, an attacker may inject authorization initiated from another browser.

A common effect is login CSRF or unsafe account linking.

The victim may end up logged into the attacker’s OAuth account.

Depending on the product, the victim may then store data inside that account.

PKCE binds the code to a client instance

PKCE adds a code_challenge to the authorization request.

During code exchange, the client sends a code_verifier.

The Authorization Server validates the relationship.

RFC 9700 requires PKCE for public clients and recommends it for confidential clients too.

Use S256.

A stolen code becomes far less useful when the attacker does not possess the verifier for that transaction.

Correct PKCE also helps prevent code injection.

state, PKCE, and issuer protect different bindings

Account linking is a frequent target

Consider an application with an existing local account.

The user selects:

1
Connect Google account

The callback receives OAuth identity and links it to the currently authenticated local user.

Now ask:

Is state bound to the current session?

Does linking require reauthentication?

Is the returned provider the same provider that started the flow?

Was the email verified by that provider?

Does the local account already have another external identity?

If the callback accepts authorization initiated by the attacker, the victim’s account may become linked to an external identity controlled by the attacker.

The attacker can later use Login with Google.

Email as identity requires care

Many applications receive:

1
2
3
{
  "email": "gabriel@example.com"
}

and search for a local account with the same address.

That can be safe when issuer, audience, and email_verified are validated correctly for a trusted provider.

It becomes dangerous when multiple providers are accepted and any of them can assert the same address.

It is also dangerous when the application ignores whether the provider verified that email.

A stable provider identity, usually issuer plus subject, is a stronger basis for account linking.

OAuth and OIDC solve different problems

OAuth delegates authorization.

OpenID Connect adds identity on top of OAuth.

With OIDC, the ID Token enters the flow.

iss.

aud.

sub.

nonce.

Signature.

Expiration.

Accepting an ID Token from the wrong issuer or for the wrong audience can break login even when the signature is valid.

The signature answers who signed the token.

Issuer and audience answer whether the token belongs to this trust relationship and this client.

Mix-Up matters with multiple Authorization Servers

A client may support several providers.

Google.

Microsoft.

A corporate provider.

If the application loses track of which issuer started the transaction, a response from one Authorization Server may be sent toward another server’s token endpoint.

RFC 9700 requires mix-up defenses when a client interacts with multiple Authorization Servers.

The iss parameter defined by RFC 9207 is one recommended defense.

The client needs to store which issuer started the flow and verify it on return.

Access tokens used in the wrong context create another path

Some applications accept an access token at the backend and call a userinfo endpoint.

That can be correct.

The problem appears when a token issued for another client, audience, or resource is accepted without proper validation.

A valid token is not valid for every application.

The same principle appears with JWT.

Context is part of validation.

An open redirect can become more serious inside OAuth

By itself, an open redirect may have limited impact.

Inside OAuth, it can become transport for a code or token.

This shows why findings need context.

A small configuration flaw can complete the missing step in an account takeover chain.

A testing method that works

I draw the full flow in Burp.

I mark the authorization request.

I record state, nonce and code_challenge. Then I record redirect URI, issuer and client ID.

Then I open two browser sessions.

One attacker.

One victim.

I swap callbacks.

I swap codes.

I repeat linking.

I change providers.

I test redirects.

Each test tries to break one specific binding.

OAuth testing method

Remediation

Use exact redirect URI matching.

Remove open redirectors from callback paths.

Use PKCE with S256.

Validate state or the correct equivalent for the flow.

With OIDC, validate nonce, iss and aud. Then validate signature and expiration.

Link external accounts using stable issuer identity.

Protect account linking with an explicit transaction and reauthentication when risk requires it.

When multiple Authorization Servers are supported, implement mix-up protection.

OAuth security depends on keeping every part of one transaction bound to the same context.

References

IETF, RFC 9700: Best Current Practice for OAuth 2.0 Security.

IETF, RFC 7636: Proof Key for Code Exchange.

IETF, RFC 9207: OAuth 2.0 Authorization Server Issuer Identification.

PortSwigger Web Security Academy, OAuth authentication vulnerabilities.

This post is licensed under CC BY 4.0 by the author.