Retour au blog

Construire des API sécurisées avec l'authentification par jeton et OpenAPI

2025-11-2312 min read

Pourquoi j'avais besoin de jetons API

J'étais en train de construire une application web avec un tableau de bord pour gérer le contenu. Tout fonctionnait bien via l'interface utilisateur, mais j'ai commencé à penser à l'automatisation. Et si je voulais utiliser ChatGPT pour gérer le contenu ? Ou automatiser des tâches répétitives avec des scripts ?

J'avais besoin d'une API avec une authentification appropriée. Quelque chose de sécurisé, facile à utiliser et bien documenté.

Voici ce que j'ai construit et ce que j'ai appris.

Les exigences

Avant de plonger dans le code, j'ai défini ce dont j'avais réellement besoin :

  • Génération de jetons sécurisés : Pas de schémas devinables, cryptographiquement sécurisés
  • Affichage unique : Afficher le jeton une seule fois, jamais plus (comme GitHub)
  • Ne jamais stocker en clair : Hacher tout avant que cela n'atteigne la base de données
  • Prise en charge de la révocation : Permettre aux utilisateurs de supprimer les jetons dont ils n'ont plus besoin
  • Suivi de l'utilisation : Afficher quand et comment les jetons sont utilisés
  • Documentation OpenAPI : Faciliter la compréhension de l'API par les assistants IA
  • Tests interactifs : Swagger UI pour que les développeurs puissent essayer les points d'accès

Cela semble beaucoup, n'est-ce pas ? Laissez-moi détailler le fonctionnement de chaque élément.

Partie 1 : Génération de jetons sécurisés

Le premier défi était de créer des jetons à la fois sécurisés et conviviaux. J'ai opté pour ce format :

imk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

Laissez-moi expliquer chaque partie :

  • imk - Préfixe de mon produit (raccourci pour "Imerkar")
  • live - Environnement (live ou test)
  • a1b2c3... - 32 caractères aléatoires

Ce format est inspiré des clés API de Stripe, et il y a une bonne raison à cela :

  1. Analyse du préfixe - Si quelqu'un compromet accidentellement un jeton dans GitHub, les outils peuvent le détecter en recherchant le préfixe imk_
  2. Séparation des environnements - Vous pouvez dire d'un coup d'œil s'il s'agit d'une clé de test ou de production
  3. Identification conviviale - Les utilisateurs peuvent identifier les clés par leur préfixe même après que la valeur complète soit masquée

Le code de génération

J'ai utilisé l'API Web Crypto pour une génération aléatoire sécurisée :

async function generateApiKey(): Promise<string> {
// Générer 24 octets (192 bits) de données aléatoires
const randomBytes = new Uint8Array(24);
crypto.getRandomValues(randomBytes);

// Convertir en encodage base64url (sûr pour l'URL, sans remplissage)
const randomString = btoa(String.fromCharCode(...randomBytes))
.replace(/\+/g, '-')
.replace(/\/g, '_')
.replace(/=/g, '');

return `imk_live_${randomString}`;
}

Ceci génère des jetons aléatoires cryptographiquement sécurisés. La fonction crypto.getRandomValues() utilise la source d'entropie du système d'exploitation, rendant les jetons imprévisibles. J'utilise l'encodage base64url pour garantir que le jeton est sûr pour l'URL et conserve toute l'entropie.

Partie 2 : Stockage sécurisé des jetons

Voici la règle d'or : ne jamais stocker les jetons API en clair.

Pensez-y comme aux mots de passe. Si votre base de données est compromise, vous ne voulez pas que les attaquants aient des clés API fonctionnelles. Donc, tout comme pour les mots de passe, nous hachons les jetons avant le stockage.

Le schéma de base de données

