OAuth 2.0 and OIDC Attacks: The Flaws That Give Away Your Account

OAuth 2.0 and OIDC Attacks: The Flaws That Give Away Your Account

The most common failures of OAuth 2.0 and OIDC: weak validation of redirect_uri, problems with state, attacks against PKCE, theft and reuse of tokens, and modern security measures to prevent them.

When an application offers “Sign in with Google”, Microsoft, or another identity provider, several protocols are usually working underneath. OAuth 2.0 allows access to resources to be delegated, while OpenID Connect (OIDC) adds an identity layer that allows an application to determine who has authenticated.

The result looks simple to the user: click a button, authenticate, and return to the application. Underneath, however, redirects, temporary codes, tokens, client identifiers, session states, cryptographic keys, and multiple trust boundaries are involved.

An error in any of those relationships can turn a legitimate authentication flow into code theft, session injection, token reuse, or even access to another person's account. And in many cases, the problem is not OAuth as a protocol, but how the application has integrated it.

In this guide, we are going to review the flaws worth looking for during an OAuth/OIDC assessment, what PKCE actually protects, why state and nonce are not exactly the same thing, how an ID Token should be validated, and what is changing with OAuth 2.1.

Note: the domains and flows used in the examples are illustrative. Any testing must only be performed against applications you own or systems for which you have authorization.

In this guide


OAuth and OIDC in 60 seconds

OAuth 2.0 is an authorization framework. It allows an application to obtain limited access to certain resources without needing the user's credentials.

OpenID Connect adds an authentication layer on top of OAuth 2.0. It is the part that allows the question “who is this user?” to be answered and provides, among other things, the ID Token.

In a modern Authorization Code Flow, greatly simplified, this is what happens:

1. User → application
   "I want to sign in"

2. Application → Authorization Server
   client_id + redirect_uri + state/nonce + code_challenge...

3. Authorization Server → user
   authentication + consent

4. Authorization Server → redirect_uri
   authorization code

5. Application → Token Endpoint
   authorization code + code_verifier

6. Token Endpoint → application
   access token + ID token + optionally refresh token

The browser sees the authorization code during the redirect, but the exchange for tokens is subsequently performed against the token endpoint. PKCE also adds proof that binds that code to the client instance that initiated the operation.

Element What it is used for Common mistake
authorization code Temporary credential exchanged for tokens. Allowing it to be intercepted, injected, or redeemed outside its transaction.
access token Authorizes access to a Resource Server. Treating it as universal proof of identity or accepting it for the wrong audience.
ID Token In OIDC, it contains verifiable information about the authentication and the user. Failing to validate signature, issuer, audience, expiration, or nonce.
refresh token Allows new access tokens to be obtained. Allowing indefinite replay if it is stolen.
An access token and an ID Token are not interchangeable. The former is intended to authorize calls to resources; the latter is the identity artifact defined by OpenID Connect.

Flaw 1: insufficient redirect_uri validation

The redirect_uri parameter determines where the Authorization Server will send the browser after authorization has been completed. In the Authorization Code Flow, that redirect normally carries the authorization code.

This makes its validation a critical security boundary.

The problem appears when the server uses flexible rules instead of comparing the complete registered URI: wildcards, partial hostname checks, string prefixes, or incorrectly constructed expressions.

# Registered URI
https://app.example.com/oauth/callback

# Incorrect validation might accept something like:
https://app.example.com.attacker.example/oauth/callback
                              ↑ the actual domain is no longer example.com

Problems also arise when overly broad patterns are registered to allow multiple subdomains or paths. The more logic the Authorization Server has to interpret in order to decide whether a redirect is valid, the greater the chance of introducing differences between what the developer intended and what the parser actually accepts.

Current security practice documented in RFC 9700 is clear: registered redirects must be compared using exact string matching, except for the specific exception involving dynamic ports in certain loopback redirects used by native applications.

What about open redirects?

Another interesting piece is finding an open redirect within a domain involved in the flow.

# Legitimate endpoint vulnerable to open redirect
https://app.example.com/redirect?next=https://destination.example

Depending on how the OAuth client has been configured, the response type, and how parameters or fragments are propagated, an open redirect can help move OAuth credentials out of the location where they should remain.

That is why RFC 9700 not only requires strict redirect URI validation: it also states that clients and Authorization Servers should not expose open redirectors that allow the browser to be sent to arbitrary destinations.

Do not assume that belonging to the same domain makes a URL safe. In OAuth, what matters is the complete registered URI and the actual destination the browser will ultimately reach.

