Torna al blog

Creare API Sicure con Autenticazione a Token e OpenAPI

2025-11-2312 min read

Perché avevo bisogno dei token API

Stavo costruendo un'applicazione web con una dashboard per la gestione dei contenuti. Tutto funzionava bene tramite l'interfaccia utente, ma ho iniziato a pensare all'automazione. E se volessi usare ChatGPT per gestire i contenuti? O automatizzare attività ripetitive con degli script?

Avevo bisogno di una API con un'autenticazione adeguata. Qualcosa di sicuro, facile da usare e ben documentato.

Ecco cosa ho costruito e cosa ho imparato.

I requisiti

Prima di immergermi nel codice, ho delineato ciò di cui avevo effettivamente bisogno:

  • Generazione di token sicuri: Nessuno schema indovinabile, crittograficamente sicuro
  • Visualizzazione una tantum: Mostrare il token una sola volta, mai più (come GitHub)
  • Mai archiviare testo semplice: Eseguire l'hashing di tutto prima che arrivi al database
  • Supporto alla revoca: Consentire agli utenti di eliminare i token di cui non hanno più bisogno
  • Tracciamento dell'utilizzo: Mostrare quando e come vengono utilizzati i token
  • Documentazione OpenAPI: Semplificare la comprensione della API per gli assistenti AI
  • Test interattivi: Swagger UI per consentire agli sviluppatori di provare gli endpoint

Sembra molto, vero? Lasciate che vi illustri come funziona ogni pezzo.

Parte 1: Generazione di token sicuri

La prima sfida è stata creare token che fossero sia sicuri che facili da usare per l'utente. Ho optato per questo formato:

imk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

Lasciate che spieghi ogni parte:

  • imk - Prefisso del mio prodotto (abbreviazione di "Imerkar")
  • live - Ambiente (live vs test)
  • a1b2c3... - 32 caratteri casuali

Questo formato è ispirato alle chiavi API di Stripe, e c'è una buona ragione per questo:

  1. Scansione del prefisso - Se qualcuno commette accidentalmente un token su GitHub, gli strumenti possono rilevarlo scansionando il prefisso imk_
  2. Separazione degli ambienti - Si può capire a colpo d'occhio se è una chiave di test o live
  3. Identificazione intuitiva per l'utente - Gli utenti possono identificare le chiavi dal loro prefisso anche dopo che il token completo è nascosto

Il codice di generazione

Ho utilizzato la Web Crypto API per la generazione casuale sicura:

async function generateApiKey(): Promise<string> {
// Genera 24 byte (192 bit) di dati casuali
const randomBytes = new Uint8Array(24);
crypto.getRandomValues(randomBytes);

// Converte in codifica base64url (sicura per URL, senza padding)
const randomString = btoa(String.fromCharCode(...randomBytes))
.replace(/\+/g, '-')
.replace(/\/g, '_')
.replace(/=/g, '');

return `imk_live_${randomString}`;
}

Questo genera token casuali crittograficamente sicuri. La funzione crypto.getRandomValues() utilizza la fonte di entropia del sistema operativo, rendendo i token imprevedibili. Utilizzo la codifica base64url per garantire che il token sia sicuro per l'URL e mantenga la piena entropia.

Parte 2: Archiviazione sicura dei token

Questa è la regola d'oro: non archiviare mai i token API in testo semplice.

Pensatela come alle password. Se il vostro database viene compromesso, non volete che gli aggressori abbiano chiavi API funzionanti. Quindi, proprio come per le password, eseguiamo l'hashing dei token prima dell'archiviazione.

Lo schema del database

CREATE TABLE api_keys (
id TEXT PRIMARY KEY,
customer_id TEXT NOT NULL,
name TEXT NOT NULL,
key_prefix TEXT NOT NULL, -- Prefisso per la visualizzazione (imk_live_xxx)
key_hash TEXT NOT NULL, -- Hash SHA-256
scopes TEXT NOT NULL, -- Array JSON di permessi
last_used_at TEXT, -- Timestamp ISO 8601
last_used_ip TEXT,
expires_at TEXT, -- Timestamp ISO 8601
revoked INTEGER DEFAULT 0,
created_at TEXT NOT NULL -- Timestamp ISO 8601
);

-- Indice per ricerche di autenticazione veloci
CREATE INDEX idx_api_keys_hash ON api_keys(key_hash);

-- Indice per elencare le chiavi del cliente
CREATE INDEX idx_api_keys_customer ON api_keys(customer_id);

Notate che archivio separatamente il key_prefix (la parte leggibile come imk_live_abc). Questo mi permette di mostrare agli utenti qualcosa come:

imk_live_a1b2c3d4... (Creato il 15 gen 2025)

Possono identificare quale token è quale senza vedere il valore completo.

Hashing dei token

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('');
}

Quando un utente crea un token:

  1. Genera il token casuale
  2. Esegue l'hashing con SHA-256
  3. Archivia l'hash nel database
  4. Restituisce il token completo all'utente (solo questa volta!)
  5. Non mostra mai più il token completo

Parte 3: Middleware di autenticazione

Ora che possiamo generare e archiviare i token, dobbiamo effettivamente autenticare le richieste. Ho creato un middleware che supporta più metodi di autenticazione:

  1. Token JWT di Clerk - Per gli utenti della dashboard loggati
  2. Token API - Per le integrazioni esterne

Il flusso di autenticazione

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

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

const token = authHeader.substring(7);

// Prova l'autenticazione con chiave API
if (token.startsWith('imk_')) {
const customerId = await verifyApiKey(token);
if (customerId) {
c.set('customerId', customerId);
return;
}
}

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

return c.json({ error: 'Token non valido' }, 401);
}

Verifica dei token API

Quando arriva una chiave API, ecco cosa succede:

async function verifyApiKey(apiKey: string): Promise<string | null> {
// Esegue l'hashing della chiave fornita
const keyHash = await hashApiKey(apiKey);

// Cerca l'hash nel 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;

// Aggiorna il timestamp dell'ultimo utilizzo
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;
}

Questo è veloce perché indicizziamo la colonna key_hash. La ricerca nel database è O(1) grazie all'indice.

Parte 4: L'interfaccia utente della dashboard

Gli utenti hanno bisogno di un modo per creare e gestire i loro token. Ho integrato questo nella pagina Impostazioni della mia dashboard.

L'esperienza utente

  1. Naviga su Impostazioni → Chiavi API
  2. Clicca su "Crea nuova chiave API"
  3. Inserisci un nome (es. "Integrazione Zapier")
  4. Clicca su Crea
  5. Visualizza il token completo con un grande avviso:
⚠️ Chiave API Creata

imk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

Salva questa chiave ora. Non potrai più vederla!
  1. Copia negli appunti
  2. Dopo la chiusura, visualizza solo il prefisso:
Integrazione Zapier
imk_live_a1b2c3d4...
Creato: 15 gen 2025
Ultimo utilizzo: 16 gen 2025
[Revoca]

Il componente React

Ecco una versione semplificata del flusso di creazione della chiave:

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); // Chiave completa, mostrata solo una volta
setShowCreateModal(false);
}

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

return (
<>
{newKey && (
<Alert>
<AlertTitle>Chiave API Creata</AlertTitle>
<AlertDescription>
<code>{newKey}</code>
<p>Salvala ora. Non la vedrai più!</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)}
/>
))}
</>
);
}

Parte 5: Documentazione OpenAPI

Qui le cose si fanno davvero interessanti. Una volta che hai una API, hai bisogno di documentazione. Ma non una documentazione qualsiasi. Documentazione leggibile dalle macchine che gli assistenti AI possono capire.

È qui che entra in gioco OpenAPI (precedentemente Swagger).

Cos'è OpenAPI?

OpenAPI è un modo standard per descrivere le API REST. È un file JSON/YAML che ti dice:

  • Quali endpoint esistono
  • Quali parametri accettano
  • Cosa restituiscono
  • Come autenticarsi
  • Quali errori possono verificarsi

La bellezza sta nel fatto che ChatGPT, Claude, Zapier e centinaia di altri strumenti possono leggere le specifiche OpenAPI e comprendere automaticamente la tua API.

Creazione del documento OpenAPI

Ho creato una specifica OpenAPI 3.0 per la mia API:

const openAPIDocument = {
openapi: '3.0.0',
info: {
title: 'Imerkar API',
version: '1.0.0',
description: 'Costruisci e gestisci siti web programmaticamente',
},
servers: [
{
url: 'https://app.imerkar.com',
description: 'Server di produzione',
},
],
components: {
securitySchemes: {
BearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT o Chiave API',
description: 'Usa la tua chiave API con autenticazione Bearer',
},
},
},
security: [{ BearerAuth: [] }],
paths: {
'/api/pages': {
get: {
summary: 'Elenca tutte le pagine',
tags: ['Pagine'],
responses: {
'200': {
description: 'Elenco delle pagine',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
pages: {
type: 'array',
items: { $ref: '#/components/schemas/Page' }
}
}
}
}
}
}
}
},
post: {
summary: 'Crea una nuova pagina',
tags: ['Pagine'],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['title'],
properties: {
title: { type: 'string' },
description: { type: 'string' },
}
}
}
}
},
responses: {
'201': {
description: 'Pagina creata',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/Page' }
}
}
}
}
}
}
}
};

Potrebbe sembrare prolisso, ma è incredibilmente potente. Con questo unico documento, qualsiasi strumento può comprendere l'intera tua API.

Servire la specifica OpenAPI

Ho creato due endpoint:

// Serve la specifica JSON
app.get('/api/docs/openapi.json', (c) => {
return c.json(openAPIDocument);
});

// Serve la specifica YAML (alcuni strumenti preferiscono YAML)
app.get('/api/docs/openapi.yaml', (c) => {
const yaml = convertToYAML(openAPIDocument);
return c.text(yaml, 200, {
'Content-Type': 'application/x-yaml'
});
});

Parte 6: Swagger UI per test interattivi

Le specifiche OpenAPI sono ottime, ma gli sviluppatori vogliono testare la tua API, non solo leggerne la descrizione. È qui che entra in gioco Swagger UI.

Swagger UI è un'interfaccia web che:

  • Legge la tua specifica OpenAPI
  • Genera documentazione interattiva
  • Consente agli sviluppatori di testare gli endpoint direttamente nel browser
  • Mostra esempi di richiesta/risposta

Configurazione di Swagger UI

Ho creato una semplice pagina HTML che carica Swagger UI da un CDN:

app.get('/api/docs', (c) => {
const html = `
<!DOCTYPE html>
<html>
<head>
<title>Documentazione 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);
});

Ora, quando gli sviluppatori visitano https://app.imerkar.com/api/docs, vedono un esploratore API interattivo e ben fatto.

Utilizzo di Swagger UI

Il flusso di lavoro è semplice:

  1. Apri la documentazione su /api/docs
  2. Clicca su "Autorizza"
  3. Incolla la tua chiave API: imk_live_...
  4. Clicca su un endpoint (es. GET /api/pages)
  5. Clicca su "Prova"
  6. Clicca su "Esegui"
  7. Visualizza la risposta reale dalla tua API

È come Postman, ma integrato nella tua documentazione.

Parte 7: Integrazione con ChatGPT

Qui è dove tutto si unisce. Con una specifica OpenAPI, puoi creare un GPT personalizzato che comprende la tua API.

Creazione di un GPT personalizzato

  1. Vai su ChatGPT → Crea un GPT
  2. Aggiungi un'Azione
  3. Importa da URL: https://app.imerkar.com/api/docs/openapi.json
  4. Imposta l'autenticazione su Token Bearer
  5. Aggiungi la tua chiave API
  6. Testalo:
Tu: "Elenca tutte le mie pagine"

ChatGPT: Lascia che controlli le tue pagine...
[Esegue la richiesta GET /api/pages]

ChatGPT: "Hai 5 pagine:
1. Home
2. Chi Siamo
3. Servizi
4. Contatti
5. Blog"

Tu: "Crea una nuova pagina chiamata 'Prezzi' con una descrizione sui nostri piani"

ChatGPT: [Esegue la richiesta POST /api/pages]

ChatGPT: "Fatto! Ho creato una nuova pagina chiamata 'Prezzi' con la tua descrizione."

Funziona e basta. ChatGPT legge la tua specifica OpenAPI e sa come usare la tua API.

Parte 8: Tracciamento dell'utilizzo e limitazione della frequenza (Rate Limiting)

Una volta che hai chiavi API in circolazione, devi tracciare come vengono utilizzate e prevenire abusi.

Registrazione dell'utilizzo

Ho creato una semplice tabella di log di utilizzo:

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 -- Timestamp ISO 8601
);

-- Indice per le query di rate limiting
CREATE INDEX idx_usage_logs_key_time ON api_key_usage_logs(api_key_id, created_at);

Ogni richiesta API viene registrata:

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()
]
);
}

Limitazione della frequenza (Rate Limiting)

Ho implementato una semplice limitazione della frequenza:

  • 30 richieste al minuto per chiave API
  • 1000 richieste al giorno
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;
}

Se il limite viene superato, restituisci un errore utile:

{
"error": "Limite di frequenza superato",
"code": "RATE_LIMIT_EXCEEDED",
"details": {
"limit": 30,
"window": "1 minuto",
"retryAfter": 42
}
}

Cosa ho imparato

Costruire questa funzionalità mi ha insegnato molto:

1. La sicurezza non è facoltativa

Non archiviare mai token in testo semplice. Eseguire l'hashing di tutto. Utilizzare crypto.subtle per la casualità. Pensare a cosa succede se il tuo database viene violato.

2. L'UX è importante anche per gli strumenti per sviluppatori

Anche i token API hanno bisogno di una buona UX. Mostrali una volta con un grande avviso. Permetti agli utenti di nominare i loro token. Mostra quando sono stati utilizzati l'ultima volta. Rendi facile la revoca.

3. OpenAPI è un superpotere

Scrivere una specifica OpenAPI sembra un lavoro di routine all'inizio, ma è incredibilmente prezioso. Abilita l'integrazione con ChatGPT, Swagger UI, la generazione di SDK client e l'automazione dei test.

4. Inizia in modo semplice, itera

Non ho costruito tutto questo in una volta. La versione 1 era solo generazione di token e autenticazione. Poi ho aggiunto l'interfaccia utente della dashboard. Poi la documentazione OpenAPI. Poi il tracciamento dell'utilizzo. Ogni iterazione l'ha migliorata.

5. Gli indici del database sono cruciali

Indicizzare la colonna key_hash rende l'autenticazione veloce. Senza di essa, ogni richiesta scansionerebbe l'intera tabella. Con essa, le ricerche sono istantanee.

Lo stack tecnologico

Per riferimento, ecco cosa ho usato:

  • Backend: Cloudflare Workers + framework Hono
  • Database: Cloudflare D1 (SQLite)
  • Frontend: React + Tailwind CSS
  • Auth (sessioni): Clerk
  • OpenAPI: Speca scritta a mano (si potrebbe usare @hono/zod-openapi per la type safety)
  • Linguaggio: TypeScript

Prossimi passi

C'è ancora altro che voglio aggiungere:

  • Permessi basati su scope - Al momento, i token hanno accesso completo. Voglio aggiungere scope come pages:read, pages:write, media:read, ecc.
  • Supporto Webhook - Permettere agli utenti di iscriversi ad eventi (page.created, page.published, ecc.)
  • Generazione SDK - Utilizzare la specifica OpenAPI per generare automaticamente librerie client in Python, JavaScript, PHP, ecc.
  • Analisi migliori - Statistiche di utilizzo più dettagliate con grafici e tendenze
  • Whitelisting IP - Consentire agli utenti di limitare i token a indirizzi IP specifici

Risorse

Se stai costruendo qualcosa di simile, queste risorse mi sono state utili:

Rimani Aggiornato

Ricevi gli ultimi articoli e insights nella tua casella di posta.

Unsubscribe anytime. No spam, ever.