Menu
CoddyTech

Vérifier des jetons

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

Un client API envoie son jeton dans un en-tête à chaque requête : Authorization: Bearer <token>. Le lire avec decodeJwt accepterait n’importe quel jeton saisi par n’importe qui. jwtVerify(token, key) recalcule la signature avec votre secret et lève une exception si elle ne correspond pas, si le jeton a expiré ou s’il ne s’agit pas du tout d’un JWT. Vous ne pouvez faire confiance à payload qu’après son retour.

async function requireDevice(req, res, next) {
    const token = (req.get('authorization') ?? '').replace(/^Bearer /, '');
    try {
        const { payload } = await jwtVerify(token, deviceKey);
        req.device = payload.sub;
    } catch {
        return res.status(401).json({ error: 'Unknown device' });
    }
    next();
}
challenge icon

Défi

Facile

Accepte maintenant les jetons que tu signes. Une application envoie son jeton dans un en-tête à chaque requête, et le serveur vérifie la signature avant de faire confiance à quoi que ce soit qu’il contient.

  1. Dans server.js, importe jwtVerify depuis jose.
  2. Écris un middleware requireUser(req, res, next). Il lit l’en-tête Authorization. Cet en-tête contient le mot Bearer, une espace, puis le jeton. Extrais-en le jeton.
  3. Vérifie le jeton avec jwtVerify, en utilisant la même clé que pour la signature. jwtVerify déclenche une exception lorsque la signature ne correspond pas, lorsque le jeton a expiré ou lorsque le texte n’est pas du tout un jeton. Dans tous ces cas, ainsi que lorsqu’il n’y a pas d’en-tête, réponds avec le statut 401 et {"error": "Log in first"}.
  4. Lorsque le jeton est valide, définis req.user comme un objet contenant id, tiré de la revendication sub du jeton, et username, tiré de sa revendication username. Appelle ensuite next().
  5. Ajoute GET /me avec requireUser. Cette route renvoie req.user : {"id": "66f1c2a9e4b0a1b2c3d4e5f6", "username": "ada"}.

Essaie : Appuie sur « Jouer ». Envoie GET /me : statut 401 et {"error": "Log in first"}. Envoie POST /signup, puis POST /token, les deux avec {"username": "ada", "password": "correct horse"}, et copie le jeton. Dans le champ des en-têtes, ajoute un en-tête nommé Authorization dont la valeur est Bearer, une espace, puis ton jeton. Envoie GET /me : statut 200, avec ton identifiant et "username": "ada". Modifie maintenant une lettre au milieu du jeton, entre les deux points, puis envoie de nouveau GET /me : statut 401, car la signature ne correspond plus.

Essayez vous-même

import express from 'express';
import { SignJWT, decodeJwt } from 'jose';
import { users } from './db.js';
import { hashPassword, verifyPassword } from './passwords.js';

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

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' });
    }
    res.json({ username: user.username });
});

app.post('/token', 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 token = await new SignJWT({ username: user.username })
        .setProtectedHeader({ alg: 'HS256' })
        .setSubject(user._id.toHexString())
        .setIssuedAt()
        .setExpirationTime('1h')
        .sign(secret);
    res.json({ token });
});

// Affiche le contenu d’un jeton : GET /inspect?token=...
app.get('/inspect', (req, res) => {
    try {
        res.json(decodeJwt(String(req.query.token ?? '')));
    } catch {
        res.status(400).json({ error: 'Not a JWT' });
    }
});

// La page d’administration. Aucune affirmation ne peut être considérée comme fiable avant la vérification de sa signature.
app.get('/admin', (req, res) => {
    res.status(403).json({ error: 'Admins only' });
});

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