BugForge
All lessons
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.compare returns a Promise. Without await, 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 before res.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.

Next in Node.js
Webhooks and signatures