Breaking OAuth 2.0 and OpenID Connect: Redirect, State, and Token Attacks

Security
Time it takes to read this article 5 minutes.

Disclaimer: This article is for education and authorized security testing only. Only test systems you own or have explicit written permission to assess. Unauthorized access to identity systems is illegal in virtually every jurisdiction.

Introduction / Overview

OAuth 2.0 and OpenID Connect (OIDC) are the backbone of modern delegated authorization and federated login. They power "Sign in with Google," mobile-app token exchange, and machine-to-machine APIs. Because they sit at the trust boundary between an app, a user, and an identity provider (IdP), a single misconfiguration – a sloppy redirect_uri allowlist, a missing state, an exposed token – can lead to full account takeover.

This post walks through the most common OAuth/OIDC attack classes from an attacker's perspective, then weights an equal amount of attention on detection and defense. The protocol terms we cover precisely: redirect_uri, state, PKCE, token leakage, and the deprecated implicit flow.

How it works / Background

The Authorization Code flow is the recommended grant. The client redirects the user to the IdP, the IdP authenticates the user and returns a short-lived code to the client's registered redirect_uri, and the client exchanges that code at the /token endpoint for an access_token (and, in OIDC, an id_token). The code-for-token exchange happens over a back channel, so tokens never touch the browser URL.

The Implicit flow (response_type=token) instead returns the access token directly in the URL fragment. RFC 9700 (OAuth 2.0 Security Best Current Practice) now formally deprecates it because fragments leak via browser history, Referer headers, and logs.

Key security parameters:

  • redirect_uri – where the IdP sends the response. Must be matched exactly against a pre-registered allowlist. Loose matching (prefix, wildcard, or open redirects on the registered host) is the #1 root cause of OAuth account takeover.
  • state – an opaque, unguessable value the client generates and verifies on return. It binds the response to the user's session and is the primary CSRF defense for the flow.
  • PKCE (RFC 7636) – Proof Key for Code Exchange. The client generates a random code_verifier, sends its SHA-256 hash as code_challenge, and proves possession of the verifier at token exchange. This stops authorization-code interception, especially on mobile/SPA public clients.

Prerequisites / Lab setup

Build a safe lab so you never touch production:

  • Keycloak as the IdP, run locally in dev mode.
  • Burp Suite (Community is fine) for intercepting and replaying authorization requests.
  • jwt_tool for inspecting and tampering with id_token / access_token JWTs.
# Spin up a disposable Keycloak IdP
docker run -p 8080:8080 \
  -e KEYCLOAK_ADMIN=admin \
  -e KEYCLOAK_ADMIN_PASSWORD=admin \
  quay.io/keycloak/keycloak:25.0 start-dev

# Clone jwt_tool for token inspection/tampering
git clone https://github.com/ticarpi/jwt_tool
pip3 install -r jwt_tool/requirements.txt
Bash

Create a realm, a confidential client, and deliberately register a loose redirect_uri like https://app.local/* so you can reproduce the issues below.

Walkthrough / PoC

1. redirect_uri manipulation

If the IdP does not match the redirect_uri exactly, you can redirect the authorization response (and the code) to an attacker-controlled endpoint. Test a baseline request, then mutate the parameter:

GET /realms/demo/protocol/openid-connect/auth?
  client_id=webapp&
  response_type=code&
  scope=openid%20profile&
  redirect_uri=https://app.local/callback&
  state=Zx9...random HTTP/1.1
Host: localhost:8080
HTTP

Classic bypass payloads to fuzz against the allowlist:

https://app.local.attacker.com/callback     # suffix confusion
https://app.local@attacker.com/callback      # userinfo trick
https://app.local/callback/../../evil        # path traversal
https://app.local//attacker.com              # protocol-relative
https://app.local/redirect?next=//attacker   # open redirect chain
Plaintext

If any of these is accepted, the code lands on your server. Combined with a missing PKCE check, you exchange it for tokens directly.

2. Missing or unverified state (CSRF)

If the client never generates state, or returns it but never validates it, you can force-login a victim into your account (login CSRF) or stitch your code into their session. Capture a valid authorization response, strip the state, and replay it against the victim's browser. The fix and the test are symmetric: a flow without a unique, session-bound state per request is vulnerable.

3. PKCE downgrade / absence

For public clients, the absence of PKCE means an intercepted code is enough to mint tokens. Test whether the /token endpoint still issues tokens when you omit code_verifier, or when you send a mismatched verifier:

# Exchange a captured authorization code WITHOUT a code_verifier
curl -s -X POST http://localhost:8080/realms/demo/protocol/openid-connect/token \
  -d grant_type=authorization_code \
  -d client_id=webapp \
  -d code=$CAPTURED_CODE \
  -d redirect_uri=https://app.local/callback
Bash

If a token comes back for a public client, PKCE is not enforced. A correct server returns invalid_grant when the code_verifier is missing or does not hash to the original code_challenge.

4. Token leakage and id_token tampering

Tokens leak through Referer headers (implicit flow), browser history, proxy logs, mobile WebViews, and overly broad postMessage handlers. Once you hold an id_token, inspect and try to tamper it:

# Inspect claims, signature, and check for alg:none / weak signing
python3 jwt_tool/jwt_tool.py "$ID_TOKEN"

# Attempt the classic "alg:none" unsigned-token bypass
python3 jwt_tool/jwt_tool.py "$ID_TOKEN" -X a
Bash

A correctly configured Resource Server rejects alg:none, validates the iss, aud, exp, and nonce claims, and verifies the RS256 signature against the IdP's JWKS. See our deeper dive in JWT attacks and forgery for signature-confusion and key-confusion variants.

Mermaid diagram

Breaking OAuth 2.0 and OpenID Connect: Redirect, State, and Token Attacks diagram 1

This diagram shows how a loose redirect_uri plus missing PKCE chains into token theft and account takeover, while strict matching and enforced PKCE break the chain.

Detection & Defense (Blue Team)

Defenses should be weighted at least as heavily as the offense – most of these are one-time configuration hardenings.

Registration and redirect_uri:

  • Enforce exact-string matching of redirect_uri. No wildcards, no prefix matching, no trailing-slash leniency. Maintain a tight per-client allowlist.
  • Eliminate open redirects on any host that appears in the allowlist; they are equivalent to a wildcard.

Flow hardening:

  • Use the Authorization Code flow with PKCE for every client type, including confidential ones (RFC 9700 guidance). Disable the implicit and password grants entirely.
  • Generate a fresh, cryptographically random state per request and reject responses whose state does not match the session. In OIDC, also validate nonce in the id_token.
  • Bind tokens to the sender where possible (DPoP / mTLS-bound tokens) so a leaked bearer token cannot be replayed.

Token validation (Resource Server):

  • Reject alg:none and any algorithm not on an explicit allowlist. Validate iss, aud, exp, iat, and signature against the IdP JWKS.
  • Keep access tokens short-lived; use rotating, one-time-use refresh tokens.

Detection / monitoring:

  • Alert on token-endpoint requests with mismatched or absent code_verifier, and on authorization requests with redirect_uri values outside the allowlist.
  • Log and rate-limit by client_id; flag spikes in invalid_grant and invalid_redirect_uri errors, which indicate active fuzzing.

A simple log-side detector for redirect-uri tampering attempts:

# Flag authorization requests whose redirect_uri is not in the allowlist
grep "protocol/openid-connect/auth" access.log \
  | grep -v "redirect_uri=https%3A%2F%2Fapp.local%2Fcallback" \
  | awk '{print $1, $7}'
Bash

This maps to MITRE ATT&CK T1550.001 (Application Access Token) and T1528 (Steal Application Access Token). For mobile-specific token storage and WebView interception, see Android app pentesting with Frida and Burp Suite for mobile API testing.

Conclusion

OAuth 2.0 and OIDC fail safely only when every guardrail is present at once: exact redirect_uri matching, a verified state/nonce, enforced PKCE, retired implicit flow, and strict token validation. Attackers chain the weakest of these into account takeover, so defenders must treat each as non-optional. Build the Keycloak lab above, reproduce each issue, then verify that hardening it breaks the exploit.

References

You Might Also Like

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

Comments

Copied title and URL