Volver al Blog

Creación de APIs Seguras con Autenticación por Tokens y OpenAPI

2025-11-2312 min read

Por qué necesitaba tokens de API

He estado creando una aplicación web con un panel de control para gestionar contenido. Todo funcionaba bien a través de la interfaz de usuario, pero empecé a pensar en la automatización. ¿Qué pasa si quiero usar ChatGPT para gestionar contenido? ¿O automatizar tareas repetitivas con scripts?

Necesitaba una API con una autenticación adecuada. Algo seguro, fácil de usar y bien documentado.

Esto es lo que construí y lo que aprendí.

Los requisitos

Antes de sumergirme en el código, definí lo que realmente necesitaba:

  • Generación de tokens segura: Sin patrones adivinables, criptográficamente segura
  • Visualización única: Mostrar el token una sola vez, nunca más (como GitHub)
  • Nunca almacenar texto plano: Hashear todo antes de que llegue a la base de datos
  • Soporte de revocación: Permitir a los usuarios eliminar tokens que ya no necesitan
  • Seguimiento de uso: Mostrar cuándo y cómo se están utilizando los tokens
  • Documentación OpenAPI: Facilitar que los asistentes de IA entiendan la API
  • Pruebas interactivas: Swagger UI para que los desarrolladores prueben los endpoints

Suena a mucho, ¿verdad? Permítanme desglosar cómo funciona cada pieza.

Parte 1: Generación de tokens seguros

El primer desafío fue crear tokens que fueran seguros y fáciles de usar para el usuario. Me decidí por este formato:

imk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

Permítanme explicar cada parte:

  • imk - Prefijo de mi producto (abreviatura de "Imerkar")
  • live - Entorno (en vivo vs. prueba)
  • a1b2c3... - 32 caracteres aleatorios

Este formato está inspirado en las claves API de Stripe, y hay una buena razón para ello:

  1. Escaneo de prefijos: Si alguien incluye accidentalmente un token en GitHub, las herramientas pueden detectarlo escaneando el prefijo imk_
  2. Separación de entornos: Se puede saber de un vistazo si es una clave de prueba o en vivo
  3. Identificación fácil de usar: Los usuarios pueden identificar las claves por su prefijo incluso después de que el token completo esté oculto

El código de generación

Utilicé la Web Crypto API para la generación aleatoria segura:

async function generateApiKey(): Promise<string> {
// Generar 24 bytes (192 bits) de datos aleatorios
const randomBytes = new Uint8Array(24);
crypto.getRandomValues(randomBytes);

// Convertir a codificación base64url (segura para URL, sin relleno)
const randomString = btoa(String.fromCharCode(...randomBytes))
.replace(/\+/g, '-')
.replace(/\/g, '_')
.replace(/=/g, '');

return `imk_live_${randomString}`;
}

Esto genera tokens aleatorios criptográficamente seguros. La función crypto.getRandomValues() utiliza la fuente de entropía del sistema operativo, haciendo que los tokens sean impredecibles. Utilizo la codificación base64url para asegurar que el token sea seguro para URL y mantenga la entropía completa.

Parte 2: Almacenamiento seguro de tokens

Aquí está la regla de oro: nunca almacene tokens de API en texto plano.

Piénselo como las contraseñas. Si su base de datos se ve comprometida, no querrá que los atacantes tengan claves API funcionales. Así que, al igual que con las contraseñas, hasheamos los tokens antes de almacenarlos.

El esquema de la base de datos

CREATE TABLE api_keys (
id TEXT PRIMARY KEY,
customer_id TEXT NOT NULL,
name TEXT NOT NULL,
key_prefix TEXT NOT NULL, -- Prefijo para mostrar (imk_live_xxx)
key_hash TEXT NOT NULL, -- Hash SHA-256
scopes TEXT NOT NULL, -- Array JSON de permisos
last_used_at TEXT, -- Marca de tiempo ISO 8601
last_used_ip TEXT,
expires_at TEXT, -- Marca de tiempo ISO 8601
revoked INTEGER DEFAULT 0,
created_at TEXT NOT NULL -- Marca de tiempo ISO 8601
);