Flaw 2: CSRF, state, and sessions that are not properly bound

During an OAuth flow, there are two different operations that must remain related: the request initiated by the application and the response that later returns to the callback.

Traditionally, state is used to bind both operations and protect the callback against CSRF.

# The application starts the flow
state = random_value_bound_to_browser_session

# Later, in the callback
received_state == expected_state
                  ↑ the response belongs to this transaction

A constant, predictable, reusable state, or one that is not properly associated with the session, loses much of its usefulness.

Without this binding, an attacker can attempt to introduce into another person's browser an OAuth response belonging to a different transaction. Depending on the functionality, the result can be login CSRF, incorrect account linking, or other forms of session confusion.

So is state always mandatory?

There is an important nuance here. Current recommendations do not simply say “state must always exist”.

RFC 9700 allows a client to rely on the CSRF protection provided by PKCE when it has verified that the Authorization Server actually supports and enforces it. In OpenID Connect flows, nonce can also provide protection against certain forms of CSRF and injection.

If those guarantees do not exist, a one-time CSRF token transported through state and securely bound to the browser must be used.

In addition, if state is used to transport application information — for example, the page to return to after login — that information must also be protected against tampering.

state, nonce, and PKCE are all related to transaction integrity, but they are not simply three names for the same control. Each mechanism protects different properties of the flow.

Flaw 3: incorrectly implemented PKCE and downgrade attacks

PKCE, originally defined in RFC 7636, was created to protect public clients against authorization code interception, but current best practices recommend using it much more broadly.

The client creates a random code_verifier and sends a transformation of that value, called the code_challenge, in the initial request.

# Start of the flow
code_verifier  = random_high_entropy_value
code_challenge = BASE64URL(SHA256(code_verifier))

# Authorization request
code_challenge=...
code_challenge_method=S256

# Subsequent token request
code=...
code_verifier=...

The Authorization Server preserves the relationship between the code and the challenge. When it later receives the code_verifier, it checks that it corresponds to the original challenge.

The result is important: stealing only the authorization code is no longer enough to redeem it.

PKCE downgrade

An Authorization Server may advertise PKCE support while implementing it as optional. This introduces a delicate situation if the presence of code_challenge is the only thing that determines whether PKCE is applied.

RFC 9700 specifically describes downgrade attacks related to PKCE. One of the inconsistencies the server must detect is receiving a code_verifier later for an authorization request that never had an associated code_challenge.

# Consistent state
authorization request: code_challenge = ABC
token request:         code_verifier  = value_that_produces_ABC
→ validate

# Inconsistent state
authorization request: no code_challenge
token request:         code_verifier = XYZ
→ reject

It is therefore not enough for a platform to say that it “supports PKCE”. You need to verify that it applies PKCE correctly throughout the entire transaction.

Current recommendations also use S256 as the secure method for deriving the challenge. The plain method does not provide the same properties when an attacker can observe the authorization request.


Flaw 4: authorization code injection and reuse

PKCE also helps protect against another problem: authorization code injection.

Instead of simply attempting to steal a code, the attacker tries to introduce a code obtained from another transaction into the application's legitimate session.

Session A
code_A ↔ challenge_A

Session B
code_B ↔ challenge_B

Attempt to mix transactions
session B receives code_A

code_verifier_B does not correspond to challenge_A → reject

For this to work correctly, the code_verifier must be specific to each operation and bound to the session that initiated the flow.

Authorization codes should also be short-lived, associated with the corresponding client, and single-use. The token endpoint is a second security boundary: properly protecting the authorization endpoint alone is not enough.

During an assessment, it is worth observing what happens when you attempt to reuse an already redeemed code, modify the client_id, change the redirect_uri during redemption, or mix artifacts obtained from different sessions.


Flaw 5: incorrect ID Token validation

When OpenID Connect is involved, another artifact appears: the ID Token. It is normally a signed JWT containing information about the issuer, the user, the audience, and the authentication event.

Receiving a JWT with the correct structure does not mean that it is valid for your application.

Important checks include:

  • Signature: it must be cryptographically validated using the keys corresponding to the expected provider.
  • iss: the issuer must exactly match the provider trusted by the application.
  • aud: the application must be among the audiences for which the token was issued.
  • exp: the token must not have expired.
  • nonce: when one was sent in the request, it must be present and correspond to the transaction that was initiated.
  • azp: in certain cases involving multiple audiences, the Authorized Party must also be checked.
