Wiki/Fundamentos/APIs REST y Seguridad
Principiante
20 min lectura

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

1. Stateless

Cada petición contiene toda la información necesaria

2. Cliente-Servidor

Separación de responsabilidades

3. Cacheable

Las respuestas pueden ser cacheadas

4. Uniform Interface

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: 1640995200

Validació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.

❌ Vulnerable
app.get('/api/users/:id', (req, res) => {
  const user = db.users.findById(req.params.id);
  res.json(user); // ¡Sin verificar permisos!
});
✅ Seguro
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.

❌ Vulnerable
app.get('/api/users/:id', (req, res) => {
  const user = db.users.findById(req.params.id);
  res.json(user); // Incluye password, token, etc
});
✅ Seguro
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.

❌ Vulnerable
app.put('/api/users/:id', (req, res) => {
  db.users.update(req.params.id, req.body);
  // Cliente puede enviar { role: 'admin' }!
});
✅ Seguro
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

Recursos Adicionales

Siguiente Paso

Aprende sobre CORS y la Same-Origin Policy

CORS y Same-Origin Policy