CREATE TABLE api_keys (
id TEXT PRIMARY KEY,
customer_id TEXT NOT NULL,
name TEXT NOT NULL,
key_prefix TEXT NOT NULL, -- Préfixe pour l'affichage (imk_live_xxx)
key_hash TEXT NOT NULL, -- Hachage SHA-256
scopes TEXT NOT NULL, -- Tableau JSON des autorisations
last_used_at TEXT, -- Horodatage ISO 8601
last_used_ip TEXT,
expires_at TEXT, -- Horodatage ISO 8601
revoked INTEGER DEFAULT 0,
created_at TEXT NOT NULL -- Horodatage ISO 8601
);

-- Index pour les recherches d'authentification rapides
CREATE INDEX idx_api_keys_hash ON api_keys(key_hash);

-- Index pour lister les clés du client
CREATE INDEX idx_api_keys_customer ON api_keys(customer_id);

Notez que je stocke le key_prefix (la partie lisible comme imk_live_abc) séparément. Cela me permet d'afficher quelque chose comme ceci aux utilisateurs :

imk_live_a1b2c3d4... (Créé le 15 janv. 2025)

Ils peuvent identifier quel jeton est lequel sans voir la valeur complète.

Hachage des jetons

async function hashApiKey(apiKey: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(apiKey);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}

Lorsqu'un utilisateur crée un jeton :

  1. Générer le jeton aléatoire
  2. Le hacher avec SHA-256
  3. Stocker le hachage dans la base de données
  4. Retourner le jeton complet à l'utilisateur (une seule fois !)
  5. Ne jamais afficher à nouveau le jeton complet

Partie 3 : Middleware d'authentification

Maintenant que nous pouvons générer et stocker des jetons, nous devons réellement authentifier les requêtes. J'ai créé un middleware qui prend en charge plusieurs méthodes d'authentification :

  1. Jetons JWT Clerk - Pour les utilisateurs connectés au tableau de bord
  2. Jetons API - Pour les intégrations externes

Le flux d'authentification

async function authenticate(c: Context) {
const authHeader = c.req.header('Authorization');

if (!authHeader?.startsWith('Bearer ')) {
return c.json({ error: 'Authorization manquante' }, 401);
}

const token = authHeader.substring(7);

// Essayer l'authentification par clé API
if (token.startsWith('imk_')) {
const customerId = await verifyApiKey(token);
if (customerId) {
c.set('customerId', customerId);
return;
}
}

// Essayer l'authentification JWT
const customerId = await verifyJWT(token);
if (customerId) {
c.set('customerId', customerId);
return;
}

return c.json({ error: 'Jeton invalide' }, 401);
}

Vérification des jetons API

Lorsqu'une clé API arrive, voici ce qui se passe :

async function verifyApiKey(apiKey: string): Promise<string | null> {
// Hacher la clé fournie
const keyHash = await hashApiKey(apiKey);

// Rechercher le hachage dans la base de données
const key = await db.query(
'SELECT customer_id, revoked, expires_at FROM api_keys WHERE key_hash = ?',
[keyHash]
);

if (!key) return null;
if (key.revoked) return null;
if (key.expires_at && new Date(key.expires_at) < new Date()) return null;

// Mettre à jour l'horodatage de dernière utilisation
await db.execute(
'UPDATE api_keys SET last_used_at = ?, last_used_ip = ? WHERE key_hash = ?',
[new Date().toISOString(), c.req.header('cf-connecting-ip'), keyHash]
);

return key.customer_id;
}

Ceci est rapide car nous indexons la colonne key_hash. La recherche dans la base de données est O(1) grâce à l'index.

Partie 4 : L'interface utilisateur du tableau de bord

Les utilisateurs doivent avoir un moyen de créer et de gérer leurs jetons. J'ai intégré cela dans la page Paramètres de mon tableau de bord.

L'expérience utilisateur

  1. Naviguer vers Paramètres → Clés API
  2. Cliquer sur "Créer une nouvelle clé API"
  3. Entrer un nom (ex. : "Intégration Zapier")
  4. Cliquer sur Créer
  5. Voir le jeton complet avec un grand avertissement :
