API Security

Clés API

Une clé API est un identifiant à longue durée de vie pour les machines. Émettez-la une seule fois, ne stockez que son hash, limitez strictement son périmètre (scope), et prévoyez un moyen de la renouveler ou de la révoquer avant même d'en avoir besoin.

intermediate14 min readUpdated 16 sept. 2026
keys.ts
ts
// keys.ts
import crypto from "node:crypto";

export function generateApiKey(env: "live" | "test") {
  const secret = crypto.randomBytes(32).toString("base64url");
  const prefix = `sk_${env}_`;
  const key = `${prefix}${secret}`;

  return {
    key,                        // returned to the user once
    prefix: key.slice(0, 12),   // stored and indexed for lookup
    hash: hashKey(key),         // stored instead of the key
  };
}

export function hashKey(key: string): string {
  return crypto.createHash("sha256").update(key).digest("hex");
}

export function timingSafeEqual(a: string, b: string): boolean {
  const left = Buffer.from(a);
  const right = Buffer.from(b);
  return left.length === right.length && crypto.timingSafeEqual(left, right);
}
Utilisation
API serveur-à-serveur et API publiques
Format
Préfixe suivi d'un secret aléatoire
Stockage
Hash SHA-256
Affichage
Une seule fois, à la création
Transport
En-tête Authorization
Périmètre
Permissions et limites de débit
Rotation
Créer la nouvelle clé, puis révoquer l'ancienne
Risque de fuite
Historique Git, logs, code client

Pourquoi c'est important

Ce qu'apporte un bon système de clés

Un identifiant, plusieurs services

Une clé authentifie une machine sans flux de connexion. Elle est simple à émettre, à envoyer et à renouveler, c'est pourquoi toutes les plateformes développeurs les utilisent.

Hachée au repos

Ne stockez qu'un hash, exactement comme vous le feriez pour un mot de passe. Un dump de base de données volé ne fournira alors rien qu'un attaquant puisse envoyer à votre API.

Scope et limitation de débit

Chaque clé possède ses propres permissions et quotas ; ainsi, une clé compromise ne peut effectuer que les actions autorisées, et seulement à la vitesse permise.

Le tableau complet

Trois propriétés d'une clé sécurisée

Un secret aléatoire impossible à deviner, un hash au repos pour éviter les attaques par rejeu après un dump, et un scope pour limiter les dégâts en cas de fuite.

Secret

Identifier

Une chaîne aléatoire à haute entropie constitue l'identifiant lui-même. Son seul rôle est d'être impossible à deviner et unique pour chaque appelant.

Scope

Limiter

Une clé transporte un ensemble de permissions. Le vérificateur contrôle l'action par rapport au scope avant l'exécution du handler, exactement comme les rôles d'un utilisateur.

Quota

Protéger

Les clés créent un compartiment naturel pour les limites de débit, afin qu'une intégration trop gourmande n'épuise pas la capacité pour tous les autres.

HTML5 en un coup d'oeil

Les composants que vous allez construire

Format

Un préfixe lisible plus un long secret aléatoire, ex: sk_live_8f2a...

Hachage

Stockez un hash SHA-256 ; comparez avec une fonction en temps constant.

Scopes

Permissions granulaires telles que projects:read et deploy:write.

Limites de débit

Associez un quota à chaque clé et appliquez-le par clé.

Rotation

Émettez un remplacement, migrez le trafic, puis révoquez l'ancienne clé.

Surveillance

Suivez last_used_at et alertez en cas de changements soudains de comportement.

Modèle de données

La table api_keys

Seuls un hash et un préfixe court sont stockés. La clé complète n'existe qu'une fois, dans la réponse qui l'a créée, et ne peut jamais être récupérée.

La table api_keysTable PostgreSQL
  • idbigserialClé primaire technique
  • nametextLibellé humain tel que 'Déploiement CI' ou 'App mobile'
  • prefixtextPremiers caractères de la clé, indexés pour une recherche rapide
  • key_hashtextSHA-256 de la clé complète, jamais la clé elle-même
  • scopestext[]Permissions que la clé peut exercer
  • owner_idbigintL'utilisateur ou le service qui a créé la clé
  • last_used_attimestamptzMis à jour à chaque utilisation pour la détection d'anomalies et le nettoyage
  • revoked_attimestamptzDéfini quand la clé est révoquée ; null signifie active

Seuls un hash et un préfixe court sont stockés. La clé complète n'existe qu'une fois, dans la réponse qui l'a créée, et ne peut jamais être récupérée.

Flux

Émission et vérification d'une clé

