Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

User login and logout

Authentication means checking who is there: a visitor enters an email and a password, the server checks them and remembers the user in the session. Passwords must never be stored as plain text — the database keeps a hash: a string from which the original password cannot be recovered, but which an entered password can be checked against.

Table and model

npm install bcrypt

Add a users table in src/db.js:

CREATE TABLE IF NOT EXISTS users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  email TEXT NOT NULL UNIQUE,
  password_hash TEXT NOT NULL
);

And create the model src/models/user.js:

// src/models/user.js
import bcrypt from 'bcrypt';
import db from '../db.js';
 
export async function create(email, password) {
  const hash = await bcrypt.hash(password, 12);
  const result = db.prepare('INSERT INTO users (email, password_hash) VALUES (?, ?)').run(email, hash);
  return Number(result.lastInsertRowid);
}
 
export async function verify(email, password) {
  const user = db.prepare('SELECT * FROM users WHERE email = ?').get(email);
 
  if (!user || !(await bcrypt.compare(password, user.password_hash))) {
    return null;
  }
  return user;
}

bcrypt.hash() turns a password into a hash, and the number 12 sets how much work this takes: guessing passwords by brute force becomes very slow. bcrypt.compare() checks a password against a hash.

Register, log in, log out

The controller src/controllers/authController.js stores only the user's id in the session:

// src/controllers/authController.js
import * as User from '../models/user.js';
 
export async function register(req, res) {
  const id = await User.create(req.body.email, req.body.password);
  req.session.userId = id;
  res.redirect('/posts');
}
 
export async function login(req, res, next) {
  const user = await User.verify(req.body.email, req.body.password);
 
  if (!user) {
    return res.status(401).render('auth/login', { title: 'Log in', error: 'Wrong email or password' });
  }
 
  req.session.regenerate((error) => {
    if (error) return next(error);
    req.session.userId = user.id;
    res.redirect('/posts');
  });
}
 
export function logout(req, res) {
  req.session.destroy(() => res.redirect('/'));
}

regenerate() issues a new session after login: this way an attacker cannot plant their own session number on a visitor in advance. destroy() deletes the whole session on logout.

The routes are added as usual: router.post('/register', ...), router.post('/login', ...), router.post('/logout', ...). Logout is a POST form with a single button, not a link.

Protecting routes

Create the middleware src/middleware/requireAuth.js:

// src/middleware/requireAuth.js
export function requireAuth(req, res, next) {
  if (!req.session.userId) {
    return res.redirect('/login');
  }
  next();
}

And put it before the handlers that are available only after login:

// src/routes/posts.js
import { requireAuth } from '../middleware/requireAuth.js';
 
router.get('/new', requireAuth, postsController.create);
router.post('/', requireAuth, postsController.store);

Open /posts/new without logging in and the browser goes to /login. After login the form opens.

Common mistake

Do not reveal what exactly is wrong: "no such email" or "wrong password". That lets anyone check whether a person is registered on the site. Answer with one general message, "Wrong email or password", as in login() above.

Thanks for reading!

Donate to the site — we work hard for you.