Vos notes uniquement
Fait partie de la section Authentification et sécurité du Journey Node.js de Coddy. Leçon 18 sur 24.
Le fait d’être connecté prouve qui tu es, pas que la note t’appartient. GET /notes/:id trouve une note uniquement à partir de son identifiant : toute personne connectée qui possède un identifiant, obtenu via un lien partagé ou un journal, peut lire cette note. Vérifier le propriétaire s’appelle l’autorisation.
Ajoute le propriétaire à la requête elle-même, afin que la base de données ne renvoie jamais le document de quelqu’un d’autre. Réponds 404, et non 403, si le document est introuvable : 403 indiquerait au visiteur que la note existe.
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' });
}Toute requête qui accède aux données d’un utilisateur doit inclure le filtre : lectures, mises à jour et suppressions.
Défi
FacileCe chapitre revient à la connexion par session du chapitre 2, et le serveur s’est développé. Il a maintenant une page à GET /, des routes pour une note à GET /notes/:id et DELETE /notes/:id, ainsi qu’un compte administrateur. Les deux routes /notes/:id présentent une faille : elles recherchent une note uniquement par son identifiant. Tout utilisateur connecté qui connaît un identifiant peut lire ou supprimer la note d’une autre personne. Corrige cette faille.
- Dans
server.js, dansGET /notes/:id, recherche la note à la fois par son_idet sonowner. Le propriétaire doit être l’utilisateur connecté,req.user.id. - Fais de même dans
DELETE /notes/:id, afin que personne ne puisse supprimer une note qui ne lui appartient pas. - Lorsqu’aucune note ne correspond, les deux routes répondent déjà avec le statut 404 et
{"error": "Note not found"}. Conserve cette réponse pour la note d’une autre personne également. Un statut 403 indiquerait au visiteur que la note existe.
Essaie : appuie sur « Jouer ».
- Envoie
POST /signup, puisPOST /login, les deux avec{"username": "ada", "password": "correct horse"}. - Envoie
POST /notesavec{"text": "Safe code 4417"}, et copie le_idde la réponse.GET /notes/suivi de cet identifiant renvoie la note. - Inscris-toi et connecte-toi avec
{"username": "bob", "password": "correct horse"}. EnvoieGET /notes/suivi de l’identifiant d’Ada : statut 404 et{"error": "Note not found"}. EnvoieDELETE /notes/suivi du même identifiant : le statut est également 404. - Connecte-toi à nouveau en tant qu’Ada, avec le corps de la première étape. Sa note est toujours là. Supprime-la maintenant avec
DELETE: statut 204.
Essayez vous-même
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());
// Le compte administrateur unique. Son mot de passe provient de l’environnement.
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 });
});
// Recherche l’utilisateur associé au cookie de session ou renvoie 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();
});
// Vos notes sous forme de 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 : chaque utilisateur ne lit et n’écrit que les siennes.
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();
});
// Pour les administrateurs : le nombre de comptes existants.
app.get('/admin', requireUser, async (req, res) => {
res.json({ users: await users.countDocuments() });
});
app.listen(3000, () => {
console.log('Vault is running on port 3000');
});
Cette leçon comprend un petit quiz. Commencez la leçon pour y répondre et suivre votre progression.
Toutes les leçons de Authentification et sécurité
1Mots de passe
Octets et texte hexadécimalDériver une clé de mot de passeUn nouveau selNe stockez jamais les mots de passeComparer les octets de cléVérifier un mot de passeRègles d’inscription4Propriétaires et sécurité
Vos notes uniquementRôles d’administrationEntraînez-vous par vous-même : Playground Express