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
EasyNotes 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.
- In
server.js, write a middleware functionrequireUser(req, res, next). Move the session lookup fromGET /meinto it: read thesidcookie, 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 callnext(). The two fields areid, the user's_idas text, andusername. An ObjectId'stoHexString()method gives its text, like"66f1c2a9e4b0a1b2c3d4e5f6".
- When there is no such user, respond with status 401 and
- Pass
requireUsertoGET /me,GET /notesandPOST /notes, before each route's own function.GET /methen responds with{"username": "ada"}, read fromreq.user. - In
POST /notes, save each note with anownerfield holdingreq.user.id. The response is the new note:{"owner": "66f1c2a9e4b0a1b2c3d4e5f6", "text": "Door code 1234", "_id": "66f1c2b0e4b0a1b2c3d4e5f7"} - In
GET /notes, return only the notes whoseowneris 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');
});
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Auth and Security
2Sessions and Cookies
Remembering with CookiesA Session LookupLogin SessionsRoutes Behind a LoginPractice on your own: Express playground