Express

Express

Tutorial Express: Middleware de autenticación

Aprende a implementar middleware de autenticación en Express para proteger rutas y verificar tokens JWT de forma segura y eficiente.

Aprende Express y certifícate

Protección de rutas

La protección de rutas es un mecanismo fundamental que permite controlar el acceso a recursos específicos de nuestra aplicación Express. Una vez que los usuarios se han autenticado y recibido su token JWT, necesitamos implementar un sistema que verifique automáticamente estos tokens antes de permitir el acceso a rutas sensibles.

En Express, la forma más elegante de implementar esta funcionalidad es mediante middleware personalizado. Este middleware actúa como una capa intermedia que intercepta las peticiones antes de que lleguen a nuestros controladores, evaluando si el usuario tiene los permisos necesarios para acceder al recurso solicitado.

Implementación básica de middleware de protección

El middleware de protección debe ubicarse estratégicamente en el flujo de la petición. Podemos implementarlo de varias formas según nuestras necesidades:

// middleware/auth.js
const jwt = require('jsonwebtoken');

const protegerRuta = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ 
      error: 'Token de acceso requerido' 
    });
  }
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.usuario = decoded;
    next();
  } catch (error) {
    return res.status(401).json({ 
      error: 'Token inválido' 
    });
  }
};

module.exports = { protegerRuta };

Este middleware básico extrae el token del header Authorization, lo valida y, si es correcto, permite que la petición continúe hacia el siguiente middleware o controlador. La información del usuario decodificada se almacena en req.usuario para su uso posterior.

Aplicación del middleware a rutas específicas

Una vez definido nuestro middleware, podemos aplicarlo a rutas individuales o grupos de rutas según nuestras necesidades de seguridad:

// routes/usuarios.js
const express = require('express');
const { protegerRuta } = require('../middleware/auth');
const router = express.Router();

// Ruta pública - no requiere autenticación
router.get('/publico', (req, res) => {
  res.json({ mensaje: 'Contenido público' });
});

// Ruta protegida - requiere autenticación
router.get('/perfil', protegerRuta, (req, res) => {
  res.json({ 
    usuario: req.usuario,
    mensaje: 'Datos del perfil del usuario'
  });
});

// Múltiples rutas protegidas
router.get('/configuracion', protegerRuta, (req, res) => {
  res.json({ configuracion: 'datos sensibles' });
});

router.put('/actualizar', protegerRuta, (req, res) => {
  // Lógica de actualización usando req.usuario
  res.json({ mensaje: 'Perfil actualizado' });
});

module.exports = router;

Protección de grupos de rutas

Para aplicaciones con múltiples rutas que requieren autenticación, es más eficiente proteger grupos completos de rutas utilizando router.use():

// routes/admin.js
const express = require('express');
const { protegerRuta } = require('../middleware/auth');
const router = express.Router();

// Aplicar protección a todas las rutas de este router
router.use(protegerRuta);

// Todas estas rutas están automáticamente protegidas
router.get('/dashboard', (req, res) => {
  res.json({ 
    usuario: req.usuario.email,
    dashboard: 'datos del panel de administración'
  });
});

router.get('/usuarios', (req, res) => {
  res.json({ usuarios: 'lista de usuarios' });
});

router.delete('/usuario/:id', (req, res) => {
  res.json({ mensaje: 'Usuario eliminado' });
});

module.exports = router;

Middleware con niveles de autorización

Podemos crear middleware más sofisticado que no solo verifique la autenticación, sino también los niveles de autorización del usuario:

// middleware/auth.js
const verificarRol = (rolesPermitidos) => {
  return (req, res, next) => {
    const token = req.headers.authorization?.split(' ')[1];
    
    if (!token) {
      return res.status(401).json({ error: 'Token requerido' });
    }
    
    try {
      const decoded = jwt.verify(token, process.env.JWT_SECRET);
      
      // Verificar si el usuario tiene el rol necesario
      if (!rolesPermitidos.includes(decoded.rol)) {
        return res.status(403).json({ 
          error: 'Permisos insuficientes' 
        });
      }
      
      req.usuario = decoded;
      next();
    } catch (error) {
      return res.status(401).json({ error: 'Token inválido' });
    }
  };
};

module.exports = { protegerRuta, verificarRol };

Este middleware avanzado permite definir qué roles pueden acceder a rutas específicas:

// routes/admin.js
const { verificarRol } = require('../middleware/auth');

// Solo administradores pueden acceder
router.delete('/usuario/:id', 
  verificarRol(['admin']), 
  (req, res) => {
    res.json({ mensaje: 'Usuario eliminado' });
  }
);

// Administradores y moderadores pueden acceder
router.get('/reportes', 
  verificarRol(['admin', 'moderador']), 
  (req, res) => {
    res.json({ reportes: 'datos de reportes' });
  }
);

Manejo de errores en middleware de protección

Es importante implementar un manejo robusto de errores en nuestro middleware de protección para proporcionar respuestas claras y seguras:

