Back to Cheat Sheets

🔑 Broken User AuthenticationAPI 2019

OWASP API Security Top 10 2019 - API2

CRITICAL RISK

📋 What Is It?

Broken User Authentication covers every way an API fails to establish, protect, or invalidate the caller's identity. Authentication in APIs is harder than in classic web apps: there is no browser session to lean on, credentials and tokens travel in headers on every request, and endpoints such as /login, /register, /token/refresh and /password/reset are publicly reachable by design - so they are attacked directly, at machine speed, with no rate limit from a human clicking a form.

Once authentication breaks, every other control downstream is decorative: the attacker is a legitimate user as far as the system is concerned.

API2 OWASP Rank 2019
Public Attack Surface
Account Takeover Impact

⚠️ Common Exploits

  • Credential Stuffing: breached password lists replayed against /login with no throttle
  • Brute Force: unlimited attempts on passwords, PINs, and OTPs
  • alg:none / Weak Secret JWT: tokens forged because the signature is not properly verified
  • Token in URL: access tokens in query strings leak via logs, referrers, and history
  • No Expiry / No Revocation: a stolen token is valid forever
  • Weak Password Reset: guessable tokens, no expiry, reusable links
  • API Key as Identity: a single shared key treated as proof of a user

🔴 Attack Flow

1. Enumerate auth endpoints: login, register, refresh, reset, OTP verify
↓
2. Probe for rate limiting, lockout, and user enumeration in responses
↓
3. Stuff credentials / brute-force OTPs at high volume
↓
4. Or capture a token and test alg:none, weak secrets, missing expiry
↓
5. IMPACT: Full account takeover - and the logs show a "valid" login!

❌ Vulnerable Code

// Bad: signature never verified - decode is not verify const claims = jwt.decode(token); ← accepts any token, forged or not if (claims.role === 'admin') allowAdmin(); // Bad: algorithm chosen by the attacker's own header jwt.verify(token, secret); ← no algorithms: [...] → alg:none / HS-vs-RS confusion # Bad: unlimited attempts, and the error tells the attacker who exists @app.route('/api/login', methods=['POST']) def login(): user = User.query.filter_by(email=request.json['email']).first() if not user: return {'error': 'No such user'}, 404 ← user enumeration if user.password == request.json['password']: ← plaintext compare return {'token': make_token(user)} # no expiry, no lockout, no MFA # Bad: guessable, non-expiring reset token reset_token = str(user.id) + str(int(time.time()))

✅ Secure Code

// Good: verify with a pinned algorithm, issuer, and audience const claims = jwt.verify(token, publicKey, { algorithms: ['RS256'], // never trust the token's own alg header issuer: process.env.EXPECTED_ISSUER, audience: process.env.EXPECTED_AUDIENCE, // exp is enforced by default }); # Good: uniform errors, hashing, throttling, lockout, MFA @app.route('/api/login', methods=['POST']) @limiter.limit('5 per minute; 20 per hour') # per IP AND per account def login(): user = User.query.filter_by(email=request.json['email']).first() ok = user and argon2.verify(request.json['password'], user.pw_hash) if not ok: record_failure(request.json['email'], request.remote_addr) return {'error': 'Invalid credentials'}, 401 # identical for both cases if user.mfa_enabled and not verify_totp(user, request.json.get('otp')): return {'error': 'Invalid credentials'}, 401 return {'access_token': issue(user, ttl=900), # short-lived 'refresh_token': issue_refresh(user)} # rotating, revocable # Good: high-entropy, single-use, expiring reset token reset_token = secrets.token_urlsafe(32) # store only its hash; expire in 15 min

✓ Prevention Checklist

  • Inventory every authentication flow, including refresh and reset
  • Use a proven identity provider rather than hand-rolled auth
  • Hash passwords with Argon2id/bcrypt/scrypt; never encrypt or store plaintext
  • Verify JWTs with a pinned algorithm, issuer, audience, and expiry
  • Keep access tokens short-lived; rotate and allow revocation of refresh tokens
  • Send tokens in headers - never in URLs or query strings
  • Rate-limit and lock out per account and per source, including OTP endpoints
  • Return identical responses for unknown user and wrong password
  • Offer MFA and enforce it for privileged accounts
  • Make reset tokens random, hashed at rest, single-use, and short-lived
  • Re-authenticate before sensitive changes (email, password, payout details)
  • Log authentication events and alert on credential-stuffing patterns

🔍 Detection & Tools

Identity & Libraries:

Auth0 / Okta / Cognito Keycloak jose / pyjwt Argon2

Testing:

jwt_tool Burp Intruder hashcat OWASP ZAP

How to Test:

  • Send 100 bad logins - is there any throttle or lockout?
  • Compare responses for unknown user vs wrong password
  • Re-sign a token with alg:none and with a guessed HMAC secret
  • Strip or extend exp; check the token is rejected
  • Reuse a password-reset link twice, and after an hour
  • Log out, then replay the old token - is it revoked?

🌍 Real-World

These are documented incident classes - recurring patterns rather than single CVEs.

  • Credential stuffing at scale: unthrottled mobile login APIs let attackers replay breached password lists until thousands of accounts open.
  • OTP brute force: a 4–6 digit code with no attempt limit is exhausted in minutes, defeating "MFA" entirely.
  • Forged JWTs: services that decode instead of verify, or accept alg:none, hand admin access to anyone who edits a claim.
  • Tokens leaked in URLs: access tokens in query strings end up in proxy logs, analytics, and browser history, then get replayed.
  • Never-expiring sessions: tokens with no expiry or revocation stay usable long after a device is lost or an employee leaves.

📌 Quick Tips

  • DO NOT use jwt.decode() as an auth check
  • DO NOT reveal whether an account exists
  • DO NOT put tokens in URLs
  • DO pin the algorithm and validate iss/aud/exp
  • DO throttle every auth endpoint, OTP included
  • DO re-authenticate before sensitive changes

📜 Compliance

Related Standards:

  • OWASP API Security Top 10 2019 - API2
  • Maps to API2:2023 Broken Authentication
  • CWE-287 Improper Authentication
  • CWE-307 Improper Restriction of Excessive Authentication Attempts
  • CWE-345 Insufficient Verification of Data Authenticity
  • NIST SP 800-63B Digital Identity Guidelines