Sign-in and permissions
Store passwords safely, choose sessions or tokens, and check ownership.
Authentication answers "who are you?" Authorisation answers "what may you do?" Most real bugs are in the second part: a signed-in user reading someone else's data.
Passwords
Node.js
import bcrypt from "bcrypt";
// Sign up: store only the hash.
const hash = await bcrypt.hash(password, 12);
await db.users.insert({ email, passwordHash: hash });
// Sign in: compare, and give one message for every failure.
const user = await db.users.findByEmail(email);
const ok = user && (await bcrypt.compare(password, user.passwordHash));
if (!ok) return res.status(401).json({ error: "Wrong email or password" });Saying "no such email" tells an attacker which accounts exist, so use the same message for both cases. Add a rate limit to the sign-in route too.
Sessions or tokens
| Approach | How it works |
|---|---|
| Session cookie | The server remembers who is signed in and gives the browser a random id in an httpOnly cookie. Signing out everywhere is easy. |
| JWT | A signed token holds the user id, and the server checks the signature instead of looking it up. It is hard to cancel before it expires, so keep its life short. |
Check ownership on every request
Node.js
app.get("/api/invoices/:id", requireLogin, async (req, res) => {
const invoice = await db.invoices.findById(req.params.id);
if (!invoice || invoice.userId !== req.user.id) {
return res.status(404).json({ error: "Not found" });
}
res.json(invoice);
});Without the userId check, anyone signed in could change the number in the URL and read other customers' invoices. Answering 404 instead of 403 also hides that the invoice exists.