📍 Guide du parcours : Concevoir une API REST — le guide de conception. Cet article approfondit une brique du parcours ; pour la vue d’ensemble, commencez par le guide.
Savoir qui appelle, et lui donner juste ce qu’il faut
Lire le catalogue de la médiathèque est public : n’importe qui peut consulter les livres. Mais créer un emprunt, ajouter un titre, supprimer un adhérent ? Là, il faut savoir qui parle, et vérifier qu’il en a le droit. Ces deux questions — « qui es-tu ? » et « as-tu le droit ? » — portent des noms qu’on confond souvent : l’authentification identifie l’appelant, l’autorisation décide ce qu’il peut faire. Une API bien conçue répond aux deux, dans cet ordre.
Ce tutoriel protège l’API Catalogue avec les trois mécanismes que vous rencontrerez partout : les jetons JWT pour les utilisateurs, OAuth 2 pour déléguer l’accès à des applications tierces, et les clés d’API pour les partenaires serveur à serveur. Vous saurez lequel choisir selon le client, et vous implémenterez une protection par JWT de bout en bout.
🎯 Ce que vous allez apprendre
- Décomposer un JWT et comprendre pourquoi il est signé mais pas chiffré.
- Émettre et vérifier un jeton en Node.js, en blindant la vérification contre l’attaque
alg: none. - Protéger des endpoints avec un middleware d’authentification et distinguer
401de403. - Comprendre les rôles et les flux d’OAuth 2, et savoir quand l’utiliser plutôt qu’un JWT maison.
- Choisir entre JWT, OAuth 2 et clé d’API selon le type d’appelant.
🛠️ Ce que vous allez construire
Un endpoint POST /connexion qui émet un JWT, et un middleware qui protège POST /livres et POST /emprunts : sans jeton valide, l’API répond 401 ; avec un jeton dépourvu du rôle requis, elle répond 403. À la fin, vous aurez une chaîne d’authentification complète et réutilisable.
Prérequis
- Node.js 20+ et un serveur Express qui tourne (le squelette du tutoriel OpenAPI convient parfaitement).
- Savoir envoyer une requête avec un en-tête, avec
curlou un client comme Insomnia. - Les codes
401et403, vus dans le guide du parcours. - ⏱️ Temps estimé : environ 50 minutes.
Étape 1 — Disséquer un JWT
Un JSON Web Token (normalisé par la RFC 7519) est une chaîne en trois parties séparées par des points : en-tête.charge_utile.signature. Chaque partie est encodée en base64url. Voici un jeton réel, raccourci :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxNSIsInJvbGUiOiJiaWJsaW90aGVjYWlyZSIsImV4cCI6MTcwMDAwMDAwMH0.3Vd9k...signature...
Décodez la première partie : c’est l’en-tête, qui annonce l’algorithme de signature. La deuxième est la charge utile (les « claims ») :
// En-tête
{ "alg": "HS256", "typ": "JWT" }
// Charge utile (claims)
{ "sub": "15", "role": "bibliothecaire", "exp": 1700000000 }
Le point capital, que la moitié des incidents de sécurité d’API ignore : un JWT est signé, pas chiffré. N’importe qui peut décoder la charge utile (c’est du base64, pas un coffre-fort). La signature ne cache rien : elle prouve que le contenu n’a pas été modifié et qu’il vient bien de votre serveur. Conséquence directe : ne mettez jamais de secret dans un JWT (mot de passe, numéro de carte). On y met une identité (sub), un rôle, une date d’expiration — rien de confidentiel.
Les claims standardisés méritent d’être connus : sub (le sujet, c’est-à-dire qui), iss (l’émetteur), aud (l’audience visée), exp (l’expiration, en temps Unix), iat (émis à), nbf (« not before », pas valide avant), jti (identifiant unique du jeton).
Étape 2 — Émettre un jeton à la connexion
Quand un bibliothécaire se connecte, le serveur vérifie ses identifiants puis lui remet un JWT qu’il présentera à chaque requête suivante. On utilise la bibliothèque jsonwebtoken (version 9.x au moment d’écrire). Installez-la :
npm install jsonwebtoken
Le secret de signature ne vit jamais dans le code : il vient d’une variable d’environnement. Créez l’endpoint de connexion :
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET; // jamais en dur dans le code
app.post('/connexion', express.json(), (req, res) => {
const { email, motDePasse } = req.body;
// En vrai : vérifier email + hash du mot de passe en base
const utilisateur = verifierIdentifiants(email, motDePasse);
if (!utilisateur) {
return res.status(401).json({ titre: 'Identifiants invalides' });
}
const jeton = jwt.sign(
{ sub: utilisateur.id, role: utilisateur.role }, // claims
SECRET,
{ algorithm: 'HS256', expiresIn: '1h' } // expire dans 1 h
);
res.json({ jeton });
});
L’option expiresIn: '1h' ajoute automatiquement un claim exp une heure dans le futur : passé ce délai, le jeton est refusé. Une durée courte limite les dégâts si un jeton fuit. Pour éviter de redemander le mot de passe toutes les heures, on émet en parallèle un « refresh token » à durée plus longue, stocké côté serveur, qui permet de régénérer un jeton d’accès — un mécanisme qu’on ajoute une fois la base en place.
✅ Point d’étape — Avec un
POST /connexionsur de bons identifiants, vous recevez un JSON contenant un longjeton. Collez-le sur jwt.io pour le décoder : vous devez y lire votresub, votreroleet unexp. C’est la preuve que le jeton est lisible — donc qu’il ne doit contenir aucun secret.
Étape 3 — Vérifier le jeton, en se protégeant de alg: none
À chaque requête protégée, le client envoie le jeton dans l’en-tête Authorization, préfixé par Bearer — c’est le schéma normalisé par la RFC 6750 :
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Le serveur extrait ce jeton et le vérifie. Ici se cache un piège de sécurité classique : l’attaque alg: none. Un attaquant forge un jeton dont l’en-tête annonce "alg": "none" et supprime la signature ; une bibliothèque mal configurée pourrait l’accepter. La parade est de toujours imposer la liste des algorithmes autorisés à la vérification. Écrivez un middleware :
function authentifier(req, res, next) {
const entete = req.headers.authorization || '';
const jeton = entete.startsWith('Bearer ') ? entete.slice(7) : null;
if (!jeton) {
return res.status(401).json({ titre: 'Jeton manquant' });
}
try {
// On épingle l'algorithme : 'none' et RS256 forgé sont rejetés
req.utilisateur = jwt.verify(jeton, SECRET, { algorithms: ['HS256'] });
next();
} catch (e) {
return res.status(401).json({ titre: 'Jeton invalide ou expiré' });
}
}
Le { algorithms: ['HS256'] } n’est pas optionnel : sans lui, la bibliothèque accepterait un autre algorithme que celui attendu. jwt.verify contrôle aussi automatiquement l’expiration et lève une exception si le jeton est périmé — d’où le 401 dans le catch.
✅ Point d’étape — Appliquez
authentifierà un endpoint et testez trois cas : sans en-tête →401 Jeton manquant; avec un jeton bidon →401 Jeton invalide; avec un vrai jeton → la requête passe. Les trois doivent se comporter exactement ainsi.
Étape 4 — Distinguer authentification et autorisation
Le middleware précédent répond à « qui es-tu ? ». Reste « as-tu le droit ? ». Un adhérent authentifié peut emprunter, mais pas ajouter un livre au catalogue : cela demande le rôle bibliothecaire. On écrit un second middleware, qui s’exécute après l’authentification :
function exigerRole(role) {
return (req, res, next) => {
if (req.utilisateur.role !== role) {
// Authentifié, mais pas le bon rôle → 403, pas 401
return res.status(403).json({ titre: 'Action non autorisée pour ce rôle' });
}
next();
};
}
// Ajouter un livre exige le rôle bibliothécaire
app.post('/livres', authentifier, exigerRole('bibliothecaire'), creerLivre);
La distinction 401 contre 403 n’est pas cosmétique : elle dit au client quoi faire. 401 signifie « ton jeton est absent ou périmé, reconnecte-toi » ; 403 signifie « ton jeton est valide, mais cette action t’est interdite — inutile de réessayer ». Un client bien écrit redirige vers la page de connexion sur 401 et affiche un message d’erreur sur 403.
✅ Point d’étape — Avec un jeton de rôle
adherent,POST /livresrenvoie403. Avec un jetonbibliothecaire, il passe. L’autorisation fonctionne par-dessus l’authentification.
Étape 5 — OAuth 2 : quand un tiers agit au nom de l’utilisateur
JWT suffit tant que c’est votre application qui parle à votre API. Mais imaginez qu’une application partenaire veuille, avec l’accord de l’adhérent, lire ses emprunts. Vous n’allez pas lui confier le mot de passe de l’adhérent ! C’est le problème que résout OAuth 2 (RFC 6749) : déléguer un accès limité sans partager les identifiants.
OAuth 2 définit quatre rôles : le propriétaire de la ressource (l’adhérent), le client (l’application partenaire), le serveur d’autorisation (qui émet les jetons) et le serveur de ressources (votre API). Le flux le plus courant et le plus sûr est le code d’autorisation avec PKCE (RFC 7636) :
- L’application partenaire redirige l’adhérent vers votre serveur d’autorisation.
- L’adhérent s’authentifie chez vous et consent à donner accès à ses emprunts (et rien d’autre : c’est la « portée », le
scope). - Votre serveur renvoie un code temporaire à l’application.
- L’application échange ce code contre un jeton d’accès, qu’elle présente ensuite à votre API comme un
Bearerclassique.
L’adhérent n’a jamais livré son mot de passe à l’application, et le jeton délivré ne permet que ce qui a été consenti. Le mécanisme PKCE ajoute une preuve cryptographique qui empêche l’interception du code ; dans la prochaine version de la norme, OAuth 2.1 (encore au stade de brouillon IETF au moment d’écrire), PKCE devient obligatoire pour tous les clients, et les anciens flux fragiles (« implicit » et « mot de passe ») sont retirés. Pour distinguer l’identité de l’utilisateur (« qui est connecté ? ») de la simple délégation d’accès, on ajoute par-dessus OAuth 2 la couche OpenID Connect, qui fournit un jeton d’identité dédié.
En pratique, on n’implémente presque jamais un serveur OAuth 2 soi-même : on s’appuie sur un fournisseur d’identité dédié. Le rôle de concepteur d’API est de valider les jetons reçus et d’en vérifier les portées — exactement le travail du middleware de l’étape 3, appliqué à des jetons émis par ce serveur d’autorisation.
Étape 6 — Les clés d’API : pour les partenaires serveur à serveur
Tout client n’est pas un utilisateur humain. Un service partenaire qui synchronise son propre catalogue avec le vôtre, la nuit, par lots, n’a pas de « connexion » au sens d’un formulaire : il a besoin d’une clé d’API. C’est une longue chaîne secrète, opaque, que le partenaire envoie à chaque requête :
GET /api/livres HTTP/1.1
X-API-Key: 9f86d0818940...e7f63c
La clé d’API est simple mais grossière : elle identifie une application, pas un utilisateur, et ne porte ni rôle fin ni expiration courte par défaut. Quelques règles d’hygiène : transmettez-la dans un en-tête (jamais dans l’URL, où elle finirait dans les journaux et l’historique) ; stockez côté serveur uniquement son empreinte (hash), jamais la clé en clair ; et prévoyez de pouvoir la révoquer et la faire tourner sans interruption. Un middleware de vérification ressemble à celui du JWT, en plus simple : on compare l’empreinte de la clé reçue à celles enregistrées.
✅ Point d’étape — Vous savez maintenant associer chaque type d’appelant à son mécanisme : un humain via votre app → JWT ; une app tierce au nom d’un humain → OAuth 2 ; un service partenaire → clé d’API.
🐞 Pièges fréquents
| Symptôme | Cause probable | Correctif |
|---|---|---|
| Un jeton forgé est accepté | Algorithme non épinglé à la vérification | Passer { algorithms: ['HS256'] } à jwt.verify |
| Des données sensibles fuient via le jeton | Secret placé dans la charge utile (non chiffrée) | Ne mettre que des claims non confidentiels |
| Le client ne sait pas s’il doit se reconnecter | 403 renvoyé là où il fallait 401 |
401 = pas/plus authentifié ; 403 = interdit |
| Le secret JWT se retrouve sur GitHub | Secret en dur dans le code source | Variable d’environnement, hors du dépôt |
| Une clé d’API apparaît dans les logs du proxy | Clé passée en paramètre d’URL | Toujours dans un en-tête (X-API-Key ou Authorization) |
🌍 Réalités du terrain
Le choix de l’expiration des jetons mérite réflexion quand les clients sont sur réseau mobile instable. Un jeton trop court force des reconnexions fréquentes, pénibles sur une connexion qui tombe ; trop long, il devient dangereux s’il fuit. Le couple « jeton d’accès court + jeton de rafraîchissement plus long » est un bon compromis : l’utilisateur reste connecté sans que le jeton réellement présenté à chaque requête ait une longue durée de vie.
Côté coût, l’authentification par JWT a une vertu : comme le jeton est auto-porteur (le serveur le vérifie par signature, sans aller en base), elle évite une requête de session à chaque appel — appréciable quand la base de données est la ressource la plus chère de l’infrastructure. La contrepartie est qu’un JWT ne se révoque pas instantanément : jusqu’à son expiration, il reste valide. Pour les actions critiques, on garde donc une liste de révocation côté serveur, ou on raccourcit l’expiration.
✅ Récapitulatif
Vous avez protégé l’API Catalogue de bout en bout : un endpoint qui émet des JWT, un middleware qui les vérifie en bloquant l’attaque alg: none, et une couche d’autorisation par rôle qui sépare proprement 401 et 403. Vous savez aussi situer OAuth 2 (déléguer un accès limité à un tiers, sans partager le mot de passe) et les clés d’API (identifier un service partenaire). Le réflexe à garder : choisir le mécanisme d’après le type d’appelant, et ne jamais confondre « prouver qui on est » avec « avoir le droit de faire ».
🧾 Aide-mémoire
| Élément | Rôle |
|---|---|
jwt.sign(claims, secret, opts) |
Émettre un jeton signé |
jwt.verify(jeton, secret, {algorithms}) |
Vérifier signature + expiration |
Authorization: Bearer <jeton> |
En-tête de transport du JWT (RFC 6750) |
sub, exp, iat, aud |
Claims standard (RFC 7519) |
401 / 403 |
Non authentifié / authentifié mais interdit |
| Code d’autorisation + PKCE | Flux OAuth 2 recommandé |
X-API-Key |
Clé d’API pour service serveur à serveur |
💪 À vous de jouer
Ajoutez un middleware exigerRole qui accepte plusieurs rôles (par exemple bibliothecaire ou admin), afin qu’un administrateur puisse aussi créer des livres.
Voir une solution
function exigerUnDesRoles(...roles) {
return (req, res, next) => {
if (!roles.includes(req.utilisateur.role)) {
return res.status(403).json({ titre: 'Action non autorisée' });
}
next();
};
}
app.post('/livres', authentifier, exigerUnDesRoles('bibliothecaire', 'admin'), creerLivre);
Le paramètre ...roles collecte tous les rôles acceptés ; includes teste l’appartenance. On garde une seule fonction au lieu d’en multiplier.
Tutoriels frères
- Documenter une API REST avec OpenAPI — déclarer le schéma de sécurité que ce tutoriel met en œuvre.
- Sécuriser une API REST : rate limiting, CORS et validation — la couche de défense qui complète l’authentification.
Pour aller plus loin
- 🔝 Retour au guide : Concevoir une API REST.
- RFC 7519 (JWT) : rfc-editor.org/rfc/rfc7519.html ; RFC 6749 (OAuth 2.0) : rfc-editor.org/rfc/rfc6749.html.
- OAuth 2.1 (brouillon en cours) et bonnes pratiques : oauth.net/2.1.
FAQ
Où stocker le jeton côté client ? Pour une application web, un cookie HttpOnly et Secure protège mieux du vol par script qu’un stockage dans localStorage, accessible au JavaScript de la page. Pour une application mobile ou un client serveur, l’en-tête Authorization est la norme. Le choix dépend de la surface d’attaque du client.
JWT ou session classique ? Les sessions stockées côté serveur se révoquent instantanément mais demandent un accès partagé (base ou cache) à chaque requête. Les JWT sont auto-portés et passent mieux à l’échelle horizontale, au prix d’une révocation moins immédiate. Pour une API sans état distribuée sur plusieurs serveurs, le JWT est souvent le bon choix.
HS256 ou RS256 ? HS256 utilise un secret partagé : simple, mais quiconque peut vérifier peut aussi signer. RS256 utilise une paire de clés : le serveur d’autorisation signe avec la clé privée, et n’importe qui vérifie avec la clé publique, sans pouvoir forger. Dès que l’émetteur et le vérificateur sont des services distincts, RS256 (asymétrique) est préférable.
Une clé d’API est-elle suffisante pour sécuriser une API publique ? Elle identifie l’appelant mais ne chiffre rien et n’expire pas par défaut : elle doit toujours voyager sur HTTPS et s’accompagner d’un rate limiting. Pour des droits fins et une identité d’utilisateur, JWT ou OAuth 2 sont plus adaptés.
Mots-clés : authentification API, autorisation, JWT, OAuth 2, PKCE, clé d’API, jsonwebtoken, Bearer token, 401 403.