10 - Authentification : JWT, API keys et OAuth2
Ce que tu vas apprendre
- La différence entre Bearer tokens (JWT) et API keys
- Quand utiliser OAuth2 pour les acces tiers
- Pourquoi 401 et 403 ne veulent pas dire la meme chose
- Le pattern middleware d'authentification
Prerequisites
Avoir lu l'article sur les erreurs. Connaitre les bases de HTTP (headers, cookies). Avoir une idee de ce qu'est un JWT (voir la serie authentification et sécurité).
La première fois que j'ai mis en production une API "sécurisée", j'avais mis le token dans le query string. GET /users?token=abc123. Quelques semaines plus tard, j'ai découvert que Google Analytics loguait les URLs completes, token compris. Lecon apprise : l'endroit ou tu mets les credentials compte autant que les credentials eux-memes.
Bearer tokens (JWT)
Le mecanisme le plus courant pour les APIs modernes. Le client s'authentifié (login), recoit un JWT, et l'envoie dans chaque requête :
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjQyfQ.abc123
Le serveur vérifié la signature du token sans appel a la base de donnees. C'est stateless, ca scale bien, et ca marche avec n'importe quel client (navigateur, mobile, CLI).
typescriptimport jwt from "jsonwebtoken";
function authenticate(req: Request, res: Response, next: NextFunction) {
const header = req.headers.authorization;
if (!header?.startsWith("Bearer ")) {
return res.status(401).json({ type: "unauthorized", title: "Missing token" });
}
try {
const token = header.slice(7);
const payload = jwt.verify(token, process.env.JWT_SECRET!);
req.user = payload;
next();
} catch {
res.status(401).json({ type: "unauthorized", title: "Invalid token" });
}
}
API keys : header vs query param
Les API keys sont faites pour l'authentification machine-to-machine. Un serveur backend qui appelle ton API, un script cron, un outil CI/CD.
Deux options pour les transmettre :
# Header (recommande)
X-API-Key: sk_live_abc123
# Query param (a eviter)
GET /data?api_key=sk_live_abc123
Le header est préférable. Les query params finissent dans les logs serveur, dans l'historique du navigateur, dans les referrers. Le header reste dans la couche HTTP, invisible dans les URLs.
Une API key n'identifié pas un utilisateur, elle identifié une application. Si tu as besoin de savoir quel utilisateur fait la requête, utilise un JWT. Si tu as besoin de savoir quelle application fait la requête, utilise une API key. Tu peux combiner les deux.
OAuth2 pour les acces tiers
OAuth2 entre en jeu quand une application tierce veut acceder aux donnees de tes utilisateurs. L'utilisateur autorise l'acces, l'app tierce recoit un token avec des permissions limitees (scopes).
Le flow le plus courant (Authorization Code) :
- L'app tierce redirige l'utilisateur vers
https://api.paltemps.fr/oauth/authorize?client_id=X&scope=read:profile - L'utilisateur se connecte et autorise
- Ton serveur redirige vers le callback de l'app avec un
code - L'app échange le
codecontre unaccess_token(appel serveur-to-serveur) - L'app utilise le
access_tokenpour appeler ton API
typescript// Etape 4 : echange du code
const response = await fetch("https://api.example.com/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "authorization_code",
code: authCode,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
redirect_uri: REDIRECT_URI
})
});
const { access_token, refresh_token, expires_in } = await response.json();
N'implemente pas OAuth2 toi-meme. Utilise une librairie eprouvee (passport, arctic, lucia).
CORS et credentials
Quand ton front (sur app.example.com) appelle ton API (sur api.example.com), le navigateur bloque les cookies et le header Authorization par défaut. Il faut configurer CORS avec credentials :
typescriptapp.use(cors({
origin: "https://app.example.com",
credentials: true
}));
Attention : credentials: true interdit Access-Control-Allow-Origin: *. Tu dois spécifier l'origin exacte. On en reparle en détail dans l'article sur CORS.
401 vs 403
Deux codes que je vois confondus en permanence :
- 401 Unauthorized : "je ne sais pas qui tu es". Le token est absent, expire ou invalide. Le client doit s'authentifier.
- 403 Forbidden : "je sais qui tu es, mais tu n'as pas le droit". Le token est valide, mais l'utilisateur n'a pas la permission.
typescript// 401 : pas de token
if (!req.user) return res.status(401).json({ type: "unauthorized" });
// 403 : token valide mais role insuffisant
if (req.user.role !== "admin") return res.status(403).json({ type: "forbidden" });
Un 401 peut déclencher une redirection vers la page de login. Un 403 devrait afficher "acces refuse". Si tu renvoies 403 quand le token est expire, le front ne saura jamais qu'il doit re-authentifier l'utilisateur.
Token refresh
Un JWT a une duree de vie courte (15 minutes, 1 heure). Quand il expire, le client utilise un refresh token pour en obtenir un nouveau sans demander a l'utilisateur de se reconnecter :
typescript// Le client detecte un 401
if (response.status === 401 && refreshToken) {
const newTokens = await fetch("/auth/refresh", {
method: "POST",
body: JSON.stringify({ refresh_token: refreshToken })
});
if (newTokens.ok) {
const { access_token } = await newTokens.json();
// Rejouer la requete originale avec le nouveau token
return retryWithToken(originalRequest, access_token);
}
// Le refresh token est aussi expire -> redirection login
redirectToLogin();
}
Le refresh token a une duree de vie plus longue (7 jours, 30 jours) et doit etre stocke de facon sécurisée (cookie httpOnly, pas localStorage).
Le pattern middleware
Separe l'authentification de l'autorisation. Deux middlewares distincts :
typescript// Authentification : qui es-tu ?
const authenticate = (req, res, next) => {
const user = verifyToken(req.headers.authorization);
if (!user) return res.status(401).json({ type: "unauthorized" });
req.user = user;
next();
};
// Autorisation : as-tu le droit ?
const authorize = (...roles: string[]) => (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({ type: "forbidden" });
}
next();
};
// Usage
app.delete("/users/:id", authenticate, authorize("admin"), deleteUser);
C'est lisible, testable, et réutilisable. Chaque route déclaré explicitement ses contraintes de sécurité.
Résumé
- Utilise
Authorization: Bearer <jwt>pour l'authentification utilisateur - Utilise des API keys en header (
X-API-Key) pour le machine-to-machine, jamais en query param - OAuth2 sert pour les acces tiers, pas pour ton propre front
- 401 = "qui es-tu ?", 403 = "tu n'as pas le droit"
- Separe authentification et autorisation dans deux middlewares distincts
- Stocke les refresh tokens dans des cookies httpOnly, pas dans localStorage
Article précédent : Erreurs Article suivant : Versioning