-- Índice para búsquedas rápidas de autenticación
CREATE INDEX idx_api_keys_hash ON api_keys(key_hash);

-- Índice para listar las claves del cliente
CREATE INDEX idx_api_keys_customer ON api_keys(customer_id);

Nótese que almaceno el key_prefix (la parte legible como imk_live_abc) por separado. Esto me permite mostrar a los usuarios algo como:

imk_live_a1b2c3d4... (Creado el 15 de enero de 2025)

Pueden identificar qué token es cuál sin ver el valor completo.

Hasheo de los tokens

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

Cuando un usuario crea un token:

  1. Generar el token aleatorio
  2. Hashearlo con SHA-256
  3. Almacenar el hash en la base de datos
  4. Devolver el token completo al usuario (¡solo esta vez!)
  5. Nunca volver a mostrar el token completo

Parte 3: Middleware de autenticación

Ahora que podemos generar y almacenar tokens, necesitamos autenticar las solicitudes. Construí un middleware que admite varios métodos de autenticación:

  1. Tokens JWT de Clerk: Para usuarios del panel de control con sesión iniciada
  2. Tokens de API: Para integraciones externas

El flujo de autenticación

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

if (!authHeader?.startsWith('Bearer ')) {
return c.json({ error: 'Falta autorización' }, 401);
}

const token = authHeader.substring(7);

// Intentar autenticación con clave API
if (token.startsWith('imk_')) {
const customerId = await verifyApiKey(token);
if (customerId) {
c.set('customerId', customerId);
return;
}
}

// Intentar autenticación JWT
const customerId = await verifyJWT(token);
if (customerId) {
c.set('customerId', customerId);
return;
}

return c.json({ error: 'Token no válido' }, 401);
}

Verificación de tokens de API

Cuando llega una clave API, esto es lo que sucede:

async function verifyApiKey(apiKey: string): Promise<string | null> {
// Hashear la clave proporcionada
const keyHash = await hashApiKey(apiKey);

// Buscar el hash en la base de datos
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;

// Actualizar la marca de tiempo de último uso
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;
}

Esto es rápido porque indexamos la columna key_hash. La búsqueda en la base de datos es O(1) gracias al índice.

Parte 4: La interfaz de usuario del panel de control

Los usuarios necesitan una forma de crear y gestionar sus tokens. Integré esto en la página de Configuración de mi panel de control.

La experiencia de usuario

  1. Navegar a Configuración → Claves API
  2. Hacer clic en "Crear nueva clave API"
  3. Introducir un nombre (ej. "Integración con Zapier")
  4. Hacer clic en Crear
  5. Ver el token completo con una gran advertencia:
⚠️ Clave API Creada

imk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

¡Guarda esta clave ahora! ¡No podrás volver a verla!
  1. Copiar al portapapeles
  2. Después de cerrar, solo ver el prefijo:
Integración con Zapier
imk_live_a1b2c3d4...
Creada: 15 de enero de 2025
Último uso: 16 de enero de 2025
[Revocar]

El componente de React

Aquí hay una versión simplificada del flujo de creación de claves:

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); // Clave completa, mostrada solo una vez
setShowCreateModal(false);
}

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