La clé complète n'existe que dans une seule réponse ; tout le reste fonctionne à partir d'un hash et d'un préfixe.

  1. 1

    Générer un secret aléatoire

    Tirez au moins 128 bits d'un CSPRNG et combinez-les avec un préfixe lisible.

  2. 2

    L'afficher une seule fois

    Renvoyez la clé complète dans la réponse de création et ne la stockez ni ne l'affichez plus jamais.

  3. 3

    Stocker un hash et un préfixe

    Persistez le hash, le préfixe court, les scopes et le propriétaire dans la table api_keys.

  4. 4

    L'envoyer dans un en-tête

    Le client présente la clé dans l'en-tête Authorization ou un en-tête dédié à chaque requête.

  5. 5

    Hacher et comparer

    Recherchez la clé par son préfixe, hachez la valeur présentée et comparez en temps constant.

  6. 6

    Attacher les scopes

    Chargez les permissions et le quota de la clé dans la requête pour que le handler puisse les appliquer.

  7. 7

    Renouveler ou révoquer

    Émettez un remplacement, migrez le trafic et marquez l'ancienne clé comme révoquée.

Le guide complet

Clés API: Tout ce que vous devez savoir

Qu’est-ce qu’une clé API ?

Une clé API est une chaîne de caractères secrète et durable qui identifie une application plutôt qu’une personne. Le client l’envoie avec chaque requête, le serveur la reconnaît, et l’accès est accordé selon les permissions associées à cette clé. C’est tout simplement ainsi que cela fonctionne.

C’est un identifiant volontairement simple. Il n’y a pas de connexion, pas d’écran de consentement et pas d’échange de jetons. Un développeur s’inscrit, crée une clé, la colle dans un fichier de configuration, et son code commence à fonctionner. C’est cette absence de friction qui explique pourquoi presque toutes les plateformes pour développeurs — paiements, cartes, e-mails, infrastructure — distribuent des clés.

Cependant, simple ne signifie pas négligent. Une clé est un identifiant de type “bearer” (au porteur) : quiconque la détient peut l’utiliser, exactement comme avec de l’argent liquide. Il n’y a pas de second facteur ni de signature. Cela signifie que la sécurité de l’ensemble du système repose sur la qualité de la génération des clés, le soin apporté à leur stockage, la précision de leur périmètre d’action et la rapidité avec laquelle vous pouvez révoquer une clé en cas de fuite. Ce guide a pour but de vous aider à maîtriser ces quatre aspects.

Quand utiliser une clé

Les clés ne sont pas la solution à tous les problèmes d’authentification, et les utiliser à tort crée des risques réels.

Optez pour une API key lorsqu’un serveur communique avec un autre serveur, lorsque l’appelant est une application à laquelle vous pouvez confier un secret à longue durée de vie, ou lorsque vous proposez une API publique aux développeurs. Les pipelines CI, les intégrations backend, les agents de monitoring et les services tiers sont des cas d’usage naturels. La clé est stockée dans un gestionnaire de secrets ou une variable d’environnement, et elle ne transite jamais par un navigateur.

Optez pour OAuth lorsque vous devez agir au nom d’un utilisateur. Si votre intégration doit lire le calendrier de quelqu’un ou envoyer des e-mails en son nom, vous avez besoin d’un flux de consentement et d’un token représentant cette délégation. Une clé ne peut pas exprimer : « ceci sont les données d’Alice et Alice a donné son accord ».

Optez pour un JWT lorsque vous avez besoin d’un token vérifiable à courte durée de vie, transportant des claims et pouvant être validé sans base de données partagée. L’authentification de service à service dans un mesh, ou une URL de téléchargement signée, en sont de bons exemples.

Le cas dangereux est celui du client public. Une clé intégrée dans une application mobile, un binaire desktop ou un bundle navigateur n’est pas secrète : n’importe qui peut l’extraire. Si votre produit nécessite que ces clients appellent votre API, placez un proxy backend devant, ou émettez des tokens à courte durée de vie depuis votre propre serveur après avoir authentifié l’utilisateur. N’expédiez jamais de clé à longue durée de vie à l’intérieur du code client.

L’anatomie d’une bonne clé

Une bonne clé doit être imprévisible et auto-descriptive. Elle se compose de deux parties : un préfixe court et lisible, et un secret long et aléatoire.

The shape of an API key
sk_live_8f2a9c1d4e6b7a0f3c5d8e2b1a4f7c9d6e3b0a8f5c2d1e4b7a9c6f0d3e8b1a4
prefixenvironment and type, safe to log and scan for
secret256 bits of cryptographically secure randomness

Le préfixe remplit deux fonctions. Il permet d’identifier l’environnement et le type de la clé en un coup d’œil, et il fournit au serveur un identifiant indexé pour la recherche sans avoir à stocker le secret. Un format fixe et reconnaissable rend également les fuites accidentelles détectables par les scanners de secrets, qui peuvent repérer sk_live_ dans un commit et le bloquer.

Le secret doit provenir d’une source aléatoire cryptographiquement sécurisée, et jamais de Math.random, d’un horodatage ou d’un UUID qui trahirait une structure. 128 bits d’entropie constituent le minimum ; 256 bits sont un choix par défaut confortable et ne coûtent rien. L’encodage Base64url permet de coller la clé en toute sécurité dans des URLs et des headers sans avoir besoin d’échappement.

const secret = crypto.randomBytes(32).toString("base64url");
const key = `sk_live_${secret}`;