⚠️ Clé API Créée

imk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

Enregistrez cette clé maintenant. Vous ne pourrez plus la voir !
  1. Copier dans le presse-papiers
  2. Après la fermeture, voir uniquement le préfixe :
Intégration Zapier
imk_live_a1b2c3d4...
Créé le : 15 janv. 2025
Dernière utilisation : 16 janv. 2025
[Révoquer]

Le composant React

Voici une version simplifiée du flux de création de clé :

function ApiKeysTab() {
const [keys, setKeys] = useState([]);
const [showCreateModal, setShowCreateModal] = useState(false);
const [newKey, setNewKey] = useState(null);

async function createKey(name: string) {
const response = await fetch('/api/api-keys', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name })
});

const data = await response.json();
setNewKey(data.key); // Clé complète, affichée une seule fois
setShowCreateModal(false);
}

async function revokeKey(keyId: string) {
await fetch(`/api/api-keys/${keyId}`, { method: 'DELETE' });
loadKeys(); // Actualiser la liste
}

return (
<>
{newKey && (
<Alert>
<AlertTitle>Clé API Créée</AlertTitle>
<AlertDescription>
<code>{newKey}</code>
<p>Enregistrez ceci maintenant. Vous ne le verrez plus !</p>
</AlertDescription>
</Alert>
)}

{keys.map(key => (
<KeyRow
key={key.id}
name={key.name}
prefix={key.key_prefix}
lastUsed={key.last_used_at}
onRevoke={() => revokeKey(key.id)}
/>
))}
</>
);
}

Partie 5 : Documentation OpenAPI

C'est là que ça devient vraiment intéressant. Une fois que vous avez une API, vous avez besoin de documentation. Mais pas n'importe quelle documentation. Une documentation lisible par machine que les assistants IA peuvent comprendre.

C'est là qu'OpenAPI (anciennement Swagger) entre en jeu.

Qu'est-ce qu'OpenAPI ?

OpenAPI est une manière standard de décrire les API REST. C'est un fichier JSON/YAML qui vous indique :

  • Quels points d'accès existent
  • Quels paramètres ils acceptent
  • Ce qu'ils retournent
  • Comment s'authentifier
  • Quelles erreurs peuvent survenir

La beauté réside dans le fait que ChatGPT, Claude, Zapier et des centaines d'autres outils peuvent lire les spécifications OpenAPI et comprendre automatiquement votre API.

Construction du document OpenAPI

J'ai créé une spécification OpenAPI 3.0 pour mon API :

const openAPIDocument = {
openapi: '3.0.0',
info: {
title: 'Imerkar API',
version: '1.0.0',
description: 'Construire et gérer des sites web par programme',
},
servers: [
{
url: 'https://app.imerkar.com',
description: 'Serveur de production',
},
],
components: {
securitySchemes: {
BearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT ou Clé API',
description: 'Utilisez votre clé API avec l'authentification Bearer',
},
},
},
security: [{ BearerAuth: [] }],
paths: {
'/api/pages': {
get: {
summary: 'Lister toutes les pages',
tags: ['Pages'],
responses: {
'200': {
description: 'Liste des pages',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
pages: {
type: 'array',
items: { $ref: '#/components/schemas/Page' }
}
}
}
}
}
}
}
},
post: {
summary: 'Créer une nouvelle page',
tags: ['Pages'],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['title'],
properties: {
title: { type: 'string' },
description: { type: 'string' },
}
}
}
}
},
responses: {
'201': {
description: 'Page créée',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/Page' }
}
}
}
}
}
}
}
};

Cela peut sembler verbeux, mais c'est incroyablement puissant. Avec ce seul document, n'importe quel outil peut comprendre toute votre API.

Servir la spécification OpenAPI

J'ai créé deux points d'accès :

// Servir la spécification JSON
app.get('/api/docs/openapi.json', (c) => {
return c.json(openAPIDocument);
});