# Conceptual validation
verify_signature(id_token, expected_provider_keys)

assert id_token.iss == expected_issuer
assert client_id in id_token.aud
assert current_time < id_token.exp
assert id_token.nonce == expected_nonce

One particularly interesting mistake appears when an application verifies that a token has a valid signature but fails to validate its audience correctly.

A token may have been legitimately issued by Google, Microsoft, or another provider and still not be intended for your application. Authenticity of the issuer does not replace validation of the intended recipient.

“The token is authentic” and “the token is valid for my application” are two different checks.

UserInfo must also be related to the authenticated identity

If the application uses the OpenID Connect UserInfo endpoint, there is another important validation: the sub claim returned in the UserInfo response must exactly match the sub from the ID Token used for that session.

Otherwise, the application could end up associating information from one user with the authentication of another.


Flaw 6: OAuth mix-up when multiple providers are involved

Applications that allow authentication against multiple Authorization Servers have an additional trust boundary that is sometimes overlooked: remembering which provider each transaction started with.

Imagine an application that supports several providers:

Provider A → accounts.identity-a.example
Provider B → login.identity-b.example

In a mix-up attack, the objective is to make the client confuse which provider initiated the process and end up sending a code or credential to the wrong endpoint.

RFC 9207 introduced the iss parameter in OAuth responses specifically so that the client can explicitly identify which Authorization Server produced the response.

When an application interacts with multiple Authorization Servers, it must verify that identity.

# The flow was initiated with:
expected_issuer = https://identity-a.example

# The response declares:
iss = https://identity-b.example

→ abort the transaction

Using different redirect URIs for each provider can serve as an alternative defense in certain architectures, but explicitly identifying the issuer avoids relying solely on URL structure.


Flaw 7: account linking based only on email

One of the most interesting flaws does not necessarily occur inside the protocol itself, but in the logic the application builds on top of it.

Suppose the application receives this profile:

{
  "iss": "https://identity.example",
  "sub": "248289761001",
  "email": "[email protected]",
  "email_verified": true
}

A quick implementation might search for a local account by email and, if one exists, automatically log in to it or link the new identity.

The problem is that OpenID Connect does not define email as a stable or unique user identifier. It can change, it can be reused, and different issuers can apply different policies.

OIDC defines the stable identifier as the combination:

(iss, sub)

https://identity.example + 248289761001

The sub identifies the subject within the issuer's context and is designed precisely so that the Relying Party can distinguish identities in a stable way.

If a new OAuth/OIDC identity needs to be linked to an existing account, the account-linking process should include additional controls: an authenticated session, reauthentication where appropriate, explicit confirmation, and adequate proof-of-ownership checks.

email_verified=true indicates that the provider verified that email according to its procedures. It does not turn the email address into a universal, immutable, or exclusive identifier.

Flaw 8: token leakage and reuse

Even with a properly constructed OAuth flow, tokens are still credentials. A traditional bearer access token can be used by whoever obtains it while it remains valid and the Resource Server accepts it.

Leakage through URLs and Referer

RFC 9700 documents that sensitive information related to an OAuth response can leak through the Referer header, especially when a page involved in the flow loads external resources or subsequently directs the user to another location.

The callback page should be particularly minimal:

  • process the authorization code immediately;
  • avoid unnecessary third-party scripts, analytics, images, or other external resources;
  • avoid leaving credentials unnecessarily in the URL;
  • apply a restrictive referrer policy where appropriate;
  • subsequently redirect to a clean URL.

Tokens inside the browser

Another common mistake is treating localStorage as a security boundary.

It is not a security boundary against JavaScript executing in the same origin. If an XSS vulnerability exists or a script included by the application is compromised, that code can access the storage mechanisms available to the application itself.

RFC 10017, published in August 2026 as a Best Current Practice for OAuth applications running in the browser, specifically analyzes these threats and different architectures for reducing token exposure.

One of these is the Backend for Frontend (BFF) pattern: the backend acts as the confidential OAuth client, stores the access and refresh tokens, and exposes a cookie-based session to the browser. This means OAuth tokens do not need to be directly available to the application's JavaScript.

# Browser application handling tokens directly
Browser JavaScript → access token → API

# BFF pattern
Browser → session cookie → BFF → access token → API
                            ↑ token remains on the backend

Cookies in a BFF architecture must be configured correctly — for example, with Secure and HttpOnly — and the backend still needs protection against CSRF. Replacing tokens with cookies without reviewing the threat model does not automatically solve the problem.

