Menu
CoddyTech

Routes Behind a Login

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

Every private route needs the session lookup from GET /me. Copied into each route, it will be forgotten once, and that route is open to everyone. Write it once as middleware instead.

Middleware runs before the route. When it puts something on req, like req.user, the route reads it from there. A user's _id is an ObjectId; user._id.toHexString() gives it as text. It either answers and stops, or puts what it found on req and calls next(). Pass it to each route that needs it, before the handler:

function requireAdult(req, res, next) {
    if (Number(req.query.age) < 18) {
        return res.status(403).json({ error: 'Adults only' });
    }
    req.adult = true;
    next();
}

app.get('/wines', requireAdult, (req, res) => res.json(wines));

The return matters: without it the route would run too.

challenge icon

Challenge

Easy

Notes are still open to everyone. Make them private: only a logged in user can read or add notes, and each user sees only their own.

  1. In server.js, write a middleware function requireUser(req, res, next). Move the session lookup from GET /me into it: read the sid cookie, find the session, then find its user.
    • When there is no such user, respond with status 401 and {"error": "Log in first"}, and stop there.
    • Otherwise, put the user on the request as req.user, an object with two fields, then call next(). The two fields are id, the user's _id as text, and username. An ObjectId's toHexString() method gives its text, like "66f1c2a9e4b0a1b2c3d4e5f6".
  2. Pass requireUser to GET /me, GET /notes and POST /notes, before each route's own function. GET /me then responds with {"username": "ada"}, read from req.user.
  3. In POST /notes, save each note with an owner field holding req.user.id. The response is the new note: {"owner": "66f1c2a9e4b0a1b2c3d4e5f6", "text": "Door code 1234", "_id": "66f1c2b0e4b0a1b2c3d4e5f7"}
  4. In GET /notes, return only the notes whose owner is the logged in user.

Try it: Press Run. Send GET /notes: status 401 and {"error": "Log in first"}. Send POST /signup, then POST /login, both with {"username": "ada", "password": "correct horse"}. Send POST /notes with {"text": "Door code 1234"}: status 201 and the note with its owner. Send GET /notes: a list holding that one note. Now sign up and log in with {"username": "bob", "password": "correct horse"}. GET /notes returns [], because bob has written nothing.

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 });
});

app.get('/me', async (req, res) => {
    const { sid } = parseCookies(req.get('cookie'));
    const session = sid && (await sessions.findOne({ _id: sid }));
    if (!session) {
        return res.status(401).json({ error: 'Log in first' });
    }
    const user = await users.findOne({ _id: session.userId });
    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');
});
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Auth and Security

Practice on your own: Express playground