Login Sessions
Part of the Auth and Security section of Coddy's Node.js journey. Lesson 10 of 24.
Login answers 200, then the next request has forgotten who logged in. A session remembers. At login the server makes an id nobody can guess, stores which user it belongs to, and sends it as a cookie. Each later request brings the id back, and the server looks it up.
The id must be random. A cookie holding the username would let anyone type Cookie: sid=ada and be Ada. randomBytes(24) gives 24 random bytes, 192 bits, which nobody can guess. toString('base64url') writes them as 32 letters, digits, - and _, characters that are safe in a cookie. Set the cookie httpOnly so scripts in the page cannot read it.
const cartId = randomBytes(24).toString('base64url');
await carts.insertOne({ _id: cartId, items: [] });
res.cookie('cart', cartId, { httpOnly: true });
// on a later request
const cart = await carts.findOne({ _id: parseCookies(req.get('cookie')).cart });Challenge
EasyThis step opens the course's prepared private notes API project, rather than your project files from Remembering with Cookies (including server.js). Your earlier work stays in its own lesson. Read the supplied files before editing server.js. This copy contains the private notes signup and login routes.
Login works, but the next request has already forgotten who logged in. Make the server remember, with a session: a random id saved on the server and sent to the browser as a cookie.
- In
server.js, importrandomBytesfromnode:crypto. Importsessionsfrom./db.jsnext tonotesandusers. The lockeddb.jsalready has asessionscollection. - In
POST /login, after the password is checked, make a session id: 24 random bytes written as base64url text. That is 32 characters of letters, digits,-and_, likeoqjRxvxmXl_nRwAwOLxgZyI2sYq9hZtN. Make a new id on every login. - Save the session in
sessionsas a document with two fields:_id: the session id.userId: the logged in user's_id.
- Send the id to the browser as a cookie named
sid, with thehttpOnlyoption, so scripts in the page cannot read it. Login still responds with{"username": "ada"}. - Add
GET /me. It reads thesidcookie, finds the session with that_id, then finds that session's user. It responds with{"username": "ada"}. When there is nosidcookie, or no session has that id, it responds with status 401 and{"error": "Log in first"}. Someone who types a cookie by hand, likesid=ada, must get that 401.
Try it: Press Run. Send POST /signup with {"username": "ada", "password": "correct horse"}. Send GET /me: status 401 and {"error": "Log in first"}. Send POST /login with the same body as signup: status 200. The response's Headers tab shows Set-Cookie: sid=...; Path=/; HttpOnly, with your random id in place of the dots. Send GET /me again. The browser sends the cookie back, so you get {"username": "ada"}. Open Data: sessions holds one document, with your id as its _id and ada's _id as its userId.
Try it yourself
import express from 'express';
import { parseCookies } from './cookies.js';
import { notes, users } from './db.js';
import { hashPassword, verifyPassword } from './passwords.js';
const app = express();
app.use(express.json());
app.post('/signup', async (req, res) => {
const username = String(req.body.username ?? '').trim().toLowerCase();
const password = String(req.body.password ?? '');
if (!username || password.length < 8) {
return res.status(400).json({ error: 'Pick a username and a password of at least 8 characters' });
}
if (await users.findOne({ username })) {
return res.status(409).json({ error: 'That username is taken' });
}
await users.insertOne({ username, passwordHash: await hashPassword(password) });
res.status(201).json({ username });
});
app.post('/login', async (req, res) => {
const username = String(req.body.username ?? '').trim().toLowerCase();
const user = await users.findOne({ username });
if (!user || !(await verifyPassword(String(req.body.password ?? ''), user.passwordHash))) {
return res.status(401).json({ error: 'Wrong username or password' });
}
res.json({ username: user.username });
});
// Counts how many times this browser has called GET /visit.
app.get('/visit', (req, res) => {
const cookies = parseCookies(req.get('cookie'));
const visits = Number(cookies.visits ?? 0) + 1;
res.cookie('visits', String(visits));
res.json({ visits });
});
// Notes. Anyone can read and write them for now.
app.get('/notes', async (req, res) => {
res.json(await notes.find().toArray());
});
app.post('/notes', async (req, res) => {
const note = { text: String(req.body.text ?? '') };
await notes.insertOne(note);
res.status(201).json(note);
});
app.listen(3000, () => {
console.log('Vault is running on port 3000');
});
All lessons in Auth and Security
Practice on your own: Express playground