Résistez à la tentation d’encoder des informations dans la clé, comme l’ID de l’utilisateur ou une date de création. Tout élément lisible dans la clé est une information dont un attaquant peut tirer profit, et tout élément dérivé de données prévisibles affaiblit le caractère aléatoire. Le préfixe est la seule partie lisible dont vous avez besoin, et il ne doit révéler aucune donnée sensible.

Affichez-la une seule fois, puis oubliez-la

La clé complète ne doit exister qu’à un seul endroit après sa création : la réponse renvoyée à l’utilisateur. À partir de ce moment, le serveur ne stocke plus qu’un hash et un préfixe, ce qui signifie qu’il peut vérifier une clé présentée, mais ne pourra jamais la reproduire.

C’est la même propriété que pour le stockage des mots de passe, et cela change radicalement les conséquences d’une faille de sécurité. Si un attaquant récupère le contenu de la table api_keys, il n’obtiendra que des hashs qui ne peuvent pas être envoyés à votre API. Sans hachage, une simple fuite de base de données ou l’exposition d’une sauvegarde livrerait instantanément les clés de tous vos clients.

L’expérience utilisateur découle de cette contrainte technique. Le tableau de bord affiche la clé une seule fois, avec un avertissement clair demandant de la copier immédiatement, puis n’affiche plus que le préfixe et des métadonnées telles que les scopes et la date de dernière utilisation. Si un utilisateur perd sa clé, il doit la renouveler (rotate) ; il ne peut pas la récupérer. Précisez-le explicitement dans votre documentation pour que personne ne s’attende à la retrouver plus tard.

res.status(201).json({
  id: row.id,
  name: row.name,
  prefix: row.prefix,
  key, // shown once, never retrievable again
  warning: "Store this key now. You will not be able to see it again.",
});

Stockez un hash, jamais la clé

Le hachage est le contrôle le plus important d’un système de clés API. C’est également celui que les équipes oublient le plus souvent, généralement parce qu’elles souhaitent pouvoir afficher la clé à nouveau plus tard. Ne le faites pas.

Utilisez un hash cryptographique rapide tel que SHA-256. Contrairement aux mots de passe, les clés possèdent une entropie complète ; il n’y a donc pas de dictionnaire d’attaque et aucun besoin pour un KDF lent. Le hash rapide permet également de maintenir un coût de vérification faible sur le chemin critique.

function hashKey(key: string): string {
  return crypto.createHash("sha256").update(key).digest("hex");
}

La comparaison doit être effectuée en temps constant. Un === naïf sur des chaînes de caractères s’arrête dès le premier octet différent, ce qui révèle quelle partie d’une clé devinée était correcte. C’est généralement une préoccupation théorique sur un réseau, mais c’est trivial à éviter et c’est une bonne pratique d’hygiène. Comparez des buffers de longueur égale avec crypto.timingSafeEqual.

const a = Buffer.from(hashKey(presented));
const b = Buffer.from(record.keyHash);
const ok = a.length === b.length && crypto.timingSafeEqual(a, b);

Si vous souhaitez masquer complètement le hash dans la base de données, un hash clé avec un “pepper” côté serveur ajoute un second secret que l’attaquant doit également obtenir. C’est optionnel pour les systèmes de haute sécurité, mais le hachage, lui, n’est pas optionnel.

Rechercher des clés sans scan complet

Le hachage pose un problème pratique : vous ne pouvez pas interroger la base de données par hash à moins de hacher la clé entrante au préalable, et vous ne pouvez pas hacher la clé entrante tant que vous ne savez pas avec quelle ligne la comparer. Hacher chaque ligne à chaque requête n’est pas envisageable.

La solution est l’index de préfixe. Stockez les douze premiers caractères de la clé dans une colonne prefix en texte clair avec un index unique. Lorsqu’une requête arrive, lisez le préfixe, trouvez la seule ligne correspondante, puis hachez la clé complète présentée et comparez-la au key_hash de cette ligne. Une seule recherche indexée, un seul hachage, une seule comparaison en temps constant.

const prefix = key.slice(0, 12);
const record = await db.apiKey.findByPrefix(prefix);
if (!record || record.revokedAt) return res.status(401).end();

const presented = hashKey(key);
if (!timingSafeEqual(presented, record.keyHash)) {
  return res.status(401).end();
}

Quelques détails permettent de garantir la robustesse du système. Le préfixe doit être assez long pour que les collisions soient rares, mais assez court pour rester un label utile ; douze caractères est un choix courant. Si une collision survient, l’index unique échoue lors de la création et vous régénérez la clé. Renvoyez la même erreur pour un préfixe inconnu et un secret erroné, afin que la réponse ne révèle pas si un préfixe existe. Enfin, mettez à jour last_used_at de manière asynchrone ou via une écriture groupée (batch), car une mise à jour synchrone à chaque requête transforme une lecture en écriture et double la charge de votre base de données.

Associer les clés à des permissions et des limites

Une clé capable de tout faire est une clé dont la fuite serait catastrophique. Limitez chaque clé au plus petit ensemble de permissions nécessaires à l’accomplissement de sa tâche.

