Node.js · 09/19 7 min
Auth: hashing, JWTs, 401 vs 403
bcrypt, signing and verifying tokens, reading the Authorization header, and role checks.
Passwords
const hash = await bcrypt.hash(password, 10);
const ok = await bcrypt.compare(password, user.passwordHash); // await it!
if (!ok) return res.status(401).json({ error: "Invalid credentials" });
bcrypt.comparereturns a Promise. Withoutawait,if (!ok)is never true and every password works.- Never store or return the plain password or the hash: strip it from responses (
select("-password"), or delete beforeres.json). - Use one generic message for "no such user" and "wrong password".
Tokens
const token = jwt.sign({ sub: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: "1h" });
const header = req.headers.authorization ?? ""; // "Bearer eyJ…"
const [scheme, raw] = header.split(" ");
if (scheme !== "Bearer" || !raw) return res.status(401).end();
try {
req.user = jwt.verify(raw, process.env.JWT_SECRET); // throws if bad or expired
} catch {
return res.status(401).end();
}
| Gotcha | Result |
|---|---|
header.split(" ")[0] |
you verify the word "Bearer" |
jwt.decode instead of jwt.verify |
anyone can forge a token: decode doesn't check the signature |
exp compared to Date.now() |
exp is seconds; Date.now() is ms |
| secret read at import time before env is loaded | signs with undefined |
401 or 403?
- 401 Unauthorized: we don't know who you are (missing, malformed, expired token).
- 403 Forbidden: we know who you are, and you're not allowed.
const requireRole = (...roles) => (req, res, next) =>
roles.includes(req.user?.role) ? next() : res.status(403).json({ error: "Forbidden" });
app.delete("/api/users/:id", requireAuth, requireRole("admin"), handler); // order matters
Ownership
Role checks aren't enough for per-user data: GET /orders/:id must also check order.userId === req.user.sub, or any logged-in user reads anyone's orders.
Practise it now
Next in Node.js
Webhooks and signatures