Limiting the damage if a token is stolen

Current best practices combine several defenses:

  • audience restriction: the token should only be usable against the Resource Server for which it was issued;
  • minimal scopes: grant only the privileges that are necessary;
  • limited lifetime: reduce the useful window of a leaked token;
  • sender-constrained tokens: bind the token to the client that can prove possession of a key.

Sender-constraining mechanisms include DPoP and access tokens bound through mutual TLS.

A traditional bearer token works because whoever possesses it can present it. With DPoP, in addition to the token, the client must prove possession of the associated private key.

Traditional bearer token
stolen token → potentially reusable

Token bound using proof-of-possession
stolen token + no private key → replay blocked

This significantly reduces certain replay scenarios, although it does not make an XSS vulnerability harmless if malicious code can perform actions within the legitimate context of the application itself.


Refresh tokens: the long-lived target

An access token usually has a relatively short lifetime. A refresh token can allow new access tokens to be obtained for much longer, which means its compromise has a different impact.

For public clients, RFC 9700 requires mechanisms capable of detecting or preventing refresh token replay through strategies such as:

  • sender-constrained refresh tokens, cryptographically bound to a client instance; or
  • refresh token rotation, issuing a new refresh token each time the previous one is used.

With rotation, the server keeps enough information to detect when an old refresh token appears again.

Legitimate use
RT1 → access token + RT2
RT1 becomes invalid

RT2 → access token + RT3

RT1 appears again later
RT1 → token reused: possible compromise

The server does not necessarily know initially whether the party reusing the token was the attacker or the legitimate client, but it can detect that two parties are trying to use the same credential chain and invalidate the associated authorization.

Refresh token management must also take into account expiration, revocation, inactivity, and relevant events such as logout or significant changes to the authorization relationship.


What really changes with OAuth 2.1

An important clarification is necessary here: as of September 2026, OAuth 2.1 is still an IETF Internet-Draft and not a final RFC.

The active version in September 2026 is draft-ietf-oauth-v2-1-16. The objective of OAuth 2.1 is not to invent a completely different protocol, but to consolidate into a modern specification many security practices learned over years of OAuth 2.0 deployments.

Area Original OAuth 2.0 Direction of OAuth 2.1
Authorization Code PKCE was later defined as an extension. PKCE becomes part of the normal operation of the Authorization Code Flow.
redirect_uri Historical implementations have allowed different behaviors. Exact comparison against registered URIs, with specifically defined exceptions.
Implicit Grant Defined in OAuth 2.0. No longer part of the framework.
Resource Owner Password Credentials Included in OAuth 2.0. No longer part of the framework.
Refresh tokens in public clients Less prescriptive. Modern replay protections are incorporated.

From a penetration-testing perspective, the important point is that there is no need to wait for OAuth 2.1 to become an RFC before expecting many of these properties. RFC 9700 already represents the Best Current Practice for OAuth 2.0 deployments.

OAuth 2.1 reduces historically problematic configurations, but it cannot by itself prevent implementation mistakes such as incorrectly validating an ID Token, confusing issuers, designing account linking incorrectly, or trusting information that does not actually prove what the application assumes it proves.

How to audit an OAuth/OIDC flow

A useful assessment should not be limited to modifying a single parameter. First, reconstruct the complete transaction and understand which component trusts which other component.

Area What to review
Redirect URI Exact matching, wildcards, subdomains, normalization, paths, and open redirects.
PKCE Use of S256, whether it is mandatory, per-transaction binding, and behavior when parameters are inconsistent.
CSRF state, nonce, PKCE, and their actual binding to the session that initiated the flow.
Authorization code Lifetime, single use, correct client, and inability to inject it into another session.
ID Token Signature, iss, aud, exp, nonce, and, where appropriate, azp.
UserInfo Matching the sub with the identity authenticated through the ID Token.
Multiple providers Issuer identification and resistance to mix-up attacks.
Account linking Use of (iss, sub), trust placed in email, and the process for linking new identities.
Access tokens Audience, scopes, lifetime, storage, logs, and replay possibilities.
Refresh tokens Rotation, sender-constraining, expiration, revocation, and reuse detection.
Callback External scripts, analytics, Referer, browser history, and rapid removal of credentials from the URL.

It is also worth analyzing requests without depending on the interface. The browser shows the happy path, but the most interesting properties are usually found in the relationships between the Authorization Server, token endpoint, callback, Resource Server, and the application's internal logic.

One especially useful question throughout the entire assessment is:

What does this value actually prove?

