Authentication Vulnerabilities: JWT, OAuth & Sessions
Explore and exploit authentication vulnerabilities — JWT attacks (alg:none, weak secrets, kid injection), OAuth misconfigurations, session management flaws, and MFA bypass techniques.
Learning Objectives
- → Exploit JWT vulnerabilities: alg:none, weak HMAC secrets, kid injection
- → Identify and exploit OAuth 2.0 misconfigurations
- → Bypass authentication via password reset poisoning and account takeover chains
- → Analyze session fixation, session prediction, and session hijacking
- → Implement secure JWT validation, OAuth flows, and session management
JWT – JSON Web Tokens
JWT Structure
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwicm9sZSI6InVzZXIiLCJleHAiOjE2OTk5OTk5OTl9.SIGNATURE
Header = Base64url({"alg":"HS256","typ":"JWT"})
Payload = Base64url({"sub":"user123","role":"user","exp":1699999999})
Signature = HMAC-SHA256(header + "." + payload, secret)
Decode (anyone can):
echo "eyJzdWIiOiJ1c2VyMTIzIiwicm9sZSI6InVzZXIifQ==" | base64 -d
{"sub":"user123","role":"user"}
JWT Attack 1: alg:none
Some libraries accept alg:none — no signature required:
1. Take a valid JWT
2. Change header: {"alg":"none","typ":"JWT"}
3. Modify payload: {"sub":"user123","role":"admin"}
4. Remove signature: header.payload. (empty signature)
5. Submit → if server accepts, you are admin
curl -H "Authorization: Bearer eyJhbGciOiJub25lIn0.eyJzdWIiOiJ1c2VyMTIzIiwicm9sZSI6ImFkbWluIn0." /api/admin
JWT Attack 2: Weak HS256 Secret
# JWT signed with weak secret like "secret" or "123456"
# Brute force with hashcat or jwt_tool:
hashcat -a 0 -m 16500 token.jwt /usr/share/wordlists/rockyou.txt
# Or jwt_tool:
python3 jwt_tool.py TOKEN -C -d /wordlists/rockyou.txt
# Once secret found: sign arbitrary payload with the same secret
python3 -c "
import jwt
print(jwt.encode({'sub': 'admin', 'role': 'admin'}, 'secret', algorithm='HS256'))
"
JWT Attack 3: Algorithm Confusion (RS256 → HS256)
If server uses RS256 (RSA asymmetric):
Signs with private key
Verifies with public key
Attack: change alg from RS256 to HS256
Tell server: alg=HS256, secret = PUBLIC KEY
Server verifies: HMAC(payload, public_key)
Attacker can sign with: HMAC(modified_payload, public_key)
[Public key is, well, public — attacker can get it!]
# Requires: server does not enforce algorithm, uses public key as HS256 secret
JWT Attack 4: kid (Key ID) Injection
// kid points to which key to use for verification
{"alg":"HS256","typ":"JWT","kid":"key1"}
// SQL injection via kid:
{"kid": "' UNION SELECT 'attacker_secret' -- "}
// Server executes: SELECT key FROM keys WHERE kid = '...'
// Injects attacker's own secret → signs with that secret
// Directory traversal via kid:
{"kid": "../../dev/null"}
// Server reads /dev/null as the key → empty string
// Sign with empty string HMAC → attacker controls tokens
JWT Defense
# Always:
import jwt
# Verify with explicit algorithm (never accept 'none')
payload = jwt.decode(
token,
secret,
algorithms=["HS256"], # explicit — not token.get('alg')
options={"require": ["exp", "iat"]}
)
# Check expiry, issuer, audience:
payload = jwt.decode(
token, secret, algorithms=["HS256"],
audience="api.example.com",
issuer="auth.example.com",
)
# Use strong secrets (min 256-bit for HS256):
import secrets
JWT_SECRET = secrets.token_hex(32) # 256-bit random
# Prefer RS256 in production:
# Sign with private key, verify with public key only
OAuth 2.0
OAuth Flow (Authorization Code)
1. User clicks "Login with Google"
2. App redirects to Google:
https://accounts.google.com/oauth/authorize?
client_id=APP_ID&
redirect_uri=https://app.com/callback&
response_type=code&
scope=email profile&
state=RANDOM_STATE
3. User logs in to Google, approves
4. Google redirects back:
https://app.com/callback?code=AUTH_CODE&state=RANDOM_STATE
5. App exchanges code for tokens (server-side):
POST https://accounts.google.com/oauth/token
client_id=... client_secret=... code=AUTH_CODE
6. App gets access_token + id_token
OAuth Misconfiguration 1: Open redirect_uri
Attack: Attacker registers https://app.com:
/callback?code=CODE (legitimate)
/callback/../redirect?url=evil.com (redirects to evil.com)
If server allows redirect_uri to be anything under app.com:
https://app.com/evil-path
Or wildcard: redirect_uri=https://attacker.com/evil
Auth server accepts it → auth code sent to attacker.com
Fix: exact match on redirect_uri (no wildcards, no path traversal)
OAuth Misconfiguration 2: Missing state parameter
state is a CSRF token for OAuth:
1. Without state: attacker can fix auth code
2. Attacker starts OAuth flow, gets to step 2, pauses
3. Victim clicks attacker's link: starts THEIR OAuth at step 4
4. Victim's auth code used to log into app as victim
→ Attacker's app session = victim's account
# Fix: generate random state, store in session, verify on return
OAuth Misconfiguration 3: Implicit Grant in SPA
Implicit grant: access_token in URL fragment
https://app.com/callback#access_token=...
Problems:
- Token in URL → logged in browser history, Referer headers
- No refresh tokens
- Client secret not verified
Fix: Use authorization_code + PKCE for SPAs:
PKCE: Proof Key for Code Exchange
code_verifier: random string
code_challenge: SHA256(code_verifier)
Server verifies: code_verifier matches stored code_challenge
Session Management Attacks
Session Fixation
1. Attacker visits site → gets session ID: SID=abc123
2. Attacker sends victim a link with their SID:
https://example.com/login?sessionid=abc123
3. Victim logs in using SID=abc123
4. Server authenticates victim but keeps SID=abc123
5. Attacker uses SID=abc123 → now authenticated as victim!
Fix: Always regenerate session ID on login (and privilege change):
# Django — done automatically by login():
from django.contrib.auth import login
login(request, user) # new session ID assigned
Password Reset Poisoning
Forgot password flow:
POST /reset-password
Host: evil.com (attacker-controlled)
Content: email=victim@example.com
Vulnerable code:
reset_link = f"https://{request.get_host()}/reset?token={token}"
email(victim, reset_link)
Attack:
- Attacker sends request with modified Host header
- Victim gets email with link to evil.com/reset?token=...
- Victim clicks → attacker receives token → resets password
Fix: Hardcode the reset URL domain (never use Host header for security-critical URLs)
Brute Force & Rate Limiting
# Lock account after N failures:
MAX_ATTEMPTS = 5
LOCK_DURATION = timedelta(minutes=15)
user.failed_login_attempts += 1
if user.failed_login_attempts >= MAX_ATTEMPTS:
user.locked_until = now() + LOCK_DURATION
# Rate limit by IP (django-ratelimit):
from ratelimit.decorators import ratelimit
@ratelimit(key='ip', rate='5/m', block=True)
def login(request):
...
# Progressive delay:
time.sleep(2 ** min(attempts, 5)) # 2, 4, 8, 16, 32 seconds
MFA Bypass Techniques
1. Response manipulation:
Server returns: {"mfa_required": true}
Attacker changes to: {"mfa_required": false}
→ MFA step skipped
2. Rate limit bypass:
6-digit TOTP = 1,000,000 combinations
If no rate limit: brute force in under an hour
3. Skip step in multi-step auth:
/login → /mfa → /dashboard
Navigate directly to /dashboard without MFA step
4. Backup code theft via social engineering or SQLi
5. SIM swapping — attacker takes over victim's phone number
→ All SMS OTPs go to attacker
Using jwt.io or jwt_tool: (1) decode a JWT without a library — base64url decode header and payload, (2) attempt alg:none attack — modify payload and remove signature, (3) crack a JWT signed with 'secret' using hashcat or jwt_tool -C, (4) re-sign the token with 'admin' role using the cracked secret, (5) test kid=../../dev/null injection.
Why is the alg:none vulnerability so serious?
Why should you use algorithms=['HS256'] explicitly in jwt.decode()?
Set up a minimal OAuth client and test: (1) start an authorization code flow, (2) modify redirect_uri to https://evil.com — does the server allow it? (3) start a flow without state parameter — demonstrate CSRF via OAuth, (4) steal the authorization code by intercepting the redirect, (5) exchange the code for tokens.
What is PKCE and why is it needed for SPAs?
Harden a Django application's session management: (1) verify session ID regenerates on login (Django does this automatically — confirm), (2) set SESSION_COOKIE_SECURE=True, SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_SAMESITE='Strict', (3) implement session timeout: SESSION_COOKIE_AGE=1800 (30 min), (4) implement account lockout after 5 failed logins, (5) fix password reset to use hardcoded domain not Host header.
Why is password reset link generation via Host header dangerous?