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.
Défi
FacileCe 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.
- Dans
server.js, écrivezGET /inspect. Cette route lit un jeton dans la chaîne de requête, comme dansGET /inspect?token=eyJ..., et renvoie ses revendications au format JSON. Lisez-les avecdecodeJwt, 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"}. decodeJwtdé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=hellorenvoie ce statut 400.- Examinez
GET /admin. Cette route lit le jeton avecdecodeJwtet affiche un secret lorsque le jeton indique"admin": true. MaisdecodeJwtne 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 queGET /adminré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');
});
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