1. The architecture
[React form] → POST /api/auth/login → [Express: find user → bcrypt.compare]
→ sign JWT (15-min expiry) → [React AuthContext stores session]
→ every protected request carries the token → [auth middleware verifies]
Three rules that prevent 90% of auth bugs:
- Never store plain-text passwords. Hash with bcrypt (cost factor 10–12).
- Never trust the client. Every protected route re-verifies the token server-side.
- Expire sessions. Short-lived access tokens beat "remember me forever."
2. Register and login (Express)
Typescript
3. The auth middleware (the part most tutorials skip)
Typescript
4. React side: AuthContext + protected routes
Tsx
5. JWT vs sessions: when each wins
6. Refresh tokens: short sessions without annoying logins
Typescript
7. Three mistakes that break MERN auth
- Secrets in the JWT payload. Payloads are Base64, not encrypted — anyone can decode them. Store only sub (user id) and role.
- No expiry. Always set expiresIn. A stolen never-expiring token is a permanent backdoor.
- Trusting localStorage blindly. XSS can read it. HttpOnly cookies are safer against XSS (pair with CSRF protection); Bearer headers avoid CSRF but not XSS. Pick based on your threat model — and never skip server-side verification either way.
8. Hardening checklist before production
- JWT_SECRET is long, random, and only in server env — never shipped to the client.
- Rate-limit /login and /register (brute-force protection).
- HTTPS only; set Secure, HttpOnly, SameSite if you move tokens into cookies.
- Add refresh tokens if 15-minute sessions annoy users — rotate them on every use.
- Log failed logins; alert on spikes.