Mistake 1: Storing JWTs in localStorage
Every JavaScript XSS vulnerability can read localStorage without any restriction. If an attacker can inject JavaScript into your application — through a dependency, a user-generated content field, or a third-party script — they can steal every JWT in localStorage and impersonate every logged-in user. Fix: access tokens stored in JavaScript memory (React context or Zustand), refresh tokens in httpOnly, SameSite=Strict, Secure cookies that JavaScript cannot read.
Mistake 2: Long-Lived Access Tokens
Access tokens that expire in 7 days or 30 days dramatically extend the window of opportunity after a token is compromised. Fix: 15-minute access token expiry, 7-day refresh token expiry, silent background refresh that transparently extends the session. Compromised access tokens are useless after 15 minutes.
Mistake 3: No Refresh Token Rotation
Without rotation, a stolen refresh token is permanently valid until it expires. Fix: every refresh token use issues a new refresh token and invalidates the previous one. Attempted reuse of an already-used refresh token immediately invalidates the entire token family and logs the user out.
Mistake 4: No Rate Limiting on Authentication Endpoints
Unprotected login and password reset endpoints are open to credential stuffing attacks. Fix: rate limit by IP address and by target username independently. Exponential back-off after 5 failed attempts per username. CAPTCHA after 3 IP-level failures in a 10-minute window.
