TechLead
Lección 7 de 9
5 min de lectura
Seguridad Web

Mejores Prácticas de Seguridad de API

Asegura tus APIs con autenticación, limitación de tasa, validación de entrada y manejo adecuado de errores.

Fundamentos de Seguridad de API

Las APIs son frecuentemente la parte más expuesta de tu aplicación. Una API segura requiere múltiples capas de protección.

Autenticación de API

1. Claves de API

// Autenticación simple con clave de API
function apiKeyAuth(req, res, next) {
  const apiKey = req.headers['x-api-key'];

  if (!apiKey) {
    return res.status(401).json({ error: 'Clave de API requerida' });
  }

  // Validar clave (usar comparación segura en tiempo)
  const validKey = await db.apiKey.findUnique({
    where: { key: hashApiKey(apiKey) },
    include: { user: true },
  });

  if (!validKey || validKey.revoked) {
    return res.status(401).json({ error: 'Clave de API inválida' });
  }

  req.user = validKey.user;
  req.apiKey = validKey;
  next();
}

// Generar claves de API seguras
function generateApiKey() {
  return crypto.randomBytes(32).toString('hex');
}

// Almacenar claves hasheadas
function hashApiKey(key) {
  return crypto.createHash('sha256').update(key).digest('hex');
}

2. OAuth 2.0 / JWT

// Middleware de autenticación JWT
async function jwtAuth(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Token Bearer requerido' });
  }

  const token = authHeader.split(' ')[1];

  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);

    // Verificar que token no está revocado
    const isRevoked = await isTokenRevoked(payload.jti);
    if (isRevoked) {
      return res.status(401).json({ error: 'Token revocado' });
    }

    req.user = payload;
    next();
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expirado' });
    }
    return res.status(401).json({ error: 'Token inválido' });
  }
}

Limitación de Tasa

const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');

// Diferentes límites para diferentes endpoints
const publicLimiter = rateLimit({
  store: new RedisStore({ client: redisClient }),
  windowMs: 15 * 60 * 1000,
  max: 100,
  standardHeaders: true,
  message: { error: 'Demasiadas solicitudes' },
});

const authLimiter = rateLimit({
  store: new RedisStore({ client: redisClient }),
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: { error: 'Demasiados intentos de autenticación' },
});

// Limitación por usuario
const userLimiter = rateLimit({
  store: new RedisStore({ client: redisClient }),
  windowMs: 60 * 1000,
  max: 60,
  keyGenerator: (req) => req.user?.id || req.ip,
  message: { error: 'Límite de tasa excedido' },
});

app.use('/api', publicLimiter);
app.use('/api/auth', authLimiter);
app.use('/api/protected', jwtAuth, userLimiter);

Validación de Entrada

import { z } from 'zod';

// Definir esquemas
const createUserSchema = z.object({
  email: z.string().email().max(255),
  name: z.string().min(1).max(100),
  age: z.number().int().min(0).max(150).optional(),
});

const paginationSchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  sort: z.enum(['asc', 'desc']).default('desc'),
});

// Middleware de validación
function validate(schema) {
  return (req, res, next) => {
    try {
      req.validated = schema.parse({
        ...req.body,
        ...req.query,
        ...req.params,
      });
      next();
    } catch (error) {
      if (error instanceof z.ZodError) {
        return res.status(400).json({
          error: 'Validación falló',
          details: error.errors,
        });
      }
      next(error);
    }
  };
}

app.post('/api/users', validate(createUserSchema), createUser);
app.get('/api/users', validate(paginationSchema), listUsers);

Manejo Seguro de Errores

// Clases de error personalizadas
class AppError extends Error {
  constructor(message, statusCode, code) {
    super(message);
    this.statusCode = statusCode;
    this.code = code;
    this.isOperational = true;
  }
}

class NotFoundError extends AppError {
  constructor(resource) {
    super(`${resource} no encontrado`, 404, 'NOT_FOUND');
  }
}

class UnauthorizedError extends AppError {
  constructor(message = 'No autorizado') {
    super(message, 401, 'UNAUTHORIZED');
  }
}

// Middleware de manejo de errores
function errorHandler(err, req, res, next) {
  // Registrar error internamente
  logger.error({
    error: err.message,
    stack: err.stack,
    path: req.path,
    method: req.method,
    userId: req.user?.id,
  });

  // No filtrar errores internos al cliente
  if (err.isOperational) {
    return res.status(err.statusCode).json({
      error: err.message,
      code: err.code,
    });
  }

  // Error genérico para errores inesperados
  res.status(500).json({
    error: 'Error interno del servidor',
    code: 'INTERNAL_ERROR',
  });
}

app.use(errorHandler);

Configuración CORS

const cors = require('cors');

// Configuración CORS estricta
const corsOptions = {
  origin: (origin, callback) => {
    const allowedOrigins = [
      'https://miapp.com',
      'https://admin.miapp.com',
    ];

    // Permitir solicitudes sin origin (apps móviles, curl)
    if (!origin) return callback(null, true);

    if (allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('No permitido por CORS'));
    }
  },
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
  exposedHeaders: ['X-Total-Count', 'X-Page-Count'],
  maxAge: 86400, // 24 horas
};

app.use(cors(corsOptions));

Sanitización de Solicitudes

const mongoSanitize = require('express-mongo-sanitize');
const xss = require('xss-clean');

// Prevenir inyección NoSQL
app.use(mongoSanitize());

// Sanitizar entrada de usuario
app.use(xss());

// Sanitización personalizada
function sanitizeInput(obj) {
  if (typeof obj === 'string') {
    return obj.replace(/[<>]/g, '');
  }
  if (typeof obj === 'object' && obj !== null) {
    return Object.fromEntries(
      Object.entries(obj).map(([key, value]) => [key, sanitizeInput(value)])
    );
  }
  return obj;
}

app.use((req, res, next) => {
  req.body = sanitizeInput(req.body);
  req.query = sanitizeInput(req.query);
  next();
});

Lista de Verificación de Seguridad de API

  • Usa HTTPS para todos los endpoints
  • Implementa autenticación adecuada (claves de API, JWT, OAuth)
  • Aplica limitación de tasa por usuario y por endpoint
  • Valida toda entrada con esquemas estrictos
  • Retorna errores genéricos (no filtres internos)
  • Configura CORS correctamente
  • Sanitiza entrada para prevenir inyección
  • Registra todo acceso a API para auditoría
  • Usa versionado de API
  • Implementa firma de solicitudes para operaciones sensibles

Continuar Aprendiendo