Implement password hashing and JWT-based auth for an Express API—without turning the tutorial into a false sense of production readiness.
What you will learn
- Hash passwords with bcrypt
- Issue and verify JWTs
- Protect routes with middleware
- Know which security controls still belong in production hardening
Prerequisites
- Working Express basics
- Node.js 20+
- Comfortable reading async route handlers
Install
npm install express bcryptjs jsonwebtoken dotenv zodSet JWT_SECRET in .env (long random string). Never commit it.
Register and login sketch
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
const users = new Map(); // replace with a real database
const secret = process.env.JWT_SECRET;
if (!secret) throw new Error("JWT_SECRET missing");
export async function register(email, password) {
if (users.has(email)) throw Object.assign(new Error("exists"), { status: 409 });
const passwordHash = await bcrypt.hash(password, 12);
const user = { id: crypto.randomUUID(), email, passwordHash };
users.set(email, user);
return { id: user.id, email: user.email };
}
export async function login(email, password) {
const user = users.get(email);
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
throw Object.assign(new Error("invalid_credentials"), { status: 401 });
}
const token = jwt.sign({ sub: user.id, email: user.email }, secret, { expiresIn: "1h" });
return { token };
}Wire POST /auth/register and POST /auth/login to these functions. Return generic errors on login failure so you do not reveal whether an email exists.
Auth middleware
export function requireAuth(req, res, next) {
const header = req.headers.authorization || "";
const token = header.startsWith("Bearer ") ? header.slice(7) : "";
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch {
res.status(401).json({ error: "unauthorized" });
}
}
// example protected route
// app.get("/me", requireAuth, (req, res) => res.json({ user: req.user }));Clients send Authorization: Bearer <token>.
Production warnings
- Use HTTPS everywhere
- Prefer short-lived access tokens + refresh strategy
- Store secrets in a vault/env manager, never in git
- Add rate limiting and lockouts for auth endpoints
- Consider established libraries (Passport, Better Auth, Lucia, Auth.js) instead of a home-grown system for real products
Practice checklist
- Register a user, login, and call a protected route with the token
- Prove an invalid token returns
401 - Rotate
JWT_SECRETlocally and observe old tokens fail verification - Read the OWASP auth cheat sheet section on session vs token threats
Sources and further reading
- JWT introduction (jwt.io)
- OWASP Authentication Cheat Sheet
- Express security best practices
- Node source: nodejs/node
Who this guide is for
- Backend developers learning token authentication
- Teams prototyping an API before selecting an identity provider
- Reviewers assessing whether a JWT design is production-ready
Definition of done
A threat-modelled authentication prototype with safe password handling, short-lived access tokens, explicit authorization checks, revocation strategy, audit events, and tests for abuse cases.
Finishing the commands is not the same as finishing the guide. Keep the result only when another person can reproduce it, inspect the evidence, and understand the remaining risks.
Professional workflow
Phase 1 — Model threats and trust boundaries
Goal: Decide what the system protects before choosing token settings.
- List account, credential, token, and administrative assets.
- Identify browser, mobile, API, and third-party clients.
- Define likely abuse: stuffing, replay, theft, enumeration, and privilege escalation.
Verification: Each high-impact threat has a preventive control and an observable signal.
Phase 2 — Implement identity safely
Goal: Keep credential verification and token issuance narrow.
- Validate normalized identifiers and strong password rules.
- Hash passwords with an adaptive algorithm and reviewed work factor.
- Issue minimal claims with issuer, audience, subject, and expiry.
Verification: Tests reject invalid credentials without revealing whether an account exists.
Phase 3 — Authorize and recover
Goal: Treat authentication as only the first gate.
- Check permissions at every protected resource boundary.
- Design logout, key rotation, password reset, and compromised-token response.
- Record security events without logging secrets or tokens.
Verification: A revoked or downgraded user cannot retain old privileges beyond the documented window.
Review questions before you continue
- Why is JWT preferable to an opaque session for this client?
- Where and how are tokens stored?
- How are keys rotated without an outage?
- What limits brute force and credential stuffing?
If an answer is unknown, record it as an explicit follow-up instead of hiding the uncertainty behind a successful demo.
Troubleshooting decision guide
- Valid-looking tokens fail: verify issuer, audience, algorithm, clock skew, and key selection.
- Logout has no immediate effect: document the access-token window and add revocation where required.
- Authorization leaks across users: query resources using both resource ID and authenticated owner scope.
- Password hashing overloads the service: benchmark the work factor and bound concurrent login attempts.
Change one variable at a time, preserve the first useful error, and write down the command or condition that fixed it. That turns a private debugging session into team knowledge.
Production-readiness checklist
- Prefer a mature identity provider when authentication is not your core competency.
- Use TLS everywhere and protect token storage against the client threat model.
- Rate-limit and monitor registration, login, reset, and verification endpoints.
- Rotate signing keys and secrets through managed configuration.
- Schedule an independent security review before handling real accounts.
These checks are intentionally stricter than a beginner demo. Use them to decide whether to continue learning, run a controlled pilot, or request specialist review.
Your next 30 minutes
- Write the threat model and token lifetime rationale.
- Test expired, malformed, wrong-audience, and privilege-escalation cases.
- Create a key-rotation and incident-response runbook.
- Do not call the prototype production-ready until every checklist item is owned.
CTA: Pick the first unchecked step, complete it now, and save the evidence in your project README or decision log. Then open the official documentation linked below before adopting the result in production.