Beveiligde API's bouwen met Tokenauthenticatie en OpenAPI
Waarom ik API-tokens nodig had
Ik was bezig met het bouwen van een webapplicatie met een dashboard voor het beheren van content. Alles werkte prima via de gebruikersinterface, maar ik begon na te denken over automatisering. Wat als ik ChatGPT wil gebruiken om content te beheren? Of repetitieve taken wil automatiseren met scripts?
Ik had een API nodig met de juiste authenticatie. Iets dat veilig, eenvoudig te gebruiken en goed gedocumenteerd is.
Hier is wat ik heb gebouwd en wat ik heb geleerd.
De vereisten
Voordat ik in de code dook, heb ik op een rijtje gezet wat ik precies nodig had:
- Beveiligde token-generatie: Geen te raden patronen, cryptografisch veilig
- Eenmalige weergave: Toon de token één keer, nooit meer (zoals GitHub)
- Nooit platte tekst opslaan: Hash alles voordat het de database bereikt
- Intrekkingenondersteuning: Gebruikers in staat stellen tokens te verwijderen die ze niet meer nodig hebben
- Gebruiksregistratie: Tonen wanneer en hoe tokens worden gebruikt
- OpenAPI-documentatie: Het eenvoudig maken voor AI-assistenten om de API te begrijpen
- Interactief testen: Swagger UI zodat ontwikkelaars endpoints kunnen uitproberen
Klinkt als veel, hè? Laat me uitleggen hoe elk onderdeel werkt.
Deel 1: Veilige tokens genereren
De eerste uitdaging was het creëren van tokens die zowel veilig als gebruiksvriendelijk zijn. Ik koos voor dit formaat:
imk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6Laat me elk deel uitleggen:
imk- Mijn productvoorvoegsel (afkorting van "Imerkar")live- Omgeving (live versus test)a1b2c3...- 32 willekeurige tekens
Dit formaat is geïnspireerd op de API-sleutels van Stripe, en daar is een goede reden voor:
- Voorvoegsel scannen - Als iemand per ongeluk een token naar GitHub committeert, kunnen tools dit detecteren door te scannen op het voorvoegsel
imk_ - Omgevingsscheiding - Je kunt in één oogopslag zien of het een test- of live-sleutel is
- Gebruiksvriendelijke identificatie - Gebruikers kunnen sleutels identificeren aan de hand van hun voorvoegsel, zelfs nadat de volledige token verborgen is
De generatiecode
Ik gebruikte de Web Crypto API voor veilige willekeurige generatie:
async function generateApiKey(): Promise<string> {
// Genereer 24 bytes (192 bits) aan willekeurige gegevens
const randomBytes = new Uint8Array(24);
crypto.getRandomValues(randomBytes);
// Converteer naar base64url-codering (URL-veilig, geen opvulling)
const randomString = btoa(String.fromCharCode(...randomBytes))
.replace(/\+/g, '-')
.replace(/\/g, '_')
.replace(/=/g, '');
return `imk_live_${randomString}`;
}Dit genereert cryptografisch veilige willekeurige tokens. De functie crypto.getRandomValues() gebruikt de entropiebron van het besturingssysteem, waardoor tokens onvoorspelbaar zijn. Ik gebruik base64url-codering om ervoor te zorgen dat de token URL-veilig is en de volledige entropie behoudt.
Deel 2: Tokens veilig opslaan
Hier is de gouden regel: sla API-tokens nooit op in platte tekst.
Denk eraan als wachtwoorden. Als uw database wordt gecompromitteerd, wilt u niet dat aanvallers werkende API-sleutels in handen krijgen. Dus net als bij wachtwoorden, hashen we de tokens vóór opslag.
Het databaseschema
CREATE TABLE api_keys (
id TEXT PRIMARY KEY,
customer_id TEXT NOT NULL,
name TEXT NOT NULL,
key_prefix TEXT NOT NULL, -- Voorvoegsel voor weergave (imk_live_xxx)
key_hash TEXT NOT NULL, -- SHA-256 hash
scopes TEXT NOT NULL, -- JSON-array van machtigingen
last_used_at TEXT, -- ISO 8601 tijdstempel
last_used_ip TEXT,
expires_at TEXT, -- ISO 8601 tijdstempel
revoked INTEGER DEFAULT 0,
created_at TEXT NOT NULL -- ISO 8601 tijdstempel
);
-- Index voor snelle authenticatie-zoekopdrachten
CREATE INDEX idx_api_keys_hash ON api_keys(key_hash);
-- Index voor het weergeven van sleutels van de klant
CREATE INDEX idx_api_keys_customer ON api_keys(customer_id);Merk op dat ik de key_prefix (het leesbare deel zoals imk_live_abc) apart opsla. Hierdoor kan ik gebruikers iets als dit tonen:
imk_live_a1b2c3d4... (Aangemaakt op 15 jan 2025)Ze kunnen identificeren welke token welke is zonder de volledige waarde te zien.
De tokens hashen
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('');
}Wanneer een gebruiker een token aanmaakt:
- Genereer de willekeurige token
- Hash deze met SHA-256
- Sla de hash op in de database
- Geef de volledige token terug aan de gebruiker (alleen deze ene keer!)
- Toon de volledige token nooit meer
Deel 3: Authenticatie-middleware
Nu we tokens kunnen genereren en opslaan, moeten we daadwerkelijk verzoeken authenticeren. Ik heb middleware gebouwd die meerdere authenticatiemethoden ondersteunt:
- Clerk JWT-tokens - Voor ingelogde dashboardgebruikers
- API-tokens - Voor externe integraties
De authenticatiestroom
async function authenticate(c: Context) {
const authHeader = c.req.header('Authorization');
if (!authHeader?.startsWith('Bearer ')) {
return c.json({ error: 'Ontbrekende autorisatie' }, 401);
}
const token = authHeader.substring(7);
// Probeer API-sleutelauthenticatie
if (token.startsWith('imk_')) {
const customerId = await verifyApiKey(token);
if (customerId) {
c.set('customerId', customerId);
return;
}
}
// Probeer JWT-authenticatie
const customerId = await verifyJWT(token);
if (customerId) {
c.set('customerId', customerId);
return;
}
return c.json({ error: 'Ongeldige token' }, 401);
}API-tokens verifiëren
Wanneer een API-sleutel binnenkomt, gebeurt het volgende:
async function verifyApiKey(apiKey: string): Promise<string | null> {
// Hash de verstrekte sleutel
const keyHash = await hashApiKey(apiKey);
// Zoek de hash op in de database
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;
// Werk de laatst gebruikte tijdstempel bij
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;
}Dit is snel omdat we de kolom key_hash indexeren. De database-zoekopdracht is O(1) dankzij de index.
Deel 4: De dashboard-UI
Gebruikers moeten een manier hebben om hun tokens te maken en te beheren. Ik heb dit ingebouwd in de Instellingenpagina van mijn dashboard.
De gebruikerservaring
- Navigeer naar Instellingen → API-sleutels
- Klik op "Nieuwe API-sleutel maken"
- Voer een naam in (bijv. "Zapier-integratie")
- Klik op Maken
- Zie de volledige token met een grote waarschuwing:
⚠️ API-sleutel gemaakt
imk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
Sla deze sleutel nu op. U zult deze niet meer kunnen zien!- Kopiëren naar klembord
- Na het sluiten, alleen het voorvoegsel zien:
Zapier-integratie
imk_live_a1b2c3d4...
Gemaakt: 15 jan 2025
Laatst gebruikt: 16 jan 2025
[Intrekken]De React-component
Hier is een vereenvoudigde versie van het maken van sleutels:
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); // Volledige sleutel, slechts één keer getoond
setShowCreateModal(false);
}
async function revokeKey(keyId: string) {
await fetch(`/api/api-keys/${keyId}`, { method: 'DELETE' });
loadKeys(); // De lijst vernieuwen
}
return (
<>
{newKey && (
<Alert>
<AlertTitle>API-sleutel gemaakt</AlertTitle>
<AlertDescription>
<code>{newKey}</code>
<p>Sla dit nu op. U zult het niet opnieuw zien!</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)}
/>
))}
</>
);
}Deel 5: OpenAPI-documentatie
Hier wordt het echt gaaf. Zodra je een API hebt, heb je documentatie nodig. Maar niet zomaar documentatie. Machineleesbare documentatie die AI-assistenten kunnen begrijpen.
Dat is waar OpenAPI (voorheen Swagger) om de hoek komt kijken.
Wat is OpenAPI?
OpenAPI is een standaardmanier om REST API's te beschrijven. Het is een JSON/YAML-bestand dat je vertelt:
- Welke endpoints bestaan
- Welke parameters ze accepteren
- Wat ze retourneren
- Hoe te authenticeren
- Welke fouten kunnen optreden
Het mooie is dat ChatGPT, Claude, Zapier en honderden andere tools OpenAPI-specificaties kunnen lezen en uw API automatisch kunnen begrijpen.
Het OpenAPI-document bouwen
Ik heb een OpenAPI 3.0-specificatie voor mijn API gemaakt:
const openAPIDocument = {
openapi: '3.0.0',
info: {
title: 'Imerkar API',
version: '1.0.0',
description: 'Websites programmatisch bouwen en beheren',
},
servers: [
{
url: 'https://app.imerkar.com',
description: 'Productieserver',
},
],
components: {
securitySchemes: {
BearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT of API-sleutel',
description: 'Gebruik uw API-sleutel met Bearer-authenticatie',
},
},
},
security: [{ BearerAuth: [] }],
paths: {
'/api/pages': {
get: {
summary: 'Lijst van alle pagina\'s',
tags: ['Pagina\'s'],
responses: {
'200': {
description: 'Lijst van pagina\'s',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
pages: {
type: 'array',
items: { $ref: '#/components/schemas/Page' }
}
}
}
}
}
}
}
},
post: {
summary: 'Een nieuwe pagina maken',
tags: ['Pagina\'s'],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['title'],
properties: {
title: { type: 'string' },
description: { type: 'string' },
}
}
}
}
},
responses: {
'201': {
description: 'Pagina gemaakt',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/Page' }
}
}
}
}
}
}
}
};Dit ziet er misschien omslachtig uit, maar het is ongelooflijk krachtig. Met dit ene document kan elke tool uw volledige API begrijpen.
De OpenAPI-specificatie serveren
Ik heb twee endpoints gemaakt:
// Serveer de JSON-specificatie
app.get('/api/docs/openapi.json', (c) => {
return c.json(openAPIDocument);
});
// Serveer de YAML-specificatie (sommige tools geven de voorkeur aan YAML)
app.get('/api/docs/openapi.yaml', (c) => {
const yaml = convertToYAML(openAPIDocument);
return c.text(yaml, 200, {
'Content-Type': 'application/x-yaml'
});
});Deel 6: Swagger UI voor interactief testen
OpenAPI-specificaties zijn geweldig, maar ontwikkelaars willen uw API testen, er niet alleen over lezen. Dat is waar Swagger UI om de hoek komt kijken.
Swagger UI is een webinterface die:
- Uw OpenAPI-specificatie leest
- Interactieve documentatie genereert
- Ontwikkelaars in staat stelt endpoints rechtstreeks in de browser te testen
- Voorbeelden van verzoeken/antwoorden toont
Swagger UI instellen
Ik heb een eenvoudige HTML-pagina gemaakt die Swagger UI laadt vanaf een CDN:
app.get('/api/docs', (c) => {
const html = `
<!DOCTYPE html>
<html>
<head>
<title>Imerkar API Documentatie</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);
});Nu zien ontwikkelaars die https://app.imerkar.com/api/docs bezoeken een prachtige, interactieve API-verkenner.
Swagger UI gebruiken
De workflow is eenvoudig:
- Open de documentatie op
/api/docs - Klik op "Autoriseren"
- Plak uw API-sleutel:
imk_live_... - Klik op een endpoint (bijv. GET /api/pages)
- Klik op "Probeer het uit"
- Klik op "Uitvoeren"
- Zie het echte antwoord van uw API
Het is als Postman, maar ingebouwd in uw documentatie.
Deel 7: Integratie met ChatGPT
Hier komt alles samen. Met een OpenAPI-specificatie kunt u een Aangepaste GPT maken die uw API begrijpt.
Een Aangepaste GPT maken
- Ga naar ChatGPT → Maak een GPT
- Voeg een Actie toe
- Importeren vanaf URL:
https://app.imerkar.com/api/docs/openapi.json - Stel authenticatie in op Bearer Token
- Voeg uw API-sleutel toe
- Test het:
U: "Lijst al mijn pagina's op"
ChatGPT: Laat me uw pagina's controleren...
[Voert GET /api/pages verzoek uit]
ChatGPT: "U heeft 5 pagina's:
1. Home
2. Over ons
3. Diensten
4. Contact
5. Blog"
U: "Maak een nieuwe pagina genaamd 'Prijzen' met een beschrijving van onze plannen"
ChatGPT: [Voert POST /api/pages verzoek uit]
ChatGPT: "Klaar! Ik heb een nieuwe pagina genaamd 'Prijzen' gemaakt met uw beschrijving."Het werkt gewoon. ChatGPT leest uw OpenAPI-specificatie en weet hoe het uw API moet gebruiken.
Deel 8: Gebruiksregistratie en snelheidsbeperking
Zodra u API-tokens in het wild heeft, moet u bijhouden hoe ze worden gebruikt en misbruik voorkomen.
Gebruikslogboek
Ik heb een eenvoudige tabel voor gebruikslogboeken gemaakt:
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 -- ISO 8601 tijdstempel
);
-- Index voor snelheidsbeperkingsvragen
CREATE INDEX idx_usage_logs_key_time ON api_key_usage_logs(api_key_id, created_at);Elk API-verzoek wordt gelogd:
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()
]
);
}Snelheidsbeperking (Rate Limiting)
Ik heb eenvoudige snelheidsbeperking geïmplementeerd:
- 30 verzoeken per minuut per API-sleutel
- 1000 verzoeken per dag
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;
}Als de limiet wordt overschreden, retourneer dan een nuttige foutmelding:
{
"error": "Snelheidslimiet overschreden",
"code": "RATE_LIMIT_EXCEEDED",
"details": {
"limit": 30,
"window": "1 minuut",
"retryAfter": 42
}
}Wat ik heb geleerd
Het bouwen van deze functie heeft me veel geleerd:
1. Beveiliging is geen optie
Sla nooit platte tekst tokens op. Hash alles. Gebruik crypto.subtle voor willekeur. Denk na over wat er gebeurt als uw database lekt.
2. UX is belangrijk voor ontwikkelaarstools
API-tokens hebben ook een goede UX nodig. Toon ze één keer met een grote waarschuwing. Laat gebruikers hun tokens een naam geven. Toon wanneer ze voor het laatst zijn gebruikt. Maak intrekking eenvoudig.
3. OpenAPI is een superkracht
Het schrijven van een OpenAPI-specificatie voelt in eerste instantie als tijdrovend werk, maar het is ongelooflijk waardevol. Het maakt ChatGPT-integratie, Swagger UI, generatie van client SDK's en testautomatisering mogelijk.
4. Begin eenvoudig, itereer
Ik heb dit niet allemaal tegelijk gebouwd. Versie 1 was alleen token-generatie en authenticatie. Daarna voegde ik de dashboard-UI toe. Daarna OpenAPI-documenten. Daarna gebruiksregistratie. Elke iteratie maakte het beter.
5. Database-indexen zijn cruciaal
Het indexeren van de kolom key_hash maakt authenticatie snel. Zonder zou elk verzoek de hele tabel doorzoeken. Hiermee zijn zoekopdrachten direct.
De tech-stack
Ter referentie, dit is wat ik heb gebruikt:
- Backend: Cloudflare Workers + Hono-framework
- Database: Cloudflare D1 (SQLite)
- Frontend: React + Tailwind CSS
- Auth (sessies): Clerk
- OpenAPI: Handgeschreven specificatie (zou @hono/zod-openapi kunnen gebruiken voor typeveiligheid)
- Taal: TypeScript
Volgende stappen
Er is nog meer dat ik wil toevoegen:
- Op bereik gebaseerde machtigingen - Momenteel hebben tokens volledige toegang. Ik wil bereiken toevoegen zoals
pages:read,pages:write,media:read, enz. - Webhook-ondersteuning - Gebruikers in staat stellen zich te abonneren op gebeurtenissen (page.created, page.published, enz.)
- SDK-generatie - Gebruik de OpenAPI-specificatie om automatisch clientbibliotheken in Python, JavaScript, PHP, enz. te genereren
- Betere analyses - Meer gedetailleerde gebruiksstatistieken met grafieken en trends
- IP-whitelisting - Gebruikers in staat stellen tokens te beperken tot specifieke IP-adressen
Bronnen
Als u iets soortgelijks bouwt, hebben deze bronnen me geholpen: