ToolBox Hub

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.

16 mars 202611 min de lecture

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

  1. Ne stockez jamais les JWT dans le localStorage : vulnérable aux attaques XSS. Utilisez des cookies HttpOnly, Secure, SameSite.
  2. 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.
  3. Utilisez des durées de vie courtes pour les access tokens (15 minutes max).
  4. 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> = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#039;',
  };
  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.

Articles associés