Les scopes ne sont que des chaînes de permissions, les mêmes atomes utilisés par le RBAC : projects:read, deploy:write, billing:manage. Stockez-les sur la clé, attachez-les à la requête après vérification, et appliquez-les exactement comme vous le feriez pour les permissions d’un utilisateur.

router.post(
  "/deployments",
  apiKeyAuth(),
  requireScope("deploy:write"),
  createDeployment
);

C’est ici que le principe du moindre privilège devient concret. Une intégration de monitoring n’a besoin que de metrics:read. Un pipeline CI a besoin de deploy:write mais jamais de billing:manage. Un partenaire en lecture seule reçoit des scopes de lecture et rien d’autre. Lorsqu’une clé fuite, le rayon d’impact est limité à son scope, c’est pourquoi la configuration par défaut devrait être une liste restreinte qu’un humain étend délibérément.

Les scopes rendent également les rate limits naturels et équitables. Puisque chaque clé est un appelant distinct, vous pouvez associer un quota par clé et l’appliquer dans votre couche de limitation de débit, afin qu’un script incontrôlé ne puisse pas consommer la capacité destinée à tous. Les plans tarifaires sont souvent mappés directement sur le quota : les clés gratuites ont un plafond bas, les clés payantes un plafond plus élevé.

Préfixes d’environnement

Les préfixes ne sont pas là pour la décoration. Ils encodent l’environnement et le type d’une clé, ce qui permet d’éviter l’un des modes de panne les plus courants et embarrassants : une clé de test pointant vers la production, ou une clé de production utilisée dans une suite de tests qui vient ensuite modifier des données réelles.

Une convention courante consiste à utiliser sk_live_ pour les clés secrètes en production et sk_test_ pour le sandbox. Les clés publiables ou publiques peuvent utiliser pk_live_. Le nommage vous revient, mais restez cohérent et documentez-le, car vos utilisateurs s’appuieront dessus pour savoir d’un coup d’œil ce qu’une clé peut modifier.

sk_live_...  secret key, production, full access within its scopes
sk_test_...  secret key, sandbox, no real data
pk_live_...  publishable key, safe to embed in a browser

Le préfixe permet également à votre serveur de router les requêtes vers le bon environnement avant même toute recherche. Une clé sk_test_ présentée à l’API de production peut être rejetée immédiatement avec un message clair, plutôt que de provoquer une erreur d’authentification opaque. Et parce que le préfixe est fixe et distinctif, les scanners de secrets et les outils de revue de code peuvent être configurés pour le signaler.

Envoyer une clé : header ou query

L’endroit où la clé transite est aussi important que la manière dont elle est stockée. Envoyez-la dans un header.

GET /v1/projects HTTP/1.1
Host: api.example.com
Authorization: Bearer sk_live_8f2a9c...

Les headers ne sont pas inclus dans les logs d’accès par défaut, n’apparaissent pas dans l’historique du navigateur et ne sont pas transmis dans le header Referer lorsqu’une page renvoie vers un autre site. Ils sont également faciles à masquer dans les logs et les proxys. Le header Authorization avec un schéma Bearer est conventionnel et supporté par la plupart des clients HTTP et des outils ; un header X-API-Key dédié convient tout autant.

Les query strings ne sont pas l’endroit approprié. Elles sont capturées par les logs d’accès, mises en cache par des intermédiaires, stockées dans l’historique du navigateur et les outils d’analytics, et fuitent via le Referer. Une clé dans une URL est une clé présente dans une douzaine d’endroits que vous ne contrôlez pas.

GET /v1/projects?api_key=sk_live_8f2a9c... HTTP/1.1

Si un client ne peut réellement pas définir de headers — comme dans certains scénarios de webhooks legacy ou d’intégration (embed) — utilisez plutôt un token à courte durée de vie et à portée restreinte dans la query string, et faites-le expirer rapidement. N’acceptez jamais une clé à longue durée de vie dans une URL, et si vos logs risquent d’en contenir, nettoyez-les.

Rotation et révocation

Toute clé devra tôt ou tard être changée. Un employé quitte l’entreprise, un ordinateur portable est perdu, une clé apparaît dans un dépôt public, ou une politique de routine impose simplement une rotation périodique. Prévoyez cela dès le départ, car intégrer la rotation a posteriori est fastidieux.

La rotation consiste à émettre un remplacement sans interrompre le service pour le client. Le schéma est le suivant : créez une nouvelle clé avec les mêmes scopes, retournez-la, autorisez les deux clés à fonctionner pendant une courte fenêtre de chevauchement, puis révoquez l’ancienne. C’est ce chevauchement qui permet une rotation sans interruption de service (zero-downtime), et le fait de publier sa durée permet aux intégrateurs de s’organiser.

// 1. Issue the replacement and return it to the owner.
// 2. Keep both keys valid for the overlap window (e.g. 24 hours).
// 3. Revoke the old key, or let it expire automatically.
await db.apiKey.revoke(oldKeyId);

