Menu
CoddyTech

Signer des jetons

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

Un jeton n’a de valeur que si personne d’autre ne peut en créer un. Avec HS256, jose combine l’en-tête et la charge utile avec un secret que seul le serveur connaît, puis hache le résultat. Ce hachage est la signature. Sans le secret, personne ne peut en créer une correspondante.

Le secret provient d’une variable d’environnement, process.env.JWT_SECRET, jamais du code : le code est partagé, copié et envoyé dans le dépôt. Cette leçon définit JWT_SECRET pour toi. jose en a besoin sous forme d’octets.

const key = new TextEncoder().encode(process.env.INVITE_SECRET);

const invite = await new SignJWT({ room: 'design' })
    .setProtectedHeader({ alg: 'HS256' })
    .setSubject(guestId)
    .setIssuedAt()
    .setExpirationTime('2d')
    .sign(key);

setSubject prend du texte, et setExpirationTime prend une durée comme '30m' ou '2d'.

challenge icon

Défi

Facile

Les applications qui appellent votre API, comme une application mobile, ne conservent pas les cookies comme le fait un navigateur. Donnez-leur plutôt un jeton : un JWT signé qu’elles renvoient avec chaque requête. POST /token sert de connexion pour ces applications.

  1. Dans server.js, importez SignJWT depuis jose, à côté de decodeJwt. Au-dessus des routes, convertissez process.env.JWT_SECRET en octets avec un TextEncoder, comme l’indique la leçon, car jose signe avec des octets. Cette leçon définit JWT_SECRET pour vous.
  2. Ajoutez POST /token. Il reçoit le même corps que la route de connexion, {"username": "ada", "password": "correct horse"}, et vérifie le nom d’utilisateur et le mot de passe de la même manière que POST /login. S’ils sont incorrects, renvoyez le statut 401 et {"error": "Wrong username or password"}.
  3. S’ils sont corrects, signez un jeton et renvoyez-le sous la forme {"token": "..."}. Le jeton doit :
    • utiliser l’algorithme HS256, signé avec les octets de JWT_SECRET ;
    • contenir la revendication username dans sa charge utile ;
    • avoir l’_id de l’utilisateur sous forme de texte comme sujet. Utilisez toHexString(), car jose refuse un sujet qui n’est pas du texte ;
    • expirer une heure après sa création.

    Un jeton de cette leçon ressemble à ceci : eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkYSIsInN1YiI6IjY2ZjFjMmE5ZTRiMGExYjJjM2Q0ZTVmNiIsImlhdCI6MTc5MzM2MTYwMCwiZXhwIjoxNzkzMzY1MjAwfQ.EdCrjAKSjZowcjE3-IjKttBLVK1VKKbL9DPnJDLdgug

Essayez : Appuyez sur « Jouer ». Envoyez POST /signup avec {"username": "ada", "password": "correct horse"}. Envoyez POST /token avec le même corps : le statut est 200 et la réponse est {"token": "..."}. Copiez le jeton et envoyez GET /inspect?token=, suivi du jeton. Vous verrez quelque chose comme {"username": "ada", "sub": "66f1c2a9e4b0a1b2c3d4e5f6", "iat": 1793361600, "exp": 1793365200} : exp est postérieur de 3600 secondes à iat. Envoyez POST /token avec {"username": "ada", "password": "wrong horse"} : le statut est 401.

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) => {
    try {
        res.json(decodeJwt(String(req.query.token ?? '')));
    } catch {
        res.status(400).json({ error: 'Not a JWT' });
    }
});

// La page d’administration. Aucune revendication 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');
});

Toutes les leçons de Authentification et sécurité

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