const protegerRutaAvanzado = (req, res, next) => {
  const authHeader = req.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({
      error: 'Token de autorización requerido',
      codigo: 'TOKEN_MISSING'
    });
  }
  
  const token = authHeader.split(' ')[1];
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    
    // Verificar si el token ha expirado
    if (decoded.exp < Date.now() / 1000) {
      return res.status(401).json({
        error: 'Token expirado',
        codigo: 'TOKEN_EXPIRED'
      });
    }
    
    req.usuario = decoded;
    next();
  } catch (error) {
    if (error.name === 'JsonWebTokenError') {
      return res.status(401).json({
        error: 'Token malformado',
        codigo: 'TOKEN_MALFORMED'
      });
    }
    
    return res.status(500).json({
      error: 'Error interno del servidor',
      codigo: 'INTERNAL_ERROR'
    });
  }
};

Esta implementación proporciona códigos de error específicos que permiten al cliente frontend manejar diferentes situaciones de forma apropiada, como redirigir al login cuando el token ha expirado o mostrar mensajes de error específicos.

Verificación de tokens

La verificación de tokens constituye el núcleo técnico del proceso de autenticación en Express. Mientras que la protección de rutas se enfoca en la aplicación del middleware, la verificación se centra en los mecanismos internos que validan la autenticidad e integridad de los tokens JWT recibidos.

Anatomía de la verificación JWT

Un token JWT válido debe pasar por múltiples capas de validación antes de considerarse auténtico. El proceso de verificación no solo confirma que el token es válido, sino que también extrae y valida la información contenida en su payload:

// utils/tokenVerifier.js
const jwt = require('jsonwebtoken');

const verificarToken = (token) => {
  try {
    // Verificación de firma y estructura
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    
    // Validación de campos obligatorios
    if (!decoded.id || !decoded.email) {
      throw new Error('Token incompleto');
    }
    
    // Verificación de expiración manual adicional
    const ahora = Math.floor(Date.now() / 1000);
    if (decoded.exp && decoded.exp < ahora) {
      throw new Error('Token expirado');
    }
    
    return {
      valido: true,
      usuario: decoded,
      error: null
    };
  } catch (error) {
    return {
      valido: false,
      usuario: null,
      error: error.message
    };
  }
};

module.exports = { verificarToken };

Validación de estructura y formato

Antes de proceder con la verificación criptográfica, es fundamental validar que el token recibido tiene la estructura correcta. Los tokens JWT malformados pueden causar errores inesperados en la aplicación:

// utils/tokenValidator.js
const validarFormatoToken = (authHeader) => {
  // Verificar presencia del header
  if (!authHeader) {
    return {
      valido: false,
      error: 'Header Authorization ausente',
      codigo: 'HEADER_MISSING'
    };
  }
  
  // Verificar formato Bearer
  const partes = authHeader.split(' ');
  if (partes.length !== 2 || partes[0] !== 'Bearer') {
    return {
      valido: false,
      error: 'Formato de token inválido',
      codigo: 'FORMAT_INVALID'
    };
  }
  
  const token = partes[1];
  
  // Verificar estructura JWT básica (3 partes separadas por puntos)
  const partesJWT = token.split('.');
  if (partesJWT.length !== 3) {
    return {
      valido: false,
      error: 'Estructura JWT inválida',
      codigo: 'JWT_MALFORMED'
    };
  }
  
  return {
    valido: true,
    token: token,
    error: null
  };
};

module.exports = { validarFormatoToken };

Verificación avanzada con validaciones personalizadas

Para aplicaciones que requieren validaciones específicas, podemos implementar verificadores que evalúen condiciones adicionales más allá de la validez básica del token:

// middleware/advancedAuth.js
const jwt = require('jsonwebtoken');

const verificarTokenAvanzado = async (req, res, next) => {
  const authHeader = req.headers.authorization;
  
  // Validación de formato
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({
      error: 'Token requerido en formato Bearer',
      codigo: 'INVALID_FORMAT'
    });
  }
  
  const token = authHeader.split(' ')[1];
  
  try {
    // Verificación criptográfica
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    
    // Validaciones de negocio específicas
    const validaciones = await ejecutarValidacionesPersonalizadas(decoded);
    
    if (!validaciones.valido) {
      return res.status(401).json({
        error: validaciones.razon,
        codigo: validaciones.codigo
      });
    }
    
    // Enriquecer request con información adicional
    req.usuario = {
      ...decoded,
      permisos: validaciones.permisos,
      sesionActiva: validaciones.sesionActiva
    };
    
    next();
  } catch (error) {
    return manejarErrorVerificacion(error, res);
  }
};

const ejecutarValidacionesPersonalizadas = async (decoded) => {
  // Verificar si el usuario sigue activo en la base de datos
  const usuarioActivo = await verificarUsuarioActivo(decoded.id);
  if (!usuarioActivo) {
    return {
      valido: false,
      razon: 'Usuario desactivado',
      codigo: 'USER_INACTIVE'
    };
  }
  
  // Verificar límite de sesiones concurrentes
  const sesionesActivas = await contarSesionesActivas(decoded.id);
  if (sesionesActivas > 3) {
    return {
      valido: false,
      razon: 'Límite de sesiones excedido',
      codigo: 'SESSION_LIMIT'
    };
  }
  
  return {
    valido: true,
    permisos: usuarioActivo.permisos,
    sesionActiva: true
  };
};

