Menu
CoddyTech

Options des cookies

Fait partie de la section Authentification et sécurité du Journey Node.js de Coddy. Leçon 13 sur 24.

Un cookie sans indicateurs est transmis en HTTP non chiffré, accompagne les requêtes déclenchées par d’autres sites et reste actif jusqu’à la fermeture du navigateur. Quatre options de res.cookie permettent de le sécuriser :

  • httpOnly : les scripts de la page ne peuvent pas le lire.
  • sameSite : 'lax' l’exclut des requêtes envoyées par d’autres sites, sauf lorsque l’utilisateur suit un lien ; 'strict' l’exclut aussi de ces requêtes.
  • secure : transmis uniquement via HTTPS, jamais via HTTP non chiffré, où d’autres personnes sur le réseau pourraient le lire.
  • maxAge : sa durée de vie, en millisecondes.
res.cookie('cart', cartId, {
    httpOnly: true,
    sameSite: 'strict',
    secure: true,
    maxAge: 30 * 60 * 1000, // 30 minutes
});
challenge icon

Défi

Facile

Renforce le cookie sid afin que les navigateurs ne l’envoient qu’aux endroits prévus et l’oublient au bout d’un jour.

  1. Dans server.js, dans POST /login, donne au cookie sid ces quatre options :
    • httpOnly, afin que les scripts de la page ne puissent pas le lire. Cette option est déjà présente.
    • sameSite défini sur 'lax', afin que le navigateur ne l’inclue pas dans les requêtes déclenchées par d’autres sites, sauf si l’utilisateur suit un lien vers ton site.
    • secure, afin qu’il ne soit transmis que par HTTPS.
    • maxAge d’un jour. maxAge est exprimé en millisecondes, et l’en-tête indique la même durée en secondes : un jour apparaît donc sous la forme Max-Age=86400.
  2. La connexion doit toujours fonctionner : après POST /login, GET /me renvoie l’utilisateur.

Essaie : appuie sur « Jouer ». Envoie POST /signup, puis POST /login, avec {"username": "ada", "password": "correct horse"} pour les deux. Ouvre l’onglet En-têtes de la réponse de connexion. Set-Cookie a la forme suivante : sid=oqjRxvxmXl_nRwAwOLxgZyI2sYq9hZtN; Max-Age=86400; Path=/; Expires=Sat, 31 Oct 2026 12:00:00 GMT; HttpOnly; Secure; SameSite=Lax. Envoie GET /me : {"username": "ada"}.

Essayez vous-même

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 });
});

// 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 };
    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();
});

// Compte le nombre de fois que ce navigateur a appelé 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 : 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.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