REST API design - 10 - Authentification : JWT, API keys et OAuth2

Bearer tokens, API keys, OAuth2, CORS et credentials : tout ce qu'il faut savoir pour sécuriser une API REST.

REST API design 11 / 25
  1. 01 REST API design - 00 - Pourquoi le design de ton API change tout
  2. 02 REST API design - 01 - Les principes REST que personne ne lit
  3. 03 REST API design - 02 - Des URLs qui ont du sens
  4. 04 REST API design - 03 - Les méthodes HTTP, pour de vrai
  5. 05 REST API design - 04 - Les codes de statut HTTP qu'il faut connaître
  6. 06 REST API design - 05 - Body, headers et le diable dans les détails
  7. 07 REST API design - 06 - La pagination, ou comment ne pas tuer ta base
  8. 08 REST API design - 07 - Filtrage et tri sans prise de tête
  9. 09 REST API design - 08 - La validation avec Zod, gardien de ton API
  10. 10 REST API design - 09 - Erreurs : un format que tes clients vont adorer
  11. 11 REST API design - 10 - Authentification : JWT, API keys et OAuth2
  12. 12 REST API design - 11 - Versioning : quand et comment faire évoluer ton API
  13. 13 REST API design - 12 - CORS : comprendre et debugger les erreurs cross-origin
  14. 14 REST API design - 13 - Rate limiting : protéger ton API sans frustrer tes clients
  15. 15 REST API design - 14 - Caching : les bonnes réponses sont celles qu'on n'envoie pas
  16. 16 REST API design - 15 - Upload de fichiers : multipart, signed URLs et streaming
  17. 17 REST API design - 16 - Relations entre ressources : embarquer, lier ou les deux
  18. 18 REST API design - 17 - HATEOAS : des liens dans tes réponses
  19. 19 REST API design - 18 - OpenAPI : le contrat entre ton API et le monde
  20. 20 REST API design - 19 - Documentation : une API non documentee est une API inutile
  21. 21 REST API design - 20 - Testing : tester ton API sans devenir fou
  22. 22 REST API design - 21 - Webhooks : quand c'est ton API qui appelle
  23. 23 REST API design - 22 - Performance : quand chaque milliseconde compte
  24. 24 REST API design - 23 - Sécurité : les attaques que tu vas subir (et comment t'en protéger)
  25. 25 REST API design - 24 - Glossaire : tous les termes REST API en un seul endroit

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) :

  1. L'app tierce redirige l'utilisateur vers https://api.paltemps.fr/oauth/authorize?client_id=X&scope=read:profile
  2. L'utilisateur se connecte et autorise
  3. Ton serveur redirige vers le callback de l'app avec un code
  4. L'app échange le code contre un access_token (appel serveur-to-serveur)
  5. L'app utilise le access_token pour 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

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.