Nur deine Notizen
Teil des Abschnitts Authentifizierung und Sicherheit der Node.js-Journey von Coddy. Lektion 18 von 24.
Angemeldet zu sein beweist, wer du bist, aber nicht, dass eine Notiz dir gehört. GET /notes/:id findet eine Notiz allein anhand ihrer ID. Daher kann jede angemeldete Person, die eine ID aus einem geteilten Link oder einem Protokoll hat, diese Notiz lesen. Die Prüfung des Eigentümers wird Autorisierung genannt.
Nimm den Eigentümer in die Abfrage selbst auf, damit die Datenbank niemals das Dokument einer anderen Person zurückgibt. Antworte mit 404 statt mit 403, wenn es nicht gefunden wird: 403 würde dem Besucher verraten, dass die Notiz existiert.
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' });
}Jede Abfrage, die auf die Daten eines Benutzers zugreift, benötigt den Filter: Lese-, Aktualisierungs- und Löschabfragen.
Aufgabe
EinfachDieses Kapitel greift den Sitzungs-Login aus Kapitel 2 wieder auf, und der Server ist gewachsen. Er hat jetzt eine Seite unter GET /, Routen für eine einzelne Notiz unter GET /notes/:id und DELETE /notes/:id sowie ein Administratorkonto. Die beiden Routen /notes/:id haben eine Sicherheitslücke: Sie suchen eine Notiz nur anhand ihrer ID. Jeder angemeldete Benutzer, der eine ID kennt, kann die Notiz einer anderen Person lesen oder löschen. Schließe diese Lücke.
- Suche in
server.jsunterGET /notes/:iddie Notiz anhand ihrer_idund ihresownerzusammen. Der Eigentümer muss der angemeldete Benutzer sein:req.user.id. - Mach dasselbe unter
DELETE /notes/:id, damit niemand eine Notiz löschen kann, die ihm nicht gehört. - Wenn keine Notiz übereinstimmt, antworten beide Routen bereits mit dem Status 404 und
{"error": "Note not found"}. Behalte diese Antwort auch für die Notiz einer anderen Person bei. Ein 403 würde dem Besucher verraten, dass die Notiz existiert.
Probiere es aus: Drücke auf „Spielen“.
- Sende
POST /signupund danachPOST /login, beide mit{"username": "ada", "password": "correct horse"}. - Sende
POST /notesmit{"text": "Safe code 4417"}und kopiere die_idaus der Antwort.GET /notes/gefolgt von dieser ID gibt die Notiz zurück. - Registriere dich und melde dich mit
{"username": "bob", "password": "correct horse"}an. SendeGET /notes/gefolgt von Adas ID: Status 404 und{"error": "Note not found"}. SendeDELETE /notes/gefolgt von derselben ID: ebenfalls 404. - Melde dich erneut als Ada an, mit ihrem Body aus dem ersten Schritt. Ihre Notiz ist noch da. Lösche sie jetzt mit
DELETE: Status 204.
Probier es selbst
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());
// Das einzige Admin-Konto. Sein Passwort stammt aus der Umgebung.
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 });
});
// Ermittelt den Benutzer anhand des Session-Cookies oder antwortet mit 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();
});
// Deine Notizen als Webseite.
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>`);
});
// Notizen: Jeder Benutzer liest und schreibt nur seine eigenen.
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();
});
// Für Admins: Wie viele Konten es gibt.
app.get('/admin', requireUser, async (req, res) => {
res.json({ users: await users.countDocuments() });
});
app.listen(3000, () => {
console.log('Vault is running on port 3000');
});
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Authentifizierung und Sicherheit
1Passwörter
Bytes und Hex-TextEinen Passwortschlüssel ableitenEin neues SaltPasswörter niemals speichernSchlüssel-Bytes vergleichenEin Passwort überprüfenRegeln für die Registrierung4Eigentümer und Sicherheit
Nur deine NotizenAdmin-RollenÜbe selbstständig: Express-Playground