// Servir la spécification YAML (certains outils préfèrent YAML)
app.get('/api/docs/openapi.yaml', (c) => {
const yaml = convertToYAML(openAPIDocument);
return c.text(yaml, 200, {
'Content-Type': 'application/x-yaml'
});
});

Partie 6 : Swagger UI pour les tests interactifs

Les spécifications OpenAPI sont excellentes, mais les développeurs veulent tester votre API, pas seulement la lire. C'est là qu'intervient Swagger UI.

Swagger UI est une interface web qui :

  • Lit votre spécification OpenAPI
  • Génère une documentation interactive
  • Permet aux développeurs de tester les points d'accès directement dans le navigateur
  • Affiche des exemples de requêtes/réponses

Configuration de Swagger UI

J'ai créé une page HTML simple qui charge Swagger UI à partir d'un CDN :

app.get('/api/docs', (c) => {
const html = `
<!DOCTYPE html>
<html>
<head>
<title>Documentation API Imerkar</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.10.3/swagger-ui.css">
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5.10.3/swagger-ui-bundle.js"></script>
<script>
window.onload = function() {
SwaggerUIBundle({
url: '/api/docs/openapi.json',
dom_id: '#swagger-ui',
persistAuthorization: true,
tryItOutEnabled: true,
});
};
</script>
</body>
</html>
`;

return c.html(html);
});

Maintenant, lorsque les développeurs visitent https://app.imerkar.com/api/docs, ils voient un explorateur d'API magnifique et interactif.

Utilisation de Swagger UI

Le flux de travail est simple :

  1. Ouvrir la documentation à l'adresse /api/docs
  2. Cliquer sur "Autoriser"
  3. Coller votre clé API : imk_live_...
  4. Cliquer sur un point d'accès (ex. : GET /api/pages)
  5. Cliquer sur "Essayer"
  6. Cliquer sur "Exécuter"
  7. Voir la vraie réponse de votre API

C'est comme Postman, mais intégré à votre documentation.

Partie 7 : Intégration avec ChatGPT

C'est là que tout prend son sens. Avec une spécification OpenAPI, vous pouvez créer un GPT personnalisé qui comprend votre API.

Création d'un GPT personnalisé

  1. Aller à ChatGPT → Créer un GPT
  2. Ajouter une action
  3. Importer depuis l'URL : https://app.imerkar.com/api/docs/openapi.json
  4. Définir l'authentification sur Jeton Bearer
  5. Ajouter votre clé API
  6. Tester :
Vous : "Lister toutes mes pages"

ChatGPT : Laissez-moi vérifier vos pages...
[Effectue une requête GET /api/pages]

ChatGPT : "Vous avez 5 pages : 1. Accueil 2. À propos de nous 3. Services 4. Contact 5. Blog"

Vous : "Créer une nouvelle page appelée 'Tarification' avec une description de nos forfaits"

ChatGPT : [Effectue une requête POST /api/pages]

ChatGPT : "Terminé ! J'ai créé une nouvelle page appelée 'Tarification' avec votre description."

Ça fonctionne, tout simplement. ChatGPT lit votre spécification OpenAPI et sait comment utiliser votre API.

Partie 8 : Suivi de l'utilisation et limitation du débit

Une fois que vous avez des jetons API en circulation, vous devez suivre leur utilisation et empêcher les abus.

Journalisation de l'utilisation

J'ai créé une table de journalisation d'utilisation simple :

CREATE TABLE api_key_usage_logs (
id TEXT PRIMARY KEY,
api_key_id TEXT NOT NULL,
endpoint TEXT NOT NULL,
method TEXT NOT NULL,
status_code INTEGER NOT NULL,
ip_address TEXT,
created_at TEXT NOT NULL -- Horodatage ISO 8601
);

-- Index pour les requêtes de limitation de débit
CREATE INDEX idx_usage_logs_key_time ON api_key_usage_logs(api_key_id, created_at);

Chaque requête API est enregistrée :

