Menu
CoddyTech

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.

challenge icon

Défi

Facile

Ce 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.

  1. Dans server.js, dans GET /notes/:id, recherche la note à la fois par son _id et son owner. Le propriétaire doit être l’utilisateur connecté, req.user.id.
  2. Fais de même dans DELETE /notes/:id, afin que personne ne puisse supprimer une note qui ne lui appartient pas.
  3. 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, puis POST /login, les deux avec {"username": "ada", "password": "correct horse"}.
  • Envoie POST /notes avec {"text": "Safe code 4417"}, et copie le _id de la réponse. GET /notes/ suivi de cet identifiant renvoie la note.
  • Inscris-toi et connecte-toi avec {"username": "bob", "password": "correct horse"}. Envoie GET /notes/ suivi de l’identifiant d’Ada : statut 404 et {"error": "Note not found"}. Envoie DELETE /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');
});
quiz iconTestez-vous

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é

Entraînez-vous par vous-même : Playground Express