La révocation est immédiate et permanente. Définissez revoked_at sur la ligne, et faites en sorte que le middleware de vérification rejette toute clé ayant une valeur non nulle. Ne supprimez pas la ligne : la conserver permet de maintenir la piste d’audit et d’empêcher la réutilisation du même préfixe. La révocation doit prendre effet dès la requête suivante, ce qui est facile lorsque la clé est vérifiée via la base de données, mais impossible lorsqu’il s’agit d’un token autonome.

Prévoyez la possibilité de révoquer une seule clé, toutes les clés d’un utilisateur, ou toutes les clés d’un tenant. Les deux dernières options correspondent au bouton « je pense que nous avons été piratés », et elles doivent être accessibles en un clic. Alertez le propriétaire chaque fois qu’une clé est créée ou révoquée, afin qu’un attaquant ayant accédé au tableau de bord ne puisse pas générer discrètement un nouvel identifiant.

Fuites : git, logs et code client

La plupart des compromissions de clés API ne sont pas des attaques sophistiquées. Il s’agit simplement d’une clé qui se trouve là où elle ne devrait pas être.

Le contrôle de source est le cas classique. Une clé collée dans un fichier de configuration, un fixture de test ou un .env qui est commité reste dans l’historique git pour toujours, même si un commit ultérieur la supprime. Scannez vos commits et vos pull requests pour détecter des motifs de clés, conservez vos secrets dans un gestionnaire ou des variables CI, et effectuez une rotation immédiate si l’une d’elles est commitée. Partez du principe qu’une clé présente dans un dépôt public est déjà compromise.

Les logs sont les fuites les plus discrètes. Un logger de requêtes qui affiche les URLs complètes, les headers ou les corps de requêtes peut capturer des millions de clés. Configurez votre logger pour masquer les headers Authorization et X-API-Key, ne loggez jamais les corps de requêtes sur les endpoints d’authentification, et auditez vos logs pour repérer des chaînes de caractères ressemblant à des clés.

Le code côté client est l’erreur fatale. Une clé dans un bundle navigateur, une application mobile ou un binaire desktop peut être extraite en quelques minutes. Aucune technique d’obfuscation ne peut corriger cela. Placez un serveur en amont ou émettez des tokens à courte durée de vie.

Autres points à vérifier : les messages d’erreur et les stack traces, les outils d’analytics tiers, les captures d’écran dans les rapports de bugs, et les ordinateurs de développeurs synchronisés avec une sauvegarde partagée. Considérez chacun de ces points comme une fuite potentielle et donnez aux utilisateurs les outils nécessaires pour réagir lorsque cela arrive.

Surveillance de l’utilisation et des anomalies

Une clé qui n’est jamais surveillée ne peut pas être protégée. Enregistrez suffisamment d’informations sur chaque utilisation pour détecter les usages abusifs et répondre aux questions après un incident.

Au minimum, stockez last_used_at et la source de la requête. À partir de là, vous pouvez mettre en place des alertes pour détecter les schémas critiques : une clé utilisée pour la première fois depuis des mois, un pic soudain du taux de requêtes, un pays d’origine qui ne correspond pas à l’intégration, ou une rafale d’erreurs 401 suggérant que quelqu’un tente de deviner les préfixes.

logger.info({
  event: "api_key.used",
  keyId: record.id,
  ownerId: record.ownerId,
  route: req.path,
  ip: req.ip,
});

Ne loguez jamais la clé elle-même, seulement son id et son préfixe. Affichez l’utilisation dans le tableau de bord pour que les clients puissent voir quelles clés sont actives et repérer celles qu’ils ne reconnaissent pas. Envoyez un e-mail lors de la création, de la rotation et de la révocation, et donnez aux propriétaires un moyen de désactiver instantanément une clé suspecte.

Pour une approche plus large de la protection d’une API contre les abus, le guide sur le rate limiting traite des quotas, de la gestion des pics de trafic et de la manière dont les limites par clé s’articulent.

Politiques d’expiration et de cycle de vie

Une clé sans date d’expiration est une clé que vous oublierez jusqu’au jour où elle fuira. Attribuez un cycle de vie à chaque clé, même si le délai par défaut est généreux.

Une expiration optionnelle permet à un utilisateur de créer une clé qui expire à une date choisie, ce qui est idéal pour un prestataire, une intégration temporaire ou une migration ponctuelle. Une durée de vie maximale absolue limite la longévité de n’importe quelle clé, après quoi la rotation devient obligatoire. De nombreuses plateformes combinent les deux : les clés ont une durée par défaut d’un an, peuvent être plus courtes, mais ne peuvent jamais dépasser deux ans.

Suivez un état plutôt qu’un simple booléen. Une clé peut être active, en passe à expirer, expirée ou révoquée, et chaque état mérite une réponse différente. Les clés approchant de l’expiration devraient déclencher l’envoi d’un e-mail afin que le propriétaire effectue la rotation avant une interruption de service, et le middleware de vérification doit traiter les clés expirées et révoquées de la même manière : rejet avec une erreur 401.

