Meilleures Pratiques de Sécurité des APIs 2026
Meilleures Pratiques de Sécurité des APIs 2026
Guide complet de sécurité pour les APIs REST et GraphQL. Authentification, autorisation, rate limiting.
Introduction : La sécurité des APIs, un enjeu critique en 2026
Les APIs (Application Programming Interfaces) sont le pilier de l'écosystème numérique moderne. En 2026, plus de 90% des applications communiquent via des APIs REST ou GraphQL. Cette omniprésence fait des APIs une cible privilégiée pour les cyberattaques : selon le rapport OWASP 2026, les vulnérabilités liées aux APIs représentent désormais le vecteur d'attaque numéro un pour les applications web.
Ce guide complet couvre les meilleures pratiques de sécurité pour protéger vos APIs contre les menaces actuelles et futures. Que vous développiez une API REST, GraphQL ou gRPC, ces principes s'appliquent universellement.
Chapitre 1 : Authentification robuste
JWT (JSON Web Tokens) : bonnes pratiques
Les JWT restent le mécanisme d'authentification le plus répandu pour les APIs. Voici comment les utiliser correctement :
import jwt from 'jsonwebtoken';
// Configuration sécurisée
const JWT_CONFIG = {
secret: process.env.JWT_SECRET!, // Au moins 256 bits
accessTokenExpiry: '15m', // Court : 15 minutes
refreshTokenExpiry: '7d', // Plus long : 7 jours
algorithm: 'HS256' as const,
issuer: 'api.monsite.com',
audience: 'monsite.com',
};
// Génération de tokens
function generateTokens(userId: string, role: string) {
const accessToken = jwt.sign(
{ sub: userId, role },
JWT_CONFIG.secret,
{
expiresIn: JWT_CONFIG.accessTokenExpiry,
algorithm: JWT_CONFIG.algorithm,
issuer: JWT_CONFIG.issuer,
audience: JWT_CONFIG.audience,
}
);
const refreshToken = jwt.sign(
{ sub: userId, type: 'refresh' },
JWT_CONFIG.secret,
{
expiresIn: JWT_CONFIG.refreshTokenExpiry,
algorithm: JWT_CONFIG.algorithm,
}
);
return { accessToken, refreshToken };
}
// Vérification du token
function verifyToken(token: string): TokenPayload {
try {
return jwt.verify(token, JWT_CONFIG.secret, {
algorithms: [JWT_CONFIG.algorithm],
issuer: JWT_CONFIG.issuer,
audience: JWT_CONFIG.audience,
}) as TokenPayload;
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
throw new ApiError(401, 'Token expiré');
}
if (error instanceof jwt.JsonWebTokenError) {
throw new ApiError(401, 'Token invalide');
}
throw new ApiError(500, 'Erreur de vérification du token');
}
}
Erreurs courantes avec les JWT
- Ne stockez jamais les JWT dans le localStorage : vulnérable aux attaques XSS. Utilisez des cookies HttpOnly, Secure, SameSite.
- Ne mettez pas de données sensibles dans le payload : les JWT sont encodés, pas chiffrés. Tout le monde peut lire le contenu.
- Utilisez des durées de vie courtes pour les access tokens (15 minutes max).
- Implémentez une liste de révocation pour les tokens compromis.
// Middleware d'authentification Express
function authMiddleware(req: Request, res: Response, next: NextFunction) {
// Extraire le token du cookie HttpOnly (préféré)
const token = req.cookies?.accessToken
// Ou du header Authorization en fallback
|| req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Authentification requise' });
}
// Vérifier si le token est dans la liste de révocation
if (isTokenRevoked(token)) {
return res.status(401).json({ error: 'Token révoqué' });
}
try {
const payload = verifyToken(token);
req.user = payload;
next();
} catch (error) {
return res.status(401).json({ error: error.message });
}
}
OAuth 2.0 et OpenID Connect
Pour les APIs qui nécessitent une authentification tierce, implémentez OAuth 2.0 avec PKCE :
// Configuration OAuth 2.0 avec PKCE (Proof Key for Code Exchange)
const oauthConfig = {
authorizationEndpoint: 'https://auth.exemple.com/authorize',
tokenEndpoint: 'https://auth.exemple.com/token',
clientId: process.env.OAUTH_CLIENT_ID,
redirectUri: 'https://monsite.com/callback',
scopes: ['openid', 'profile', 'email'],
};
// Étape 1 : Générer le code verifier et le challenge
function generatePKCE() {
const codeVerifier = crypto.randomBytes(32).toString('base64url');
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url');
return { codeVerifier, codeChallenge };
}
Passkeys : l'avenir de l'authentification
En 2026, les Passkeys (WebAuthn) deviennent le standard pour l'authentification sans mot de passe :
// Enregistrement d'un passkey
async function registerPasskey(userId: string) {
const options = await generateRegistrationOptions({
rpName: 'Mon Application',
rpID: 'monsite.com',
userID: userId,
userName: 'utilisateur@exemple.com',
attestationType: 'none',
authenticatorSelection: {
residentKey: 'preferred',
userVerification: 'preferred',
},
});
return options;
}
Chapitre 2 : Autorisation granulaire
RBAC (Role-Based Access Control)
// Définition des rôles et permissions
const PERMISSIONS = {
admin: ['read', 'write', 'delete', 'manage_users'],
editor: ['read', 'write'],
viewer: ['read'],
} as const;
type Role = keyof typeof PERMISSIONS;
type Permission = (typeof PERMISSIONS)[Role][number];
// Middleware d'autorisation
function authorize(...requiredPermissions: Permission[]) {
return (req: Request, res: Response, next: NextFunction) => {
const userRole = req.user?.role as Role;
if (!userRole || !PERMISSIONS[userRole]) {
return res.status(403).json({ error: 'Rôle non reconnu' });
}
const userPermissions = PERMISSIONS[userRole];
const hasPermission = requiredPermissions.every(
(perm) => userPermissions.includes(perm)
);
if (!hasPermission) {
return res.status(403).json({
error: 'Permissions insuffisantes',
required: requiredPermissions,
current: userPermissions,
});
}
next();
};
}
// Utilisation
app.delete(
'/api/users/:id',
authMiddleware,
authorize('delete', 'manage_users'),
deleteUserHandler
);
ABAC (Attribute-Based Access Control)
Pour des cas plus complexes, ABAC offre une granularité supérieure :
// Politiques basées sur les attributs
interface Policy {
effect: 'allow' | 'deny';
conditions: Condition[];
}
interface Condition {
attribute: string;
operator: 'equals' | 'in' | 'greaterThan' | 'lessThan';
value: unknown;
}
// Exemple : un utilisateur ne peut modifier que ses propres articles
const editArticlePolicy: Policy = {
effect: 'allow',
conditions: [
{ attribute: 'user.id', operator: 'equals', value: 'resource.authorId' },
{ attribute: 'resource.status', operator: 'in', value: ['draft', 'review'] },
],
};
Chapitre 3 : Rate Limiting et protection contre les abus
Implémentation du rate limiting
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
// Rate limiter global
const globalLimiter = rateLimit({
store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requêtes par fenêtre
standardHeaders: true, // Envoyer les headers RateLimit-*
legacyHeaders: false,
message: {
error: 'Trop de requêtes. Veuillez réessayer plus tard.',
retryAfter: 'Consultez le header Retry-After',
},
});
// Rate limiter strict pour l'authentification
const authLimiter = rateLimit({
store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
windowMs: 15 * 60 * 1000,
max: 5, // Seulement 5 tentatives
skipSuccessfulRequests: true,
message: {
error: 'Trop de tentatives de connexion. Réessayez dans 15 minutes.',
},
});
// Rate limiter par API key
const apiKeyLimiter = rateLimit({
keyGenerator: (req) => req.headers['x-api-key'] as string || req.ip,
windowMs: 60 * 1000, // 1 minute
max: 60, // 60 requêtes par minute
});
app.use('/api/', globalLimiter);
app.use('/api/auth/login', authLimiter);
app.use('/api/v1/', apiKeyLimiter);
Protection contre les attaques par force brute
// Verrouillage progressif du compte
const loginAttempts = new Map<string, { count: number; lockedUntil?: Date }>();
async function handleLogin(email: string, password: string) {
const attempts = loginAttempts.get(email) || { count: 0 };
// Vérifier le verrouillage
if (attempts.lockedUntil && attempts.lockedUntil > new Date()) {
const waitTime = Math.ceil(
(attempts.lockedUntil.getTime() - Date.now()) / 1000
);
throw new ApiError(
429,
`Compte temporairement verrouillé. Réessayez dans ${waitTime} secondes.`
);
}
const user = await authenticateUser(email, password);
if (!user) {
attempts.count++;
// Verrouillage progressif
if (attempts.count >= 5) {
const lockDuration = Math.min(
Math.pow(2, attempts.count - 5) * 60 * 1000,
30 * 60 * 1000 // Maximum 30 minutes
);
attempts.lockedUntil = new Date(Date.now() + lockDuration);
}
loginAttempts.set(email, attempts);
throw new ApiError(401, 'Identifiants incorrects');
}
// Réinitialiser les tentatives après un succès
loginAttempts.delete(email);
return user;
}
Chapitre 4 : Validation des entrées
Validation stricte avec Zod
Ne faites jamais confiance aux données entrantes. Validez chaque paramètre :
import { z } from 'zod';
// Schéma de validation pour la création d'utilisateur
const CreateUserSchema = z.object({
name: z.string()
.min(2, 'Le nom doit contenir au moins 2 caractères')
.max(100, 'Le nom ne peut pas dépasser 100 caractères')
.regex(/^[a-zA-ZÀ-ÿ\s'-]+$/, 'Caractères non autorisés dans le nom'),
email: z.string()
.email('Adresse email invalide')
.toLowerCase(),
password: z.string()
.min(12, 'Le mot de passe doit contenir au moins 12 caractères')
.regex(/[A-Z]/, 'Le mot de passe doit contenir au moins une majuscule')
.regex(/[a-z]/, 'Le mot de passe doit contenir au moins une minuscule')
.regex(/[0-9]/, 'Le mot de passe doit contenir au moins un chiffre')
.regex(/[^A-Za-z0-9]/, 'Le mot de passe doit contenir au moins un caractère spécial'),
age: z.number()
.int('L\'âge doit être un nombre entier')
.min(13, 'Vous devez avoir au moins 13 ans')
.max(150, 'Âge invalide')
.optional(),
});
// Middleware de validation
function validate<T>(schema: z.ZodSchema<T>) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: 'Données invalides',
details: result.error.issues.map(issue => ({
field: issue.path.join('.'),
message: issue.message,
})),
});
}
req.body = result.data;
next();
};
}
app.post('/api/users', validate(CreateUserSchema), createUserHandler);
Pour tester et valider vos structures de données JSON, utilisez le formateur JSON et le testeur de regex de ToolBox Hub.
Protection contre les injections SQL
// MAUVAIS : injection SQL possible
const query = `SELECT * FROM users WHERE id = '${userId}'`;
// BIEN : requête paramétrée
const result = await db.execute({
sql: 'SELECT * FROM users WHERE id = ?',
args: [userId],
});
// BIEN : utilisation d'un ORM (Drizzle)
const user = await db.query.users.findFirst({
where: eq(users.id, userId),
});
Protection contre les attaques XSS
import DOMPurify from 'isomorphic-dompurify';
// Nettoyer les entrées HTML
function sanitizeHtml(input: string): string {
return DOMPurify.sanitize(input, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href', 'target'],
});
}
// Échapper les caractères spéciaux pour les sorties non-HTML
function escapeHtml(str: string): string {
const map: Record<string, string> = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
return str.replace(/[&<>"']/g, (char) => map[char]);
}
Chapitre 5 : Headers de sécurité
Configuration des headers HTTP
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'strict-dynamic'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'", 'https://api.monsite.com'],
fontSrc: ["'self'", 'https://fonts.googleapis.com'],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
},
},
crossOriginEmbedderPolicy: true,
crossOriginOpenerPolicy: true,
crossOriginResourcePolicy: { policy: 'same-site' },
hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
}));
Configuration CORS
import cors from 'cors';
const corsOptions = {
origin: (origin: string | undefined, callback: Function) => {
const allowedOrigins = [
'https://monsite.com',
'https://app.monsite.com',
];
// Autoriser les requêtes sans origin (outils de dev, Postman)
if (!origin && process.env.NODE_ENV === 'development') {
return callback(null, true);
}
if (origin && allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Non autorisé par CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
exposedHeaders: ['X-RateLimit-Limit', 'X-RateLimit-Remaining'],
maxAge: 86400, // Cache preflight pour 24h
};
app.use(cors(corsOptions));
Chapitre 6 : Sécurité spécifique à GraphQL
Les APIs GraphQL présentent des défis de sécurité uniques :
Limiter la profondeur et la complexité des requêtes
import depthLimit from 'graphql-depth-limit';
import { createComplexityRule } from 'graphql-query-complexity';
const server = new ApolloServer({
schema,
validationRules: [
// Limiter la profondeur de requête à 7 niveaux
depthLimit(7),
// Limiter la complexité totale
createComplexityRule({
maximumComplexity: 1000,
estimators: [
fieldExtensionsEstimator(),
simpleEstimator({ defaultComplexity: 1 }),
],
onComplete: (complexity) => {
if (complexity > 500) {
console.warn(`Requête complexe détectée : ${complexity}`);
}
},
}),
],
});
Désactiver l'introspection en production
const server = new ApolloServer({
schema,
introspection: process.env.NODE_ENV !== 'production',
});
Chapitre 7 : Logging et monitoring de sécurité
Journalisation structurée
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
redact: {
paths: ['req.headers.authorization', 'req.body.password', 'req.body.token'],
censor: '[CENSURÉ]',
},
});
// Middleware de logging sécurisé
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
logger.info({
method: req.method,
url: req.originalUrl,
statusCode: res.statusCode,
responseTime: Date.now() - start,
userAgent: req.headers['user-agent'],
ip: req.ip,
userId: req.user?.sub,
});
});
next();
});
Détection d'anomalies
Surveillez les patterns suspects :
- Pics inhabituels de requêtes d'un même utilisateur ou IP
- Tentatives d'accès à des endpoints inexistants (scan d'API)
- Erreurs 401/403 répétées (tentatives de brute force)
- Requêtes avec des payloads anormalement grands
- Patterns d'injection SQL ou XSS dans les paramètres
Checklist de sécurité des APIs
Utilisez cette checklist pour auditer la sécurité de vos APIs :
- Authentification JWT avec tokens à durée de vie courte
- Refresh tokens stockés de manière sécurisée
- Rate limiting sur tous les endpoints
- Validation stricte de toutes les entrées
- Protection contre les injections SQL (requêtes paramétrées)
- Headers de sécurité configurés (HSTS, CSP, CORS)
- HTTPS obligatoire (TLS 1.3)
- Logging structuré sans données sensibles
- Gestion d'erreurs qui ne divulgue pas d'informations internes
- Dépendances à jour et auditées régulièrement
- Tests de pénétration réguliers
- Plan de réponse aux incidents documenté
Conclusion
La sécurité des APIs est un processus continu qui nécessite une vigilance constante. Les techniques présentées dans ce guide constituent une base solide, mais la sécurité évolue aussi vite que les menaces. Restez informé des dernières vulnérabilités, auditez régulièrement votre code et adoptez une approche de défense en profondeur.
Pour valider vos configurations JSON et tester vos expressions régulières de validation, utilisez le formateur JSON et le testeur de regex de ToolBox Hub. Ces outils sont essentiels dans votre workflow de sécurité quotidien.