Manejo especializado de errores de verificación

Diferentes tipos de errores de verificación requieren respuestas específicas. Un manejo granular permite al cliente frontend reaccionar apropiadamente a cada situación:

const manejarErrorVerificacion = (error, res) => {
  const erroresJWT = {
    'TokenExpiredError': {
      status: 401,
      mensaje: 'Token expirado',
      codigo: 'TOKEN_EXPIRED',
      accion: 'REFRESH_TOKEN'
    },
    'JsonWebTokenError': {
      status: 401,
      mensaje: 'Token inválido',
      codigo: 'TOKEN_INVALID',
      accion: 'LOGIN_REQUIRED'
    },
    'NotBeforeError': {
      status: 401,
      mensaje: 'Token no válido aún',
      codigo: 'TOKEN_NOT_ACTIVE',
      accion: 'WAIT_OR_LOGIN'
    }
  };
  
  const errorInfo = erroresJWT[error.name] || {
    status: 500,
    mensaje: 'Error de verificación',
    codigo: 'VERIFICATION_ERROR',
    accion: 'RETRY_OR_CONTACT_SUPPORT'
  };
  
  return res.status(errorInfo.status).json({
    error: errorInfo.mensaje,
    codigo: errorInfo.codigo,
    accion: errorInfo.accion,
    timestamp: new Date().toISOString()
  });
};

Verificación con refresh tokens

Para aplicaciones que implementan refresh tokens, la verificación debe manejar tanto tokens de acceso como tokens de renovación:

// middleware/refreshAuth.js
const verificarRefreshToken = async (req, res, next) => {
  const { refreshToken } = req.body;
  
  if (!refreshToken) {
    return res.status(401).json({
      error: 'Refresh token requerido',
      codigo: 'REFRESH_TOKEN_MISSING'
    });
  }
  
  try {
    // Verificar refresh token
    const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
    
    // Validar que sea específicamente un refresh token
    if (decoded.tipo !== 'refresh') {
      return res.status(401).json({
        error: 'Token de tipo incorrecto',
        codigo: 'WRONG_TOKEN_TYPE'
      });
    }
    
    // Verificar que el refresh token esté en la lista de tokens válidos
    const tokenValido = await verificarRefreshTokenEnBD(refreshToken, decoded.id);
    
    if (!tokenValido) {
      return res.status(401).json({
        error: 'Refresh token revocado',
        codigo: 'REFRESH_TOKEN_REVOKED'
      });
    }
    
    req.usuario = decoded;
    next();
  } catch (error) {
    return res.status(401).json({
      error: 'Refresh token inválido',
      codigo: 'REFRESH_TOKEN_INVALID'
    });
  }
};

module.exports = { verificarRefreshToken };

Optimización de la verificación

Para aplicaciones con alto volumen de peticiones, podemos implementar técnicas de optimización que mejoren el rendimiento de la verificación sin comprometer la seguridad:

// utils/optimizedVerifier.js
const NodeCache = require('node-cache');

// Cache para tokens verificados (TTL corto por seguridad)
const tokenCache = new NodeCache({ stdTTL: 300 }); // 5 minutos

const verificarTokenOptimizado = (token) => {
  // Verificar cache primero
  const cacheKey = `token_${token.substring(0, 20)}`;
  const cached = tokenCache.get(cacheKey);
  
  if (cached) {
    // Verificar que el token cacheado no haya expirado
    if (cached.exp > Math.floor(Date.now() / 1000)) {
      return {
        valido: true,
        usuario: cached,
        fromCache: true
      };
    } else {
      tokenCache.del(cacheKey);
    }
  }
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    
    // Cachear solo si el token tiene tiempo de vida suficiente
    const tiempoRestante = decoded.exp - Math.floor(Date.now() / 1000);
    if (tiempoRestante > 60) { // Solo cachear si quedan más de 1 minuto
      tokenCache.set(cacheKey, decoded, Math.min(tiempoRestante, 300));
    }
    
    return {
      valido: true,
      usuario: decoded,
      fromCache: false
    };
  } catch (error) {
    return {
      valido: false,
      error: error.message,
      fromCache: false
    };
  }
};

module.exports = { verificarTokenOptimizado };

Esta implementación optimizada utiliza un sistema de cache que reduce la carga computacional de verificar repetidamente los mismos tokens, manteniendo un equilibrio entre rendimiento y seguridad mediante TTL apropiados.

Aprende Express online

Otras lecciones de Express

Accede a todas las lecciones de Express y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a Express y certifícate

Ejercicios de programación de Express

Evalúa tus conocimientos de esta lección Middleware de autenticación con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.