Menu
CoddyTech

Logging Out

Part of the Auth and Security section of Coddy's Node.js journey. Lesson 12 of 24.

Clearing the cookie only asks the browser to forget the id. Anyone who copied the id, from a shared computer or a stolen cookie, still holds a key that works. Logging out has to delete the session on the server.

res.clearCookie(name) tells the browser to drop a cookie. Status 204 No Content means it worked and there is nothing to send back: res.status(204).end().

Here is a related route, logging a user out on every device at once:

app.post('/logout-everywhere', requireUser, async (req, res) => {
    await sessions.deleteMany({ userId: new ObjectId(req.user.id) });
    res.clearCookie('sid');
    res.status(204).end();
});
challenge icon

Challenge

Easy

Add a way to log out. Deleting the cookie is not enough on its own, because anyone who copied the session id could keep using it. So the server forgets the session too.

  1. In server.js, add POST /logout. It needs no body.
  2. Read the sid cookie. When there is one, delete the session with that _id from sessions.
  3. Tell the browser to forget the sid cookie.
  4. Respond with status 204 and no body. 204 No Content means it worked and there is nothing to send back. Respond with 204 even when the request had no sid cookie.

Try it: Press Run. Send POST /signup, then POST /login, both with {"username": "ada", "password": "correct horse"}. GET /me returns {"username": "ada"}. Send POST /logout: status 204 and an empty body. Send GET /me again: status 401 and {"error": "Log in first"}. Open Data: sessions is empty. Send POST /logout once more: still 204.

Try it yourself

import express from 'express';
import { randomBytes } from 'node:crypto';
import { parseCookies } from './cookies.js';
import { notes, sessions, 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' });
    }
    const sid = randomBytes(24).toString('base64url');
    await sessions.insertOne({ _id: sid, userId: user._id, createdAt: new Date() });
    res.cookie('sid', sid, { httpOnly: true });
    res.json({ username: user.username });
});

// Finds the user of the session cookie, or answers 401.
async function requireUser(req, res, next) {
    const { sid } = parseCookies(req.get('cookie'));
    const session = sid && (await sessions.findOne({ _id: sid }));
    const user = session && (await users.findOne({ _id: session.userId }));
    if (!user) {
        return res.status(401).json({ error: 'Log in first' });
    }
    req.user = { id: user._id.toHexString(), username: user.username };
    next();
}

app.get('/me', requireUser, (req, res) => {
    res.json({ username: req.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: each user reads and writes only their own.
app.get('/notes', requireUser, async (req, res) => {
    res.json(await notes.find({ owner: req.user.id }).toArray());
});

app.post('/notes', requireUser, async (req, res) => {
    const note = { owner: req.user.id, 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