4.2 · Authentication & security (JWT, bcrypt, Passport, Helmet)

Level 4 · API, authentication & real time

4.2Authentication & security (JWT, bcrypt, Passport, Helmet)

Goal: Secure authentication (passwords hashed with bcrypt, verified JWTs, authorisation) and apply security basics (Helmet, protected secrets).
Estimated time: 13 min · self-paced reading · If you only have 5 min: read “The essentials in 30s” + the diagram.

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.

Common misinterpretation: “I added authentication with a JWT, so my API is secure.” No. A JWT only maintains identity: without bcrypt (plain-text passwords), without a protected secret (key hardcoded in code pushed to Git), without Helmet or HTTPS, without input validation and without the 401/403 distinction, the building remains full of holes. Security is a bundle of measures, not a single brick.
The authenticated cycle Signup → login → verified requests Signup bcrypt hashes, westore the hashLogin We compare, weissue a signed JWTRequests Client sends back theJWT (Authorization)Middleware Verifies: 401 or403, or req.user
Signup → login → verified requests
Text version of the diagram (SEO / screen readers / translation): 1) At signup, Salim hashes the password with bcrypt and stores only the hash. 2) At login, he compares then issues a signed JWT. 3) The client sends this token back in the Authorization header on each request. 4) A middleware verifies it on protected routes: 401 if missing or invalid, 403 if not allowed. Lesson: hash, sign, verify — with the secret outside the code.

🔎 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

  1. Hash passwords with bcrypt at signup (never plain text) and compare at login (bcrypt.compare).
  2. Issue a signed JWT after successful login, with a secret read from process.env (never hardcoded).
  3. Write a middleware that VERIFIES the JWT on protected routes (401 if missing/invalid) and attaches the user to req.user.
  4. Add Helmet, store all secrets in environment variables (.env ignored by Git) and distinguish 401 / 403.
You secure authentication (hashed passwords, verified JWTs, authorisation) and apply the basics (Helmet, protected secrets) — a professional responsibility.

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.

Caution: Security is your responsibility: never plain-text passwords (bcrypt mandatory), JWT secrets and keys in environment variables (.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.

Section exit checklist
  • 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.
Self-rating 1-5: no hardcoded secrets in my code?do my sensitive routes distinguish 401 from 403?

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.

Check your understanding

Where should the JWT signing secret live?

Additional resources (optional, to go further)

Level 4 examFresh, harder questions to test what you've learned