4.2Authentication & security (JWT, bcrypt, Passport, Helmet)
The essentials in 30 seconds
- Never a plain-text password: store the bcrypt hash (one-way, salted, slow) and compare at login without ever decrypting.
- JWT = self-contained signed token: issued after login, sent back in the Authorization header, verified by a middleware (401 unauthenticated, 403 unauthorised).
- Salim's reflex: JWT secret in an environment variable (never hardcoded), Helmet enabled, and “never trust inputs”.
Running case — Salim secures the API accounts
Salim's products API (4.1) must now handle sellers: signup, login, restricted routes. First reflex: hash each password with bcrypt before storage, then issue a signed JWT on successful login. He writes a middleware that verifies the token on protected routes, distinguishes 401 (not logged in) from 403 (no permission), enables Helmet and stores his JWT secret in an environment variable. When a seller logs in, their client sends the token back with each request — without ever exposing a password. Second milestone of the thread: accounts that hold even if the database leaks.
Continuation of the 4.1 case — Salim protects the API he has just built.
1. Passwords: bcrypt, never plain text
Absolute rule: never store a plain-text password. Store its hash — a one-way transformation, impossible to reverse. The standard library is bcrypt: it hashes with a random salt (against precomputed tables) and deliberately slowly (against brute force). At signup you hash and store the hash; at login you compare (bcrypt.compare) without ever decrypting. If the database is stolen, passwords stay protected.
Never code your own “homemade” hash: use bcrypt or a proven equivalent. And remember that many users reuse their passwords elsewhere: a plain-text leak exposes them far beyond your application.
2. Keeping the session: JWT
HTTP is stateless: after login, you must recognise the user on each request. The JWT (JSON Web Token) is a cryptographically signed token, issued after successful login; the client sends it back with each request (Authorization: Bearer ... header) and the server verifies it — without storing any session, the JWT is “self-contained”. It is the most popular approach for APIs and mobile.
A middleware protects the routes: missing or invalid token → 401; valid token but insufficient rights → 403. Passport simplifies authentication (local strategies, Google/GitHub OAuth). Remember the distinction: authentication (who are you? 401) vs authorisation (are you allowed? 403).
3. Cross-cutting security
Beyond authentication: Helmet (Express middleware setting security HTTP headers), HTTPS in production, secrets in environment variables (.env ignored by Git, never in code), rate limiting against brute force, input validation (see 4.4). The principle behind everything: never trust inputs or the client — anything coming from outside is potentially malicious. Security is not an option added at the end; it is a permanent concern, built in from design.
🔎 Concrete example — 🔎 Concrete example — Signup, login, protected route
Salim's complete circuit, with the secret read from the environment:
POST /inscription → bcrypt.hash(motDePasse) → store the hash
POST /connexion → bcrypt.compare → jwt.sign({ id }, SECRET)
GET /mon-compte → Authorization: Bearer ... → middleware verifies
→ missing/invalid token: 401 · no permission: 403
Reading: three routes, three distinct responsibilities.
Lesson: authentication (who?) and authorisation (which right?) are two filters.
Section vocabulary
- bcrypt
- Password hashing library: one-way, with random salt, deliberately slow against brute force.
Simple example: Salim hashes at signup and compares at login, without ever decrypting.
What it is not: reversible encryption or a “homemade” hash.
Reflex: plain-text password in the database = serious fault, always hash.
- JWT
- Signed token issued after login, sent back by the client on each request and verified by the server; stateless, ideal for APIs.
Simple example: Salim's seller receives their JWT at login and sends it back in Authorization.
What it is not: a server-side stored session with a cookie.
Reflex: verify the signature on each protected request.
- Authentication vs authorisation
- Authentication: WHO the user is (401 if not). Authorisation: whether they have the RIGHT (403 if not).
Simple example: Salim returns 401 without a token, 403 for a seller on an admin route.
What it is not: a single blurry check mixing both.
Reflex: choose the right code: 401 or 403?
- Helmet
- Express middleware setting security HTTP headers that protect against classic web flaws.
Simple example: Salim enables it in one line: app.use(helmet()).
What it is not: a complete authentication system on its own.
Reflex: enable it by default on every Express app.
- Environment variable
- Configuration read from the environment (.env file ignored by Git), the only acceptable place for secrets.
Simple example: Salim's JWT secret lives in process.env.JWT_SECRET, never in code.
What it is not: a key pasted hardcoded in the source and pushed to Git.
Reflex: .env ignored by Git + .env.example sample without values.
In practice — In practice — Securing authentication
- Hash passwords with bcrypt at signup (never plain text) and compare at login (bcrypt.compare).
- Issue a signed JWT after successful login, with a secret read from process.env (never hardcoded).
- Write a middleware that VERIFIES the JWT on protected routes (401 if missing/invalid) and attaches the user to req.user.
- Add Helmet, store all secrets in environment variables (.env ignored by Git) and distinguish 401 / 403.
Ready-to-say script — explaining your security choices in review (30 s)
“This is Salim: no plain-text passwords, everything is hashed with bcrypt; after login we issue a signed JWT whose secret lives in an environment variable, never in code. A middleware verifies the token on protected routes — 401 without a valid token, 403 without the right — and Helmet sets our security headers.”
Say in code review; show the .gitignore excluding .env.
Copy-paste template — bcrypt + JWT + Helmet auth (tested code)
require('dotenv').config();
const express = require('express');
const helmet = require('helmet');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const app = express();
app.use(helmet());
app.use(express.json());
const SECRET = process.env.JWT_SECRET; // never hardcoded!
if (!SECRET) throw new Error('JWT_SECRET missing: define it in .env');
const vendeurs = []; // demo: in real life, a database
// Signup: hash, then store the hash
app.post('/inscription', async (req, res) => {
if (!req.body.email || !req.body.motDePasse) {
return res.status(400).json({ erreur: 'Email and password required' });
}
const hachage = await bcrypt.hash(req.body.motDePasse, 10);
vendeurs.push({ email: req.body.email, hachage });
res.status(201).json({ message: 'Account created' });
});
// Login: compare, then issue a JWT
app.post('/connexion', async (req, res) => {
const vendeur = vendeurs.find((v) => v.email === req.body.email);
if (!vendeur) return res.status(401).json({ erreur: 'Invalid credentials' });
const ok = await bcrypt.compare(req.body.motDePasse, vendeur.hachage);
if (!ok) return res.status(401).json({ erreur: 'Invalid credentials' });
const jeton = jwt.sign({ email: vendeur.email }, SECRET, { expiresIn: '1h' });
res.json({ jeton });
});
// Middleware: verifies the JWT on protected routes
function verifierJeton(req, res, next) {
const entete = req.headers.authorization || '';
const jeton = entete.replace('Bearer ', '');
if (!jeton) return res.status(401).json({ erreur: 'Missing token' });
try {
req.utilisateur = jwt.verify(jeton, SECRET);
next();
} catch (err) {
return res.status(401).json({ erreur: 'Invalid token' });
}
}
app.get('/mon-compte', verifierJeton, (req, res) => {
res.json({ email: req.utilisateur.email });
});
app.listen(3000);
Small personal project
bcrypt + JWT + Helmet are enough: don't complicate with OAuth while local covers the need.
Login via Google/GitHub
Add Passport with an OAuth strategy: delegate identity, keep the JWT for your own routes.
API exposed in production
Like Salim: mandatory HTTPS, short-lived JWTs, rate limiting on /connexion against brute force.
Beginner team
Non-negotiable ritual: no secrets in code or Git — systematic review before each push.
.env ignored by Git, never hardcoded in code), Helmet enabled and HTTPS in production. This guide's examples use fictitious data only — no real secrets. When in doubt about personal data, have the architecture validated by a technical lead. Teaching content, not a security audit.✓ Best practices
- Hash with bcrypt, compare without decrypting.
- JWT secret in an environment variable, never hardcoded.
- Distinguish 401 (who?) from 403 (which right?).
✗ Common pitfalls
- Storing plain-text passwords.
- Pasting the JWT secret into code or Git.
- Confusing authentication and authorisation.
Key takeaways
- NEVER plain-text passwords: BCRYPT HASHING (one-way, salt, slow); compare at login. Never homemade hashing.
- JWT: self-contained signed token (Authorization), verified by middleware; Passport simplifies (local, OAuth).
- AUTHENTICATION (401) vs AUTHORISATION (403) — two distinct checks.
- Helmet, HTTPS, SECRETS in environment variables, rate limiting, validation. Principle: NEVER TRUST inputs.
48h challenge — apply tomorrow
Add signup + login + 1 protected route to your API: bcrypt, JWT via process.env, Helmet — then verify the 401 without a token.
- I can explain bcrypt, JWT, 401 vs 403.
- I know how to place a secret in an environment variable.
- I have 1 protected route tested with and without a token.
Frequently asked questions
Why never store passwords in plain text, and how does bcrypt protect them?
If the database leaks (hacking, stolen backup — it happens regularly), plain-text passwords expose all accounts, and worse, the other services where users reuse the same password. Hashing is one-way: you store the hash at signup, you compare at login, without ever being able to “recover” the password. bcrypt adds a random salt (against precomputed tables) and deliberate slowness (against brute force). Stealing bcrypt hashes does not let anyone log in directly.
Where to start when security feels intimidating?
With the “never trust inputs” mindset — on its own it prevents a large share of flaws — then with high-impact fundamentals: bcrypt for passwords, secrets in environment variables (never in Git), HTTPS in production, correct authentication and authorisation (401/403), input validation (4.4), Helmet. Security is built in from design, not at the end.
Where should the JWT signing secret live?