Guía práctica sobre autenticación y autorización de API, incluidos OAuth 2.0, JWT y gestión de claves de API.
OAuth 2.0 es el marco de autorización más utilizado para el acceso a la API. Seleccionar el tipo de concesión adecuado es fundamental.
| Tipo de subvención | Caso práctico | Nivel de seguridad |
|---|---|---|
| Authorization Code + PKCE | SPA y aplicaciones móviles (recomendado) | Más alto |
| Authorization Code | Aplicaciones web del lado del servidor | Alta |
| Client Credentials | Comunicación de servidor a servidor (M2M) | Medio |
| Implícito (obsoleto) | SPA heredados | Bajo |
| Contraseña del propietario del recurso (obsoleta) | Sólo de primera mano y de gran confianza | Bajo |
Implicit Grant está ahora obsoleto porque el token se expone en el fragmento de URL. Utilice Código de autorización + PKCE para SPA.
// 1. Generar aleatoriamente code_verifier function generateCodeVerifier() { const array = new Uint8Array(32); crypto.getRandomValues(array); return btoa(String.fromCharCode(...array)) .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } // 2. Generar code_challenge con SHA-256 async function generateCodeChallenge(verifier) { const encoder = new TextEncoder(); const data = encoder.encode(verifier); const digest = await crypto.subtle.digest('SHA-256', data); return btoa(String.fromCharCode(...new Uint8Array(digest))) .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } // 3. Solicitud de autorización const verifier = generateCodeVerifier(); const challenge = await generateCodeChallenge(verifier); const authUrl = `https://auth.example.com/authorize?` + `response_type=code&` + `client_id=${clientId}&` + `redirect_uri=${redirectUri}&` + `code_challenge=${challenge}&` + `code_challenge_method=S256&` + `scope=read write`;
JWT se utiliza ampliamente como token de autenticación sin estado, pero los errores de implementación conducen directamente a vulnerabilidades de seguridad.
const jwt = require('jsonwebtoken'); // --- Emisión de tokens --- function issueTokens(user) { const accessToken = jwt.sign( { sub: user.id, role: user.role }, process.env.JWT_SECRET, { algorithm: 'HS256', expiresIn: '15m', issuer: 'api.example.com', audience: 'app.example.com', } ); const refreshToken = jwt.sign( { sub: user.id, type: 'refresh' }, process.env.REFRESH_SECRET, { algorithm: 'HS256', expiresIn: '7d' } ); return { accessToken, refreshToken }; } // Verificación de tokens function verifyAccessToken(token) { return jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'], // ← ¡Especificar siempre! issuer: 'api.example.com', audience: 'app.example.com', }); }
| Almacenamiento | Resistencia XSS | Resistencia a CSRF | Recomendación |
|---|---|---|---|
| HttpOnly Cookie | ◎ | △ (requiere configuración SameSite) | Recomendado |
| Memoria (variable) | ◎ | ◎ | Pérdida de navegación en la página |
| localStorage | ✕ (puede ser robado mediante XSS). | ◎ | No recomendado |
| sessionStorage | ✕ (puede ser robado mediante XSS). | ◎ | Uso limitado |
Almacenar el token de acceso en una cookie con los atributos HttpOnly, Secure y SameSite=Strict es el enfoque más seguro. Esto evita el robo del token mediante ataques XSS.
Las claves API son un mecanismo de autenticación sencillo, pero una gestión inadecuada puede acarrear graves riesgos.
Generar con suficiente entropía (256 bits o más). Almacenar con hash; nunca guardar texto plano en la base de datos.
Asigne ámbitos mínimos por clave (sólo lectura, sólo recursos específicos, etc.). Evita los permisos comodín.
Rote las llaves con regularidad (en un plazo de 90 días). Establezca un periodo de gracia para las llaves antiguas. Revóquelas inmediatamente en caso de fuga.
Enviar mediante cabecera (X-API-Key o Authorization). No incluir en parámetros de consulta de URL (aparecen en los registros).
No codifique las claves en el código fuente. Adminístrelas con variables de entorno o un gestor de secretos.
Supervisar el uso de la clave API. Detecte patrones anómalos (solicitudes masivas, accesos desde IP desconocidas).
const crypto = require('crypto'); // Hash de la clave API con SHA-256 y comparar async function validateApiKey(req, res, next) { const apiKey = req.headers['x-api-key']; if (!apiKey) { return res.status(401).json({ error: 'API key required' }); } // Hash y buscar en DB (timingSafeEqual no es necesario ya que comparamos hashes) const hashedKey = crypto .createHash('sha256') .update(apiKey) .digest('hex'); const keyRecord = await db.findApiKey(hashedKey); if (!keyRecord || keyRecord.revokedAt) { return res.status(403).json({ error: 'Invalid API key' }); } // Comprobar caducidad if (keyRecord.expiresAt && new Date() > keyRecord.expiresAt) { return res.status(403).json({ error: 'API key expired' }); } req.apiClient = keyRecord; next(); }
AI agents and LLM-powered applications require additional authentication and authorization patterns beyond traditional API security.
Assign different access levels per model tier (e.g., GPT-4 requires elevated scope). Prevent unauthorized use of expensive or sensitive models.
Embed token budgets in access tokens or API key metadata. Enforce per-request and per-day limits to prevent denial-of-wallet attacks.
Check user permissions before executing certain prompt types (e.g., code generation, data analysis). Map prompt categories to role-based permissions.
Ensure conversation history, fine-tuned models, and RAG data are isolated per tenant. Use tenant-scoped API keys with namespace enforcement.
Assign cryptographic identities to AI agents. Use signed JWTs for inter-agent communication. Verify agent identity before granting tool access.
Automate API key rotation for LLM providers. Use short-lived tokens for agent sessions. Revoke credentials immediately when agents are decommissioned.
| Scenario | Auth Method | Required Scope | Approval |
|---|---|---|---|
| Agent reads public data | API Key | data:read |
Automatic |
| Agent modifies user data | OAuth 2.0 (delegated) | data:write |
User consent required |
| Agent calls external API | OAuth 2.0 + mTLS | external:invoke |
Human-in-the-loop |
| Agent executes code / shell | Scoped JWT + Sandbox | exec:sandbox |
Admin approval + audit log |
import jwt import time from datetime import datetime, timedelta class AgentTokenIssuer: """Issues scoped, short-lived tokens for AI agents.""" ALLOWED_SCOPES = { "reader": ["data:read"], "writer": ["data:read", "data:write"], "executor": ["data:read", "data:write", "exec:sandbox"], } def __init__(self, secret_key: str): self.secret_key = secret_key def issue_agent_token( self, agent_id: str, role: str, tenant_id: str, token_budget: int = 10000, ttl_minutes: int = 15, ) -> str: # Validate role and resolve scopes if role not in self.ALLOWED_SCOPES: raise ValueError(f"Invalid role: {role}") payload = { "sub": agent_id, "tenant": tenant_id, "scopes": self.ALLOWED_SCOPES[role], "token_budget": token_budget, "iat": datetime.utcnow(), "exp": datetime.utcnow() + timedelta(minutes=ttl_minutes), "type": "agent", } return jwt.encode(payload, self.secret_key, algorithm="HS256") def verify_tool_access(self, token: str, required_scope: str) -> dict: # Decode and verify agent token payload = jwt.decode( token, self.secret_key, algorithms=["HS256"] ) if required_scope not in payload["scopes"]: raise PermissionError( f"Agent {payload['sub']} lacks scope: {required_scope}" ) return payload
Related: LLM06: Excessive Agency, ASI01: Excessive Agency, ASI03: Insecure Tool/Function Calling
| Método | Sin estado | Caso práctico | Consideraciones de seguridad |
|---|---|---|---|
| OAuth 2.0 + PKCE | ○ | API que requieren autorización del usuario | estado/PKCE requerido, gestión de la caducidad de tokens |
| JWT Bearer | ○ | Comunicación entre microservicios | Se requiere especificación del algoritmo, no hay datos sensibles en la carga útil |
| Clave API | ○ | Servidor a servidor, integraciones externas | Rotación y restricción del alcance requeridas |
| mTLS | ○ | Comunicación M2M de alta seguridad | Elevado coste operativo de la gestión de certificados |
| Cookie de sesión | ✕ | Aplicaciones web tradicionales | Protección CSRF necesaria, problemas de escalabilidad |