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.

Hard 70m 3 tasks
Prerequisites: CSRF & Clickjacking

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.

✦ Answer the questions to complete this task

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.

✦ Answer the questions to complete this task

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.

✦ Answer the questions to complete this task

Why is password reset link generation via Host header dangerous?

💪 Exercises & Challenges

📝 MCQ Medium +20 XP

Auth Vulnerabilities MCQ

Auth Vulnerabilities MCQ

Start →
⚙️ Practical Medium +30 XP

Secure a JWT Authentication System

Secure a JWT Authentication System

Start →
🚩 Challenge Hard +50 XP

OAuth State CSRF

OAuth State CSRF

Start →