Authentication
Authentication answers “who are you?”. In Node.js you hash passwords, verify them at login, and then remember the user on later requests with a session cookie or a token.
On this page · 7 min · Advanced
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 7 min
- Difficulty
- Advanced
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Register users with hashed passwords
- Log users in and create a session
- Protect routes with an auth middleware
- Choose between sessions and tokens
Explanation#
Registration: validate the email and password, hash the password (bcrypt/argon2/scrypt), store the hash — never the password.
Login: find the user by email, compare the password with the hash, and on success create a session. Return the same error for “unknown email” and “wrong password” so attackers cannot discover accounts.
Sessions: the server stores a random session id → user mapping and sends the id in a cookie (httpOnly, secure, sameSite). Easy to revoke; needs shared storage (Redis/DB) when you have several servers.
Tokens (JWT): the server signs the user’s claims; the client sends them back on each request. No server storage, but harder to revoke — keep them short-lived.
An auth middleware reads the cookie or token, loads the user, sets req.user and calls next() — or returns 401. Logout deletes the session (or the cookie).
Syntax#
function requireAuth(req, res, next) {
const user = sessions.get(readSessionId(req));
if (!user) return res.status(401).json({ error: 'Please log in' });
req.user = user;
next();
}
Code example: Register, login, protected route and logout with sessions#
import express from 'express';
import { randomBytes, scrypt, timingSafeEqual } from 'node:crypto';
import { promisify } from 'node:util';
const scryptAsync = promisify(scrypt);
async function hashPassword(password) {
const salt = randomBytes(16).toString('hex');
const hash = await scryptAsync(password, salt, 64);
return `${salt}:${hash.toString('hex')}`;
}
async function verifyPassword(password, stored) {
const [salt, hex] = stored.split(':');
const hash = await scryptAsync(password, salt, 64);
return timingSafeEqual(hash, Buffer.from(hex, 'hex'));
}
const users = new Map(); // email → { email, name, passwordHash }
const sessions = new Map(); // sessionId → email
const readSession = req => (req.headers.cookie ?? '').match(/sid=([a-f0-9]+)/)?.[1];
function requireAuth(req, res, next) {
const email = sessions.get(readSession(req));
if (!email) return res.status(401).json({ error: 'Please log in' });
req.user = users.get(email);
next();
}
const app = express();
app.use(express.json());
app.post('/register', async (req, res) => {
const { email, password, name } = req.body;
if (!email?.includes('@') || (password ?? '').length < 8) return res.status(400).json({ error: 'Valid email and 8+ character password required' });
if (users.has(email)) return res.status(409).json({ error: 'Email already registered' });
users.set(email, { email, name, passwordHash: await hashPassword(password) });
res.status(201).json({ email, name });
});
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = users.get(email);
if (!user || !(await verifyPassword(password ?? '', user.passwordHash))) {
return res.status(401).json({ error: 'Invalid email or password' }); // same message for both cases
}
const sid = randomBytes(24).toString('hex');
sessions.set(sid, email);
res.cookie('sid', sid, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 3600_000 }).json({ name: user.name });
});
app.get('/me', requireAuth, (req, res) => res.json({ email: req.user.email, name: req.user.name }));
app.post('/logout', requireAuth, (req, res) => {
sessions.delete(readSession(req));
res.clearCookie('sid').sendStatus(204);
});
const server = app.listen(3020, async () => {
const base = 'http://localhost:3020';
const post = (path, body, cookie) => fetch(base + path, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(cookie && { cookie }) }, body: JSON.stringify(body) });
console.log((await post('/register', { email: 'asha@example.com', password: 'p@ssword1', name: 'Asha' })).status);
console.log((await post('/login', { email: 'asha@example.com', password: 'wrong' })).status);
const login = await post('/login', { email: 'asha@example.com', password: 'p@ssword1' });
const cookie = login.headers.get('set-cookie').split(';')[0];
console.log(login.status, 'cookie flags:', /HttpOnly/i.test(login.headers.get('set-cookie')) ? 'HttpOnly' : 'none');
console.log(await (await fetch(base + '/me', { headers: { cookie } })).text());
console.log((await post('/logout', {}, cookie)).status);
console.log((await fetch(base + '/me', { headers: { cookie } })).status);
server.close();
});
201
401
200 cookie flags: HttpOnly
{"email":"asha@example.com","name":"Asha"}
204
401This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- Passwords are hashed with scrypt and a random salt; only "salt:hash" is stored.
- verifyPassword re-hashes the attempt and compares with timingSafeEqual.
- A successful login creates a random session id and sends it as an httpOnly cookie.
- requireAuth looks up the session and attaches req.user, or returns 401.
- Logout deletes the session server-side, so the old cookie stops working immediately.
Real-world use case#
A server-rendered admin panel and its API use cookie sessions stored in Redis, so admins can be logged out instantly from every device when their account is disabled.
Common mistakes#
MD5/SHA-256 are far too fast to resist cracking. Use bcrypt, scrypt or argon2 with a salt.
This lets attackers discover which emails have accounts. Use one generic message.
They leak through logs, referrers and XSS. Use httpOnly, secure cookies.
Best practices#
- Slow, salted password hashing
- httpOnly + secure + sameSite cookies
- Generic login error messages
- Rate-limit login attempts
Remember this#
Store hashes, never passwords.
Session = random id in an httpOnly cookie → server lookup.
Auth middleware sets req.user or returns 401.
Practice exercise#
Your task: Write a requireRole(role) middleware factory that returns 403 unless req.user.role equals role (assume requireAuth already set req.user).
Need a hint?
Show solution
import express from 'express';
const requireRole = role => (req, res, next) =>
req.user?.role === role ? next() : res.status(403).json({ error: 'Forbidden' });
const app = express();
app.use((req, res, next) => { req.user = { name: 'Ravi', role: 'customer' }; next(); }); // pretend requireAuth
app.get('/admin', requireRole('admin'), (req, res) => res.json({ ok: true }));
const server = app.listen(3021, async () => {
console.log((await fetch('http://localhost:3021/admin')).status); // 403
server.close();
});
Mini quiz#
What should be stored in the users table?
Want more? Practise all Node.js MCQs · Node.js interview questions
My notes#
Saved in this browser.