function isUsable(key: ApiKey): boolean {
  if (key.revokedAt) return false;
  if (key.expiresAt && key.expiresAt < new Date()) return false;
  return true;
}

L’expiration est un filet de sécurité, pas un substitut à la révocation. Une clé volée qui expire dans un an reste dangereuse ; la rotation et la surveillance demeurent donc les principaux contrôles. Cependant, une limite d’expiration signifie qu’une clé oubliée, ou celle d’un employé ayant quitté l’entreprise, finira par cesser de fonctionner d’elle-même.

Tester l’authentification par clé API

La vérification des clés API représente une petite quantité de code qui protège un accès considérable ; testez-la donc rigoureusement, en privilégiant les cas d’erreur (tests négatifs).

import request from "supertest";
import app from "../app.js";

test("rejects a request with no key", async () => {
  await request(app).get("/v1/projects").expect(401);
});

test("rejects a malformed key", async () => {
  await request(app)
    .get("/v1/projects")
    .set("Authorization", "Bearer not-a-real-key")
    .expect(401);
});

test("rejects a revoked key", async () => {
  const { key } = await createKey();
  await revokeKey(key);
  await request(app)
    .get("/v1/projects")
    .set("Authorization", `Bearer ${key}`)
    .expect(401);
});

test("rejects a key without the required scope", async () => {
  const { key } = await createKey({ scopes: ["projects:read"] });
  await request(app)
    .post("/v1/deployments")
    .set("Authorization", `Bearer ${key}`)
    .expect(403);
});

Ajoutez des tests prouvant que la clé complète n’est pas stockée : créez une clé, inspectez la ligne en base de données et vérifiez que key_hash est bien le hash et que le texte en clair n’apparaît nulle part. Vérifiez que last_used_at s’incrémente lors de l’utilisation. Testez également explicitement le chevauchement lors de la rotation : l’ancienne et la nouvelle clé doivent toutes deux fonctionner pendant la fenêtre de transition, et seule la nouvelle doit être valide une fois celle-ci fermée.

Enfin, vérifiez que les réponses d’erreur ne font pas de distinction entre un préfixe inconnu et un secret erroné. Les deux doivent retourner le même statut et le même corps de réponse, sinon vous offrez à un attaquant un moyen de confirmer quels préfixes existent.

Création de l’interface de gestion des clés

L’écran de gestion est l’endroit où les propriétés de sécurité deviennent visibles pour les utilisateurs ; il doit donc rendre le comportement recommandé le plus simple à adopter.

La vue en liste affiche pour chaque clé son nom, son préfixe, ses scopes, sa date de création et sa dernière utilisation, mais jamais le secret. Elle propose un bouton de révocation avec une confirmation, et fait de la “création de clé” le chemin privilégié pour la rotation. Le flux de création permet à l’utilisateur de choisir les scopes et une expiration optionnelle, puis affiche la clé complète une seule fois avec un bouton de copie et un avertissement sans équivoque.

sk_live_8f2a...   CI deploy      deploy:write       created 3 days ago
sk_live_1c4b...   Monitoring     metrics:read       last used 2 minutes ago
sk_test_9a7d...   Staging        projects:read      revoked yesterday

Quelques détails utiles à inclure : un horodatage de “dernière utilisation” pour que les utilisateurs puissent repérer les clés qu’ils ne reconnaissent pas, une révocation en un clic pour une seule clé, une option “tout révoquer” pour le compte, et des notifications par e-mail à chaque création et révocation. Si vous affichez un graphique d’utilisation, faites-le par clé, car c’est l’unité de référence pour l’utilisateur.

Ne créez pas de bouton “révéler la clé”. Cela ne peut pas exister si vous utilisez un hachage correct, et son absence est une fonctionnalité : cela signifie qu’une fuite de base de données est surmontable. Expliquez dans l’interface que les clés ne sont affichées qu’une seule fois et que la rotation est la méthode de récupération, afin que cette contrainte paraisse délibérée et non comme un dysfonctionnement.

Choisir l’encodage et la longueur

Le choix de l’encodage est une décision mineure avec quelques conséquences pratiques. Le Base64url est le choix le plus courant car il est compact, sûr pour les URLs et les headers, et sensible à la casse, ce qui maximise l’entropie par caractère. L’hexadécimal (hex) est plus long, mais plus facile à lire à haute voix et à retrouver dans les logs. Le Base62 se situe entre les deux et évite totalement + et /.

Encodage 128 bits 256 bits Notes
base64url 22 caract. 43 caract. Compact, sûr pour les URLs, sensible à la casse
hex 32 caract. 64 caract. Plus long, facile à copier, insensible à la casse
base62 22 caract. 43 caract. Alphanumérique uniquement, sans symboles

Quel que soit votre choix, gardez le secret sensible à la casse et ne le convertissez jamais en minuscules avant la comparaison. Un bug courant provient d’un middleware ou d’un proxy qui normalise les valeurs des headers, cassant ainsi silencieusement les clés contenant des majuscules. Documentez le format exact et rendez le préfixe suffisamment distinctif pour qu’une clé soit reconnaissable dans un ticket de support sans être utile à un attaquant.