return (
<>
{newKey && (
<Alert>
<AlertTitle>Clave API Creada</AlertTitle>
<AlertDescription>
<code>{newKey}</code>
<p>¡Guárdala ahora! ¡No la volverás a ver!</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: Documentación OpenAPI

Aquí es donde se pone realmente interesante. Una vez que tienes una API, necesitas documentación. Pero no cualquier documentación. Documentación legible por máquinas que los asistentes de IA puedan entender.

Ahí es donde entra OpenAPI (anteriormente Swagger).

¿Qué es OpenAPI?

OpenAPI es una forma estándar de describir APIs REST. Es un archivo JSON/YAML que te dice:

  • Qué endpoints existen
  • Qué parámetros aceptan
  • Qué devuelven
  • Cómo autenticarse
  • Qué errores pueden ocurrir

La belleza es que ChatGPT, Claude, Zapier y cientos de otras herramientas pueden leer las especificaciones OpenAPI y entender automáticamente tu API.

Creación del documento OpenAPI

Creé una especificación OpenAPI 3.0 para mi API:

const openAPIDocument = {
openapi: '3.0.0',
info: {
title: 'Imerkar API',
version: '1.0.0',
description: 'Construye y gestiona sitios web programáticamente',
},
servers: [
{
url: 'https://app.imerkar.com',
description: 'Servidor de producción',
},
],
components: {
securitySchemes: {
BearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT o Clave API',
description: 'Usa tu clave API con autenticación Bearer',
},
},
},
security: [{ BearerAuth: [] }],
paths: {
'/api/pages': {
get: {
summary: 'Listar todas las páginas',
tags: ['Páginas'],
responses: {
'200': {
description: 'Lista de páginas',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
pages: {
type: 'array',
items: { $ref: '#/components/schemas/Page' }
}
}
}
}
}
}
}
},
post: {
summary: 'Crear una nueva página',
tags: ['Páginas'],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['title'],
properties: {
title: { type: 'string' },
description: { type: 'string' },
}
}
}
}
},
responses: {
'201': {
description: 'Página creada',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/Page' }
}
}
}
}
}
}
}
};

Esto puede parecer verboso, pero es increíblemente potente. Con este único documento, cualquier herramienta puede entender toda tu API.

Servir la especificación OpenAPI

Creé dos endpoints:

// Servir la especificación JSON
app.get('/api/docs/openapi.json', (c) => {
return c.json(openAPIDocument);
});

// Servir la especificación YAML (algunas herramientas prefieren 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 para pruebas interactivas

Las especificaciones OpenAPI son geniales, pero los desarrolladores quieren probar tu API, no solo leer sobre ella. Ahí es donde entra Swagger UI.

Swagger UI es una interfaz web que:

  • Lee tu especificación OpenAPI
  • Genera documentación interactiva
  • Permite a los desarrolladores probar endpoints directamente en el navegador
  • Muestra ejemplos de solicitudes/respuestas

Configuración de Swagger UI

Creé una página HTML simple que carga Swagger UI desde un CDN:

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

Ahora, cuando los desarrolladores visitan https://app.imerkar.com/api/docs, ven un explorador de API hermoso e interactivo.

Uso de Swagger UI

El flujo de trabajo es simple:

  1. Abrir la documentación en /api/docs
  2. Hacer clic en "Autorizar"
  3. Pegar tu clave API: imk_live_...
  4. Hacer clic en un endpoint (ej. GET /api/pages)
  5. Hacer clic en "Probarlo"
  6. Hacer clic en "Ejecutar"
  7. Ver la respuesta real de tu API

Es como Postman, pero integrado en tu documentación.

Parte 7: Integración con ChatGPT

Aquí es donde todo se une. Con una especificación OpenAPI, puedes crear un GPT personalizado que entienda tu API.

Creación de un GPT personalizado

  1. Ir a ChatGPT → Crear un GPT
  2. Añadir una Acción
  3. Importar desde URL: https://app.imerkar.com/api/docs/openapi.json
  4. Configurar la autenticación como Token Bearer
  5. Añadir tu clave API
  6. Probarlo:
Tú: "Lista todas mis páginas"

ChatGPT: Déjame revisar tus páginas...
[Realiza la solicitud GET /api/pages]

ChatGPT: "Tienes 5 páginas:
1. Inicio
2. Quiénes somos
3. Servicios
4. Contacto
5. Blog"

