Appearance
Security basics
Auth chapters get you logged in. This chapter is the seatbelt - validation, headers, rate limits, and the JWT mistakes I keep seeing (including my own older code). 😅
Validate input with Zod
bash
npm install zodts
import { z } from 'zod'
const registerSchema = z.object({
email: z.string().email(),
password: z.string().min(8).max(100),
})
authRouter.post('/register', async (req, res, next) => {
try {
const body = registerSchema.parse(req.body)
// body.email / body.password are typed + cleaned
// …
} catch (err) {
if (err instanceof z.ZodError) {
return next(new HttpError(400, err.issues[0]?.message ?? 'invalid input'))
}
next(err)
}
})FE map: same library you might use in forms - run it again on the server. Client validation is UX; server validation is security.
Helmet
bash
npm install helmetts
import helmet from 'helmet'
app.use(helmet())Sets sane HTTP headers (XSS protections, etc.). Not a full security program - a cheap win.
Rate limit auth routes
bash
npm install express-rate-limitts
import rateLimit from 'express-rate-limit'
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 50, // tune for your threat model
standardHeaders: true,
legacyHeaders: false,
})
app.use('/api/auth', authLimiter)Slows credential stuffing and reset spam. Put stricter limits on /login and /forgot if you want.
JWT footguns
- Weak
JWT_SECRET- use a long random string; never commit it - Storing sensitive data in the payload - anyone can base64-decode JWT claims; signature only proves integrity
- No expiry - always set
expiresIn - localStorage XSS - if JS can read the token, XSS can steal it; httpOnly cookies trade that for CSRF complexity
- Assuming logout revokes tokens - delete client-side; server still accepts until expiry unless you version/blocklist
For this tutorial's learning path, Bearer + localStorage is OK. For a bank app, read more. 🙂
Passwords
- Min length (8+; prefer 12+ in products)
- bcrypt / argon2 - not MD5, not SHA alone
- Constant-ish error messages on login
- Optional: breach checks (Have I Been Pwned k-anonymity API) later
SQL
Always bound parameters (? placeholders). If you ever write:
ts
db.prepare(`SELECT * FROM users WHERE email = '${email}'`) // NOPE…stop. That's how SQLi demos get written.
CORS is not auth
Allowing origin: http://localhost:5173 does not authenticate users. CORS protects browsers from other websites reading responses. APIs still need requireAuth.
Quick hardening checklist
- [ ] Zod (or similar) on auth bodies
- [ ]
helmet() - [ ] Rate limit
/api/auth - [ ] Strong
JWT_SECRET - [ ] HTTPS in production
- [ ]
.envgitignored - [ ] No password hashes in JSON responses
You don't need a CISO office to ship a side project - but skipping all of the above is how side projects become incident writeups.
Next: where to go after Express feels comfortable.
Best,
-- ł.