Ne rendez pas la clé auto-descriptive au-delà du préfixe. Intégrer l’ID utilisateur, une somme de contrôle (checksum) ou une date d’expiration à l’intérieur de la clé peut vous tenter de sauter la requête en base de données, mais cela signifie également que la clé transporte des informations et ne peut de toute façon pas être révoquée sans une vérification. Gardez le secret opaque et laissez la base de données gérer la sémantique.

Stocker les secrets client en toute sécurité

Le stockage côté serveur n’est qu’une partie de l’équation ; le client doit également garder la clé en sécurité. Fournissez aux intégrateurs des conseils clairs et tranchés, car le réflexe de nombreux développeurs est de coller une clé dans un fichier et de l’envoyer via un commit.

Recommandez l’utilisation de variables d’environnement pour les serveurs, injectées par la plateforme ou un gestionnaire de secrets plutôt que d’être écrites dans un .env versionné. En CI, utilisez le magasin de secrets du pipeline et masquez la valeur dans les logs. Pour le développement local, chargez-les depuis un .env ignoré par Git, et privilégiez des clés sk_test_ afin qu’une erreur n’affecte que les données de sandbox.

# Load from the environment, never hard-code.
export MYAPP_API_KEY="sk_live_8f2a9c..."
curl -H "Authorization: Bearer $MYAPP_API_KEY" https://api.example.com/v1/projects

Orientez les utilisateurs vers des gestionnaires de secrets managés — AWS Secrets Manager, Google Secret Manager, Vault, ou les variables intégrées de la plateforme — et expliquez la rotation des clés avec des instructions concrètes. Si votre SDK lit la clé depuis une variable d’environnement par défaut, la plupart des intégrations adopteront le bon comportement sans même avoir besoin d’instructions, ce qui constitue le meilleur type de contrôle de sécurité.

Clés secrètes et clés publiables

De nombreuses plateformes fournissent deux types de clés, et les confondre peut causer de graves problèmes. Une clé secrète authentifie un serveur de confiance et ne doit jamais être exposée. Une clé publiable est destinée à être intégrée dans le code client et peut être rendue publique sans risque, car elle ne confère aucun privilège par elle-même.

sk_live_...  secret, server-only, carries scopes and a quota
pk_live_...  publishable, client-safe, identifies the account only

Une clé publiable est utile pour l’attribution et la limitation du débit (rate limiting) dans le navigateur, mais chaque opération privilégiée doit toujours être autorisée par un élément que le client ne peut pas falsifier : un jeton à courte durée de vie généré par votre serveur, ou une session utilisateur. Considérez la clé publiable comme un identifiant et non comme un identifiant d’authentification, et ne laissez jamais sa seule présence accorder un accès.

Nommer ces deux clés de manière cohérente rend la distinction évidente au premier coup d’œil et permet aux scanners de secrets, aux revues de code et à la documentation de renforcer la même règle. Si un développeur colle par erreur une clé sk_ dans le code front-end, le préfixe seul devrait rendre l’erreur visible avant le déploiement.

Bonnes pratiques

  • Générez le secret à partir d’un CSPRNG avec au moins 128 bits d’entropie, préfixé pour faciliter la lecture et l’identification de l’environnement.
  • Ne stockez qu’un hash SHA-256 et un court préfixe ; ne persistez jamais la clé complète.
  • Affichez la clé complète une seule fois, lors de sa création, et faites de la rotation le moyen de récupération.
  • Comparez les hashs en temps constant et retournez la même erreur pour les clés inconnues et invalides.
  • Indexez le préfixe afin que la vérification ne nécessite qu’une seule recherche et un seul hash.
  • Limitez chaque clé aux permissions minimales et associez-lui une limite de débit (rate limit) spécifique.
  • Encodez l’environnement dans le préfixe et rejetez une clé de sandbox sur l’API de production.
  • Acceptez les clés dans le header Authorization, jamais dans une query string.
  • Supportez la rotation sans interruption de service (zero-downtime) avec une fenêtre de chevauchement, et rendez la révocation immédiate.
  • Surveillez last_used_at, alertez en cas d’anomalies et notifiez les propriétaires à chaque modification d’identifiants.
  • Séparez les clés secrètes des clés publiables, et ne permettez jamais à une clé publiable d’accorder un accès par elle-même.
  • Documentez le format des clés, les scopes et la politique de rotation pour que les intégrateurs puissent s’y conformer sans tâtonner.

Erreurs courantes

  • Stocker des clés en texte brut en supposant que la base de données ne fuitera jamais.
  • Hasher chaque ligne pour trouver une correspondance au lieu d’indexer un préfixe.
  • Utiliser Math.random ou un UUID comme secret, ce qui réduit l’espace de recherche.
  • Intégrer une clé à longue durée de vie dans un bundle navigateur, une application mobile ou un binaire desktop.
  • Placer la clé dans une query string, où les logs et l’historique peuvent la capturer.
  • Accorder un accès complet à chaque clé parce que la définition de scopes semblait être un travail superflu.
  • Ne jamais faire de rotation ou définir d’expiration pour les clés, rendant une fuite utile indéfiniment.
  • Supprimer les lignes révoquées et perdre ainsi la piste d’audit.
  • Logger l’intégralité des headers ou des URLs, faisant fuiter des clés dans les outils d’observabilité.
  • Retourner une erreur différente pour un préfixe inconnu que pour un secret erroné.

Et après ?

Les clés API sont les identifiants les plus simples de votre boîte à outils, et les mêmes réflexes — hachage au repos, limitation stricte des scopes, rotation à la demande — s’appliquent partout. Si vous avez besoin de jetons vérifiables à courte durée de vie contenant des revendications (claims), consultez le guide sur les JWT. Lorsque l’appelant agit au nom d’un utilisateur et nécessite son consentement, OAuth 2.0 est le modèle approprié. Les chaînes de permission d’une clé sont les mêmes composants utilisés par le RBAC, ce guide est donc le compagnon idéal pour gérer le scoping. Enfin, comme les clés sont un support naturel pour les quotas, la section sur le rate limiting explique comment empêcher une seule intégration d’épuiser la capacité pour tout le monde.

En pratique

Créer, vérifier, limiter, révoquer

Les quatre opérations dont tout système de clés API a besoin, et rien de plus.

routes/keys.ts
import crypto from "node:crypto";

router.post("/keys", requireAuth(), async (req, res) => {
  const { name, scopes } = req.body;
  const secret = crypto.randomBytes(32).toString("base64url");
  const key = `sk_live_${secret}`;

  const [row] = await db.apiKey.insert({
    name,
    ownerId: req.user.id,
    prefix: key.slice(0, 12),
    keyHash: crypto.createHash("sha256").update(key).digest("hex"),
    scopes,
  });

  // The only time the full key is ever returned.
  res.status(201).json({
    id: row.id,
    name: row.name,
    prefix: row.prefix,
    key,
  });
});

Stocker un hash, pas la clé

Un hash suffit pour vérifier une clé présentée et est inutile pour quiconque vole la table. C'est le même raisonnement qui s'applique aux mots de passe.

Préférer
CREATE TABLE api_keys (
  id         bigserial PRIMARY KEY,
  prefix     text NOT NULL,
  key_hash   text NOT NULL,
  scopes     text[] NOT NULL DEFAULT '{}',
  revoked_at timestamptz
);

CREATE INDEX api_keys_prefix_idx ON api_keys (prefix);
Éviter
CREATE TABLE api_keys (
  id  bigserial PRIMARY KEY,
  key text NOT NULL
  -- one database dump or backup leak
  -- hands over every customer's key
);

Envoyer les clés dans un en-tête

Les en-têtes ne sont pas loggués par défaut, n'apparaissent pas dans l'historique du navigateur et ne fuitent pas via l'en-tête Referer lors d'un lien externe.

Préférer
GET /v1/projects HTTP/1.1
Host: api.example.com
Authorization: Bearer sk_live_8f2a9c...
Éviter
GET /v1/projects?api_key=sk_live_8f2a9c... HTTP/1.1
Host: api.example.com
# query strings land in access logs, proxies
# and browser history

Compromis

Les clés API sont-elles le bon choix ?

Les clés sont simples et universelles, ce qui fait à la fois leur force et leur faiblesse. Comprenez ce à quoi vous renoncez.

Strengths

  • Extrêmement simple pour les clients

    Pas de flux de connexion, pas de refresh token et pas de décalage d'horloge. Un client stocke une chaîne et l'envoie, c'est pourquoi chaque CLI et plateforme développeur utilise des clés.

  • Révocable par intégration

    Chaque clé est un identifiant nommé. Vous pouvez révoquer la clé utilisée par un script ayant fuité sans affecter aucun autre client ou service.

  • Facile à limiter et à mesurer

    Une clé est une unité naturelle pour les permissions et les limites de débit ; vous pouvez donner à une intégration un accès en lecture seule et un quota modeste sans rien construire de nouveau.

Trade-offs

  • Longue durée par nature

    Les clés n'expirent généralement pas, donc une clé fuitée est dangereuse jusqu'à ce que quelqu'un s'en aperçoive. Des expirations courtes, la rotation et la surveillance réduisent cette fenêtre.

  • Pas de contexte utilisateur

    Une clé identifie un service, pas une personne. Tout ce qui nécessite un consentement, un accès délégué ou un audit par utilisateur relève d'OAuth.

  • Difficile à garder secret côté client

    Une clé intégrée dans une application mobile ou un bundle navigateur est publique. Utilisez un proxy backend ou un token à courte durée de vie pour ces clients.

FAQ

Foire aux questions

Keep learning

Related topics from the roadmap.

$ commencer à apprendre

Prêt à apprendre API Key Management ?

Notre tutoriel interactif vous guide à travers API Key Management pas à pas — avec des quiz et du vrai code que vous pouvez exécuter dans le navigateur.