Only Your Notes
Part of the Auth and Security section of Coddy's Node.js journey. Lesson 18 of 24.
Being logged in proves who you are, not that a note is yours. GET /notes/:id finds a note by its id alone, so anyone logged in who has an id, from a shared link or a log, reads that note. Checking the owner is called authorization.
Put the owner in the query itself, so the database never returns someone else's document. Answer 404, not 403, when it is not found: 403 would tell the visitor the note exists.
const order = await orders.findOne({
_id: new ObjectId(req.params.id),
customer: req.user.id,
});
if (!order) {
return res.status(404).json({ error: 'Order not found' });
}Every query that touches one user's data needs the filter: reads, updates and deletes.
Challenge
EasyThis chapter goes back to the session login from chapter 2, and the server has grown. It now has a page at GET /, routes for one note at GET /notes/:id and DELETE /notes/:id, and an admin account. The two /notes/:id routes have a hole: they find a note by its id alone. Any logged in user who knows an id can read or delete someone else's note. Close it.
- In
server.js, inGET /notes/:id, find the note by its_idand itsownertogether. The owner must be the logged in user,req.user.id. - Do the same in
DELETE /notes/:id, so nobody can delete a note they do not own. - When no note matches, both routes already respond with status 404 and
{"error": "Note not found"}. Keep that response for someone else's note too. A 403 would tell the visitor that the note exists.
Try it: Press Run.
- Send
POST /signup, thenPOST /login, both with{"username": "ada", "password": "correct horse"}. - Send
POST /noteswith{"text": "Safe code 4417"}, and copy the_idfrom the response.GET /notes/followed by that id returns the note. - Sign up and log in with
{"username": "bob", "password": "correct horse"}. SendGET /notes/followed by ada's id: status 404 and{"error": "Note not found"}. SendDELETE /notes/followed by the same id: also 404. - Log in as ada again, with her body from the first step. Her note is still there.
DELETEit now: status 204.
Try it yourself
import express from 'express';
import { ObjectId } from 'mongodb';
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());
// The one admin account. Its password comes from the environment.
await users.insertOne({ username: 'admin', role: 'admin', passwordHash: await hashPassword(process.env.ADMIN_PASSWORD) });
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,
sameSite: 'lax',
secure: true,
maxAge: 24 * 60 * 60 * 1000,
});
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, role: user.role };
next();
}
app.get('/me', requireUser, (req, res) => {
res.json({ username: req.user.username });
});
app.post('/logout', async (req, res) => {
const { sid } = parseCookies(req.get('cookie'));
if (sid) {
await sessions.deleteOne({ _id: sid });
}
res.clearCookie('sid');
res.status(204).end();
});
// Your notes as a page.
app.get('/', requireUser, async (req, res) => {
const list = await notes.find({ owner: req.user.id }).toArray();
const items = list.map((note) => `<li>${note.text}</li>`).join('');
res.send(`<!doctype html><title>Vault</title><h1>${req.user.username}'s notes</h1><ul>${items}</ul>`);
});
// 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.get('/notes/:id', requireUser, async (req, res) => {
if (!ObjectId.isValid(req.params.id)) {
return res.status(404).json({ error: 'Note not found' });
}
const note = await notes.findOne({ _id: new ObjectId(req.params.id) });
if (!note) {
return res.status(404).json({ error: 'Note not found' });
}
res.json(note);
});
app.delete('/notes/:id', requireUser, async (req, res) => {
if (!ObjectId.isValid(req.params.id)) {
return res.status(404).json({ error: 'Note not found' });
}
const { deletedCount } = await notes.deleteOne({ _id: new ObjectId(req.params.id) });
if (!deletedCount) {
return res.status(404).json({ error: 'Note not found' });
}
res.status(204).end();
});
// For admins: how many accounts there are.
app.get('/admin', requireUser, async (req, res) => {
res.json({ users: await users.countDocuments() });
});
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
Practice on your own: Express playground