Menu
CoddyTech

Qu'est-ce qu'un JWT

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

Une session nécessite une consultation de la base de données à chaque requête. Un JWT (JSON Web Token) contient lui-même les informations : trois parties séparées par des points. Chaque partie est écrite en base64url, une façon d’écrire des octets avec des lettres, des chiffres, - et _. L’en-tête indique l’algorithme, la charge utile contient les revendications (sub pour indiquer à qui appartient le jeton, exp pour indiquer quand il expire), et la signature prouve que le serveur l’a créé.

Base64url est un encodage, pas un chiffrement. Toute personne qui détient un jeton peut lire sa charge utile :

const payload = token.split('.')[1];
JSON.parse(Buffer.from(payload, 'base64url').toString());
// { sub: '42', name: 'Lin', exp: 1767225600 }

decodeJwt(token) de jose, le paquet JWT, fait la même chose. Il lit la charge utile sans vérifier la signature et déclenche une erreur lorsque le texte n’est pas un JWT. Donc, ne mets jamais de secret dans une charge utile et ne prends jamais de décision à partir d’une revendication que decodeJwt t’a fournie : n’importe qui peut écrire n’importe quelle revendication.

challenge icon

Défi

Facile

Ce chapitre ajoute une deuxième façon de se connecter, avec des jetons, pour les applications qui appellent votre API. Il part d’une version réduite du serveur, avec l’inscription et la connexion, mais sans sessions ni notes. Un JWT contient ses informations à l’intérieur, et tout le monde peut les lire. Créez une route qui affiche ce que contient un jeton, et fermez une page d’administration qui fait confiance à tout ce qu’un jeton affirme.

  1. Dans server.js, écrivez GET /inspect. Cette route lit un jeton dans la chaîne de requête, comme dans GET /inspect?token=eyJ..., et renvoie ses revendications au format JSON. Lisez-les avec decodeJwt, qui est déjà importée. Pour un jeton dont la charge utile est {"sub": "lin", "plan": "free"}, la route renvoie exactement {"sub": "lin", "plan": "free"}.
  2. decodeJwt déclenche une erreur si le texte n’est pas un JWT. Interceptez cette erreur et répondez avec le statut 400 et {"error": "Not a JWT"}. Par exemple, GET /inspect?token=hello renvoie ce statut 400.
  3. Examinez GET /admin. Cette route lit le jeton avec decodeJwt et affiche un secret lorsque le jeton indique "admin": true. Mais decodeJwt ne vérifie jamais la signature, donc n’importe qui peut saisir un jeton qui indique "admin": true. Ce serveur ne peut pas encore vérifier les signatures. Faites donc en sorte que GET /admin réponde avec le statut 403 et {"error": "Admins only"} à chaque requête, quel que soit le jeton reçu. 403 Forbidden signifie « vous ne pouvez pas voir ceci ». Le chapitre 4 ouvrira de nouveau une page d’administration, mais cette fois uniquement pour les véritables administrateurs.

Essayez : appuyez sur Run. La barre de requête commence par GET /admin?token=, suivi de ce jeton : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJtYWxsb3J5IiwiYWRtaW4iOnRydWV9.c2lnbmF0dXJl Un attaquant nommé mallory l’a écrit à la main, sans clé secrète. Envoyez-le avant de modifier quoi que ce soit : la version de départ renvoie {"secret": "The vault key is 42"}. Après votre modification, appuyez de nouveau sur Run et envoyez la même requête : statut 403 et {"error": "Admins only"}. Envoyez GET /inspect?token= suivi du même jeton : {"sub": "mallory", "admin": true}, lu sans aucune clé. Envoyez GET /inspect?token=hello : statut 400 et {"error": "Not a JWT"}.

Essayez vous-même

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

// Affiche le contenu d’un token : GET /inspect?token=...
app.get('/inspect', (req, res) => {
    res.status(501).json({ error: 'Not written yet' });
});

// La page d’administration.
app.get('/admin', (req, res) => {
    const claims = decodeJwt(String(req.query.token ?? ''));
    if (claims.admin === true) {
        return res.json({ secret: 'The vault key is 42' });
    }
    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