APIs REST y Seguridad
Principios de diseño seguro para APIs RESTful y mejores prácticas de implementación
¿Qué es una API REST?
REST (Representational State Transfer) es un estilo arquitectónico para diseñar servicios web. Una API RESTful utiliza HTTP requests para realizar operaciones CRUD (Create, Read, Update, Delete).
Principios REST
Cada petición contiene toda la información necesaria
Separación de responsabilidades
Las respuestas pueden ser cacheadas
Interfaz consistente y predecible
Métodos HTTP en REST
GET /api/users # Obtener lista de usuarios GET /api/users/123 # Obtener usuario específico POST /api/users # Crear nuevo usuario PUT /api/users/123 # Actualizar usuario completo PATCH /api/users/123 # Actualizar campos específicos DELETE /api/users/123 # Eliminar usuario
Autenticación en APIs REST
1. API Keys
Claves únicas para identificar y autenticar a los clientes de la API.
// Cliente
fetch('https://api.example.com/data', {
headers: {
'X-API-Key': 'sk_live_abc123...'
}
});
// Servidor (Express)
app.use('/api', (req, res, next) => {
const apiKey = req.headers['x-api-key'];
if (!apiKey || !validateApiKey(apiKey)) {
return res.status(401).json({ error: 'API Key inválida' });
}
req.client = getClientByApiKey(apiKey);
next();
});Limitación: Las API Keys no expiran automáticamente y pueden ser interceptadas si no se usa HTTPS.
2. Bearer Tokens (JWT)
Tokens JSON Web Tokens que contienen claims sobre el usuario.
// Cliente - Login
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const { token } = await response.json();
// Cliente - Usar token
fetch('/api/protected', {
headers: {
'Authorization': `Bearer ${token}`
}
});
// Servidor - Verificar token
const jwt = require('jsonwebtoken');
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Token requerido' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Token inválido' });
}
req.user = user;
next();
});
}
app.get('/api/protected', authenticateToken, (req, res) => {
res.json({ data: 'Contenido protegido', user: req.user });
});3. OAuth 2.0
Framework de autorización que permite acceso de terceros sin compartir credenciales.
// Flujo de autorización OAuth 2.0
// 1. Cliente redirige a servidor de autorización
window.location = `https://oauth.example.com/authorize?
client_id=YOUR_CLIENT_ID&
redirect_uri=https://yourapp.com/callback&
response_type=code&
scope=read write`;
// 2. Usuario se autentica y aprueba
// 3. Callback recibe código
// GET https://yourapp.com/callback?code=abc123
// 4. Intercambiar código por token
const tokenResponse = await fetch('https://oauth.example.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
code: 'abc123',
client_id: 'YOUR_CLIENT_ID',
client_secret: 'YOUR_SECRET',
redirect_uri: 'https://yourapp.com/callback'
})
});
const { access_token } = await tokenResponse.json();
// 5. Usar access token para llamar a la API
fetch('https://api.example.com/user', {
headers: { 'Authorization': `Bearer ${access_token}` }
});Rate Limiting
Limitar el número de peticiones que un cliente puede hacer en un período de tiempo.
// Usando express-rate-limit
const rateLimit = require('express-rate-limit');
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 100, // máximo 100 peticiones por ventana
message: 'Demasiadas peticiones, intenta más tarde',
standardHeaders: true, // Return rate limit info in headers
legacyHeaders: false,
});
// Aplicar a todas las rutas /api
app.use('/api/', apiLimiter);
// Rate limit más estricto para login (prevenir brute force)
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // solo 5 intentos de login
skipSuccessfulRequests: true, // no contar logins exitosos
});
app.post('/api/auth/login', loginLimiter, loginHandler);
// Headers de respuesta
// X-RateLimit-Limit: 100
// X-RateLimit-Remaining: 85
// X-RateLimit-Reset: 1640995200Validación de Entrada en APIs
// Usando Joi para validación
const Joi = require('joi');
const userSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).required(),
age: Joi.number().integer().min(18).max(120),
role: Joi.string().valid('user', 'admin', 'editor')
});
function validateRequest(schema) {
return (req, res, next) => {
const { error, value } = schema.validate(req.body, {
abortEarly: false, // reportar todos los errores
stripUnknown: true // eliminar campos no definidos
});
if (error) {
const errors = error.details.map(d => ({
field: d.path.join('.'),
message: d.message
}));
return res.status(400).json({ errors });
}
req.validatedBody = value;
next();
};
}
// Uso en rutas
app.post('/api/users',
validateRequest(userSchema),
async (req, res) => {
const user = await createUser(req.validatedBody);
res.status(201).json(user);
}
);Vulnerabilidades Comunes en APIs
1. Broken Object Level Authorization (BOLA)
No validar que el usuario tiene permisos para acceder al objeto solicitado.
app.get('/api/users/:id', (req, res) => {
const user = db.users.findById(req.params.id);
res.json(user); // ¡Sin verificar permisos!
});app.get('/api/users/:id', auth, (req, res) => {
if (req.user.id !== req.params.id &&
req.user.role !== 'admin') {
return res.status(403).json({ error: 'Forbidden' });
}
const user = db.users.findById(req.params.id);
res.json(user);
});2. Excessive Data Exposure
Devolver más datos de los necesarios, confiando en que el cliente filtre.
app.get('/api/users/:id', (req, res) => {
const user = db.users.findById(req.params.id);
res.json(user); // Incluye password, token, etc
});app.get('/api/users/:id', (req, res) => {
const user = db.users.findById(req.params.id);
const safe = {
id: user.id,
name: user.name,
email: user.email
};
res.json(safe);
});3. Mass Assignment
Permitir que el cliente modifique propiedades que no debería.
app.put('/api/users/:id', (req, res) => {
db.users.update(req.params.id, req.body);
// Cliente puede enviar { role: 'admin' }!
});app.put('/api/users/:id', (req, res) => {
const allowed = ['name', 'email', 'bio'];
const data = pick(req.body, allowed);
db.users.update(req.params.id, data);
});Mejores Prácticas de Seguridad
Usar HTTPS siempre
Nunca exponer APIs sobre HTTP sin cifrar
Validar TODO input
Type, format, length, range - validar cada campo
Implementar autenticación y autorización
Verificar identidad Y permisos en cada endpoint
Rate limiting
Prevenir abuso y ataques de fuerza bruta
Logging y monitoreo
Registrar accesos, errores y actividad sospechosa
Versionado de API
Usar /v1/, /v2/ para mantener compatibilidad
Documentar con OpenAPI/Swagger
Facilita testing y uso correcto de la API