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 bcryptAdd 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.