client_id    → identifies the client, but is normally not a secret

access token → grants access to certain resources

ID Token     → contains identity claims for a specific audience

state        → can bind the response to the browser transaction

nonce        → binds the ID Token to a previously initiated authentication

PKCE         → binds the authorization code to whoever knows the verifier

iss          → identifies the issuer that produced the response or token

sub          → identifies the subject within the context of an issuer

Understanding those properties is much more useful than memorizing a list of parameters.


What this means for offensive security

OAuth and OpenID Connect appear in web applications, APIs, enterprise environments, mobile applications, and cloud architectures. A vulnerability in these flows can therefore cross several layers of an application at the same time: authentication, authorization, business logic, and session management.

In addition, many vulnerabilities do not depend on a particularly complex payload. The work lies in understanding which artifact belongs to which transaction, which server should accept it, and which properties must be validated before trusting it.

That reasoning starts with fundamentals such as HTTP, sessions, authentication, and authorization, which we cover in the Web eXploitation Junior (WXJ) course, and is taken further in the Web eXploitation Expert (WXE) course, where OAuth, OIDC, JWT, APIs, authentication, and business logic are part of advanced web application analysis.

The difference between finding an OAuth endpoint and finding a real vulnerability usually lies precisely there: stop looking at isolated parameters and reconstruct the entire trust boundary.

Do you want to learn how to audit authentication, APIs, and business logic beyond basic vulnerabilities?

At SixHack Academy, you can start by building a solid foundation with Web eXploitation Junior (WXJ) and continue with Web eXploitation Expert (WXE), where we go deeper into modern authentication, JWT, OAuth/OIDC, APIs, and advanced web exploitation scenarios.


Frequently asked questions

What is the difference between OAuth 2.0 and OpenID Connect?

OAuth 2.0 is primarily an authorization framework: it allows an application to obtain limited access to certain resources. OpenID Connect adds an identity layer on top of OAuth 2.0 and allows the user to be authenticated through elements such as the ID Token.

Does PKCE always replace the state parameter?

Not universally. RFC 9700 allows PKCE to be relied upon as CSRF protection when the client has verified that the Authorization Server supports PKCE correctly. state remains necessary in other situations and is still useful for maintaining application state. In OIDC, nonce also plays an important role in binding the transaction.

What is PKCE used for?

It binds an authorization code to an ephemeral secret generated by the client. Even if another party obtains the code, it should not be able to redeem it without knowing the corresponding code_verifier. It also helps prevent code injection attacks.

Is storing an access token in localStorage insecure?

localStorage does not protect a token against malicious JavaScript executing in the same origin, for example through XSS or a compromised script. In sensitive applications, architectures such as BFF can be used to keep tokens outside the browser's JavaScript context.

Does an HttpOnly cookie solve the problem?

Not by itself. HttpOnly prevents JavaScript from directly reading the cookie, but an application that uses cookies still needs to consider CSRF and other attacks in which malicious code acts through the victim's browser.

Which fields of an ID Token should I validate?

Among other checks, its signature, issuer (iss), audience (aud), expiration (exp), and, where appropriate, nonce must be verified. In certain cases, azp must also be validated. The exact validation depends on the flow and the characteristics of the client.

Can I identify an OIDC user only by their email address?

Email should not be used as a stable identifier. OpenID Connect defines Subject Identifiers through sub, interpreted within the context of the corresponding issuer. In practice, the combination of iss + sub is the appropriate reference for identifying a federated identity. Email addresses can change or be reused.

Has OAuth 2.1 officially replaced OAuth 2.0?

No. As of September 2026, OAuth 2.1 is still being developed as an IETF Internet-Draft. However, many of the measures it incorporates already represent modern OAuth security practices and appear in documents such as RFC 9700.

What are sender-constrained tokens?

They are tokens whose use is cryptographically bound to a specific client or key. Technologies such as DPoP and mutual TLS reduce the risk that a stolen access token can simply be replayed from another system.

What is an OAuth mix-up attack?

It is a class of attack that particularly affects clients that work with multiple Authorization Servers. The goal is to make the client confuse which server produced a response and end up using codes or credentials against the wrong endpoint. RFC 9207 defines the iss parameter as one of the main countermeasures.

Is OAuth only relevant to web applications?

No. OAuth and OIDC also appear in mobile applications, APIs, desktop applications, devices, and enterprise environments. The threat model changes depending on the type of client, which is why specific recommendations exist for native applications and browser-based applications.


References

← Back to Articles