async function logApiUsage(keyId: string, req: Request, status: number) {
await db.execute(
`INSERT INTO api_key_usage_logs (id, api_key_id, endpoint, method, status_code, ip_address, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[
generateId(),
keyId,
new URL(req.url).pathname,
req.method,
status,
req.headers.get('cf-connecting-ip'),
new Date().toISOString()
]
);
}

Limitation du débit (Rate Limiting)

J'ai implémenté une limitation de débit simple :

  • 30 requêtes par minute par clé API
  • 1000 requêtes par jour
async function checkRateLimit(keyId: string): Promise<boolean> {
const oneMinuteAgo = new Date(Date.now() - 60000).toISOString();

const count = await db.query(
'SELECT COUNT(*) as count FROM api_key_usage_logs WHERE api_key_id = ? AND created_at > ?',
[keyId, oneMinuteAgo]
);

return count.count < 30;
}

Si la limite est dépassée, renvoyez une erreur utile :

{
"error": "Limite de débit dépassée",
"code": "RATE_LIMIT_EXCEEDED",
"details": {
"limit": 30,
"window": "1 minute",
"retryAfter": 42
}
}

Ce que j'ai appris

Construire cette fonctionnalité m'a beaucoup appris :

1. La sécurité n'est pas facultative

Ne stockez jamais de jetons en clair. Hachez tout. Utilisez crypto.subtle pour l'aléatoire. Pensez à ce qui se passe si votre base de données fuit.

2. L'expérience utilisateur compte pour les outils de développeur

Les jetons API ont aussi besoin d'une bonne UX. Affichez-les une seule fois avec un grand avertissement. Permettez aux utilisateurs de nommer leurs jetons. Indiquez quand ils ont été utilisés pour la dernière fois. Rendez la révocation facile.

3. OpenAPI est un superpouvoir

Écrire une spécification OpenAPI ressemble à une corvée au début, mais c'est incroyablement précieux. Cela permet l'intégration avec ChatGPT, Swagger UI, la génération de SDK clients et l'automatisation des tests.

4. Commencez simple, itérez

Je n'ai pas tout construit d'un coup. La version 1 était juste la génération de jetons et l'authentification. Ensuite, j'ai ajouté l'interface utilisateur du tableau de bord. Puis la documentation OpenAPI. Ensuite, le suivi de l'utilisation. Chaque itération l'a améliorée.

5. Les index de base de données sont cruciaux

Indexer la colonne key_hash rend l'authentification rapide. Sans cela, chaque requête analyserait toute la table. Avec cela, les recherches sont instantanées.

La pile technologique

Pour référence, voici ce que j'ai utilisé :

  • Backend : Cloudflare Workers + framework Hono
  • Base de données : Cloudflare D1 (SQLite)
  • Frontend : React + Tailwind CSS
  • Authentification (sessions) : Clerk
  • OpenAPI : Spécification écrite à la main (pourrait utiliser @hono/zod-openapi pour la sécurité des types)
  • Langage : TypeScript

Prochaines étapes

Il y a encore des choses que je veux ajouter :

  • Autorisations basées sur les scopes - Actuellement, les jetons ont un accès complet. Je veux ajouter des scopes comme pages:read, pages:write, media:read, etc.
  • Prise en charge des Webhooks - Permettre aux utilisateurs de s'abonner à des événements (page.created, page.published, etc.)
  • Génération de SDK - Utiliser la spécification OpenAPI pour générer automatiquement des bibliothèques clientes en Python, JavaScript, PHP, etc.
  • Meilleures analyses - Statistiques d'utilisation plus détaillées avec graphiques et tendances
  • Liste blanche d'IP - Permettre aux utilisateurs de restreindre les jetons à des adresses IP spécifiques

Ressources

Si vous construisez quelque chose de similaire, ces ressources m'ont été utiles :

Restez Informé

Recevez les derniers articles et insights dans votre boîte mail.

Unsubscribe anytime. No spam, ever.