Tú: "Crea una nueva página llamada 'Precios' con una descripción sobre nuestros planes"

ChatGPT: [Realiza la solicitud POST /api/pages]

ChatGPT: "¡Listo! Creé una nueva página llamada 'Precios' con tu descripción."

Simplemente funciona. ChatGPT lee tu especificación OpenAPI y sabe cómo usar tu API.

Parte 8: Seguimiento de uso y limitación de velocidad

Una vez que tienes claves API en circulación, necesitas rastrear cómo se están utilizando y prevenir el abuso.

Registro de uso

Creé una tabla simple de registro de uso:

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 -- Marca de tiempo ISO 8601
);

-- Índice para consultas de limitación de velocidad
CREATE INDEX idx_usage_logs_key_time ON api_key_usage_logs(api_key_id, created_at);

Cada solicitud API se registra:

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

Limitación de velocidad (Rate Limiting)

Implementé una limitación de velocidad simple:

  • 30 solicitudes por minuto por clave API
  • 1000 solicitudes por día
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 se excede el límite, devuelve un error útil:

{
"error": "Límite de velocidad excedido",
"code": "RATE_LIMIT_EXCEEDED",
"details": {
"limit": 30,
"window": "1 minuto",
"retryAfter": 42
}
}

Lo que aprendí

Construir esta característica me enseñó mucho:

1. La seguridad no es opcional

Nunca almacene tokens en texto plano. Hashee todo. Use crypto.subtle para la aleatoriedad. Piense en lo que sucede si su base de datos se filtra.

2. La experiencia de usuario (UX) importa para las herramientas de desarrollador

Las claves API también necesitan una buena UX. Muéstrelas una vez con una gran advertencia. Permita a los usuarios nombrar sus tokens. Muestre cuándo se usaron por última vez. Facilite la revocación.

3. OpenAPI es un superpoder

Escribir una especificación OpenAPI se siente como un trabajo tedioso al principio, pero es increíblemente valioso. Permite la integración con ChatGPT, Swagger UI, la generación de SDKs de cliente y la automatización de pruebas.

4. Empiece simple, itere

No construí todo esto a la vez. La versión 1 fue solo generación de tokens y autenticación. Luego agregué la interfaz de usuario del panel de control. Luego la documentación OpenAPI. Luego el seguimiento de uso. Cada iteración lo mejoró.

5. Los índices de la base de datos son cruciales

Indexar la columna key_hash hace que la autenticación sea rápida. Sin ella, cada solicitud escanearía toda la tabla. Con ella, las búsquedas son instantáneas.

El stack tecnológico

Como referencia, esto es lo que utilicé:

  • Backend: Cloudflare Workers + framework Hono
  • Base de datos: Cloudflare D1 (SQLite)
  • Frontend: React + Tailwind CSS
  • Auth (sesiones): Clerk
  • OpenAPI: Especificación escrita a mano (podría usar @hono/zod-openapi para seguridad de tipos)
  • Lenguaje: TypeScript

Próximos pasos

Todavía hay más que quiero añadir:

  • Permisos basados en ámbitos (Scopes): Ahora mismo, los tokens tienen acceso total. Quiero añadir ámbitos como pages:read, pages:write, media:read, etc.
  • Soporte de Webhooks: Permitir a los usuarios suscribirse a eventos (page.created, page.published, etc.)
  • Generación de SDK: Usar la especificación OpenAPI para generar automáticamente librerías cliente en Python, JavaScript, PHP, etc.
  • Mejores analíticas: Estadísticas de uso más detalladas con gráficos y tendencias
  • Lista blanca de IP: Permitir a los usuarios restringir tokens a direcciones IP específicas

Recursos

Si está construyendo algo similar, estos recursos me ayudaron:

Mantente Actualizado

Recibe las últimas publicaciones e insights en tu bandeja de entrada.

Unsubscribe anytime. No spam, ever.