Authentification API
Stratégies, implémentation et pièges pour sécuriser les endpoints d’une API HTTP.
Vue d’ensemble
| Stratégie | Cas d’usage | Stockage client | Expiration typique |
|---|---|---|---|
| JWT | APIs stateless, microservices | LocalStorage ou httpOnly cookie | Configurée (ex. 15 min) |
| Session cookie | Applications web classiques | Cookie httpOnly | Inactive ou 24 h |
| API key | Machine-à-machine, CLI, webhooks | Variable d’environnement | Illimitée (rotation manuelle) |
| OAuth 2.0 | Authentification tierce (Google, GitHub) | Token en mémoire (browser) | Configurée |
Choix rapide : JWT pour APIs stateless, session pour apps web avec navigateur, API key pour les services qui s’authentifient entre eux.
JWT — JSON Web Token
Un JWT est un token signé en trois parties : header.payload.signature.
Structure
// Header
{
"alg": "HS256",
"typ": "JWT"
}
// Payload (claim)
{
"sub": "42",
"iat": 1720000000,
"exp": 1720003600,
"role": "admin"
}- Header : algorithme de signature (
HS256,RS256) et type. - Payload : claims (sujet, expiration, rôles). Non chiffré — ne jamais y placer de donnée sensible.
- Signature : garantit l’intégrité. Vérifiée avec la clé secrète.
Création (Node.js / Express)
const jwt = require('jsonwebtoken');
const payload = { sub: user.id, role: user.role };
const secret = process.env.JWT_SECRET;
const accessToken = jwt.sign(payload, secret, { expiresIn: '15m' });
const refreshToken = jwt.sign(payload, secret, { expiresIn: '7d' });Vérification
const decoded = jwt.verify(token, secret);
// decoded = { sub: '42', iat: 1720000000, exp: 1720003600, role: 'admin' }
if (decoded.exp < Math.floor(Date.now() / 1000)) {
throw new Error('Token expiré');
}Refresh Token — Renouvellement sans reconnexion
Le flux standard utilise deux tokens :
// 1. Le client envoie le refreshToken au endpoint /refresh
const decoded = jwt.verify(refreshToken, refreshSecret);
// 2. Vérifier que le token est encore valide et non révoqué (base de données)
if (!await isRefreshTokenValid(decoded.jti)) {
throw new Error('Refresh token révoqué');
}
// 3. Émettre un nouvel accessToken
const newAccessToken = jwt.sign(
{ sub: decoded.sub, role: decoded.role },
accessSecret,
{ expiresIn: '15m' }
);
// 4. Révoquer l'ancien refreshToken et en créer un nouveau
await revokeRefreshToken(decoded.jti);
const newRefreshToken = jwt.sign(
{ sub: decoded.sub, jti: crypto.randomUUID() },
refreshSecret,
{ expiresIn: '7d' }
);Piège : un access token expiré ne doit jamais être renvoyé au client sans un refresh. Vérifiez toujours
expcôté serveur à chaque requête protégée.
Stockage côté client
| Emplacement | Sécurités | Risque |
|---|---|---|
httpOnly cookie | opaque au JS, serveur-only access | XSS ne peut pas le voler |
localStorage | accessible au JS | Vulnérable au XSS |
sessionStorage | porté sur la session du navigateur | Pertes après fermeture d’onglet |
Recommandation : pour une API stateless consommée par un SPA, préférer un cookie
httpOnlyavec un token anti-CSRF (double submit ouSameSite=Strict).
Clés API
Utiliser une clé API quand l’authentifié est un service (pas un humain). Pas de flow d’authentification, juste un credential statique.
Quand utiliser une clé API
- Appels server-à-server (webhooks, workers, CI/CD)
- Accès programmatique via CLI
- Intégrations tierces (autres entreprises, partenaires)
Rotation des clés
# Exemple : générer une clé API via la CLI
curl -X POST https://api.exemple.com/v1/api-keys \
-H "Authorization: Bearer eyJhbG...9... \
-H "Content-Type: application/json" \
-d '{"name": "worker-production", "scopes": ["read:posts", "write:posts"]}'
# Réponse
{
"id": "ak_7f8a9b0c1d2e",
"key": "sk_liv...7j8k",
"name": "worker-production",
"scopes": ["read:posts", "write:posts"],
"created_at": "2025-07-04T10:00:00Z"
}Préfixes de clés (notation conventionnelle)
| Préfixe | Usage | Environnement |
|---|---|---|
sk_live_ | Clé de signature | Production |
sk_test_ | Clé de test | Sandbox |
rk_live_ | Clé de lecture | Production (lecture seule) |
pk_live_ | Clé publique | Front-end (limitée) |
Règle : une clé en production ne doit jamais apparaître dans les logs. Masquer les 4 premiers caractères et les 4 derniers :
sk_live_****....3f4g5h6i7j8k.
OAuth 2.0 / OpenID Connect
Flux Client Credentials (machine-à-machine)
Le flux le plus simple pour OAuth 2.0. Aucun utilisateur final impliqué.
curl -X POST https://auth.exemple.com/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=app_client_id" \
-d "client_secret=app_client_secret" \
-d "scope=read:users write:users"
# Réponse
{
"access_token": "eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read:users write:users"
}Rôle de l’ID Token (OpenID Connect)
OpenID Connect ajoute un calque au-dessus d’OAuth 2.0 pour fournir des informations sur l’utilisateur. L’id_token est un JWT signé contenant :
{
"sub": "user-42",
"name": "Arthur Dupont",
"email": "arthur@example.com",
"iss": "https://auth.exemple.com",
"aud": "app_client_id",
"exp": 1720003600,
"iat": 1720000000
}Piège : ne pas confondre
access_token(utilisé pour accéder aux ressources) etid_token(utilisé pour identifier l’utilisateur). Les deux peuvent être différents.
Headers d’authentification
Schéma Authorization: Bearer
Le standard HTTP pour transmettre un token :
GET /api/users/me HTTP/1.1
Host: api.exemple.com
Authorization: Bearer eyJhbG...9...Autres schémas courants
| Schéma | Usage | Exemple |
|---|---|---|
Bearer | JWT, OAuth tokens | Authorization: Bearer eyJhbG... |
Basic | Username/password encodé en base64 | Authorization: Basic dXNlcjpwYXNz |
Token (custom) | API keys simples | Authorization: Token sk_live_4f8a... |
Convention : préférer
Bearerpour les tokens JWT/OAuth etTokenpour les API keys. Les deux fonctionnent mais un choix cohérent simplifie le débogage.
Dépréciation de Basic
// ❌ À éviter en production — les credentials transitaient en base64 (pas chiffrés)
Authorization: Basic YXJ0aHVyOnNlY3JldDEyMw==Piège :
Basicn’est pas « sécurisé » — le base64 est trivialment décodable. Toujours utiliser HTTPS avecBasic, ou mieux, remplacer par un schéma plus robuste.
Middleware d’authentification
Pattern Express (Node.js)
function authMiddleware(req, res, next) {
const header = req.headers.authorization;
if (!header || !header.startsWith('Bearer ')) {
return res.status(401).json({
error: { code: 'MISSING_TOKEN', message: 'Token absent.' }
});
}
const token = header.slice(7);
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({
error: { code: 'TOKEN_EXPIRED', message: 'Token expiré.' }
});
}
return res.status(401).json({
error: { code: 'INVALID_TOKEN', message: 'Token invalide.' }
});
}
}
// Usage
app.get('/api/users/me', authMiddleware, (req, res) => {
res.json({ user: req.user });
});Pattern Laravel (PHP)
// app/Http/Middleware/ApiTokenAuth.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class ApiTokenAuth
{
public function handle(Request $request, Closure $next)
{
$token = $request->bearerToken();
if (!$token) {
return response()->json([
'error' => ['code' => 'MISSING_TOKEN', 'message' => 'Token absent.']
], 401);
}
$user = Auth::guard('api')->user();
if (!$user) {
return response()->json([
'error' => ['code' => 'INVALID_TOKEN', 'message' => 'Token invalide.']
], 401);
}
$request->setUserResolver(function () use ($user) {
return $user;
});
return $next($request);
}
}Registre dans app/Http/Kernel.php :
'api' => [
'throttle:api',
\App\Http\Middleware\ApiTokenAuth::class,
],Recommandation : un middleware d’authentification doit être injactable (
@requires auth) et retourner un401standardisé avec un code erreur machine.
Pièges courants
JWT sans expiration
// ❌ MAUVAIS — le token est valide indéfiniment
jwt.sign(payload, secret);Piège : un JWT sans
expest valide à vie. Si le secret fuit, l’accès compromis est permanent. Toujours définir une expiration courte pour l’access_token(15 min).
Tokens dans les logs
// ❌ MAUVAIS — le token est imprimé en clair dans les logs
app.use((req, res, next) => {
console.log(`Requête ${req.method} ${req.url} — Token: ${req.headers.authorization}`);
next();
});Piège : les tokens dans les logs peuvent être compromis si un accès aux logs est obtenu (ELK, Datadog, CloudWatch). Masquer systématiquement les credentials.
Horodatage serveur vs client
Ne jamais faire confiance à l’horloge du client pour vérifier une expiration. Toujours utiliser l’heure serveur :
// ❌ MAUVAIS — le client peut bidouiller son horloge
const now = new Date(); // heure client
// ✅ BON — heure serveur
const now = Math.floor(Date.now() / 1000);
const exp = decoded.exp;
if (exp < now) {
throw new Error('Token expiré');
}Piège : un utilisateur malveillant peut avancer son horloge pour contourner l’expiration. Côté serveur, utiliser
Date.now()ou l’horloge du serveur.
Renouvellement silent
Si un access_token expire pendant qu’une SPA est ouverte, ne pas rediriger brutalement vers la page de connexion. Le renouvellement silent (via un endpoint /refresh) préserve l’expérience utilisateur.
Checklist d’implémentation
- Schéma d’authentification choisi (JWT, session, API key, OAuth)
- Middleware d’authentification unitaire et testé
- Expiration configurée (
expclaim) sur tous les tokens - Refresh token réversible (stocké en base, révoqué après usage)
- Headers de token masqués dans les logs
- Code d’erreur standardisé (
MISSING_TOKEN,INVALID_TOKEN,TOKEN_EXPIRED) - Vérification de l’heure serveur (pas client)
- Rotation des clés API planifiée et documentée
- HTTPS forcé pour tous les endpoints authentifiés