Node.js · APIs & Data

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.

Advanced 7 min Lesson 27 of 37Examples target Node.js 22 LTS.
0/37 in Node.js
On this page · 7 min · Advanced

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#

JavaScript Illustrative
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#

JavaScriptauth.js Illustrative
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();
});
Expected output
201
401
200 cookie flags: HttpOnly
{"email":"asha@example.com","name":"Asha"}
204
401

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. Passwords are hashed with scrypt and a random salt; only "salt:hash" is stored.
  2. verifyPassword re-hashes the attempt and compares with timingSafeEqual.
  3. A successful login creates a random session id and sends it as an httpOnly cookie.
  4. requireAuth looks up the session and attaches req.user, or returns 401.
  5. 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#

Storing plain or fast-hashed passwords

MD5/SHA-256 are far too fast to resist cracking. Use bcrypt, scrypt or argon2 with a salt.

Different errors for unknown email and wrong password

This lets attackers discover which emails have accounts. Use one generic message.

Session ids in URLs or localStorage

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?
return (req, res, next) => req.user?.role === role ? next() : res.status(403).json(...).
Show solution
JavaScript Illustrative
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#

Authentication quizMultiple choice Question 1 of 3

What should be stored in the users table?

Score so far: 0/0

Want more? Practise all Node.js MCQs · Node.js interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.