Autenticación JWT: Guía completa para desarrolladores web (2026)

Publicado en marzo 2026 • 10 min de lectura

¿Qué es la autenticación JWT?

JSON Web Token (JWT, pronunciado "yot") es un estándar compacto y autocontenido para transmitir información de autenticación y autorización entre partes como un token firmado digitalmente. Una vez que un usuario inicia sesión, el servidor emite un JWT que contiene claims sobre ese usuario. El cliente almacena el token y lo envía con cada solicitud subsiguiente. El servidor verifica la firma del token para confirmar la autenticidad — sin necesidad de consultar la base de datos.

Los JWTs están definidos por RFC 7519 y son el mecanismo de autenticación dominante para APIs REST, aplicaciones de página única y arquitecturas de microservicios. Son stateless por diseño: el servidor no necesita almacenar datos de sesión, lo que hace que el escalado horizontal sea sencillo.

Cómo funciona JWT

El flujo completo de autenticación JWT funciona así:

  1. Login: El usuario envía sus credenciales (usuario + contraseña) al endpoint de login del servidor.
  2. Emisión del token: El servidor valida las credenciales contra la base de datos. Si son correctas, firma un JWT que contiene el ID del usuario, roles y una marca de tiempo de expiración, y luego devuelve el token al cliente.
  3. Almacenamiento del token: El cliente almacena el token (en memoria, localStorage o una cookie httpOnly — más sobre esto después).
  4. Solicitudes autenticadas: Para cada solicitud API subsiguiente, el cliente incluye el JWT en el encabezado Authorization: Authorization: Bearer <token>.
  5. Verificación: El servidor extrae el token del encabezado, verifica la firma usando su clave secreta, comprueba la expiración y lee los claims — todo sin una consulta a la base de datos.
  6. Respuesta: Si el token es válido, el servidor procesa la solicitud. Si no (expirado, manipulado, ausente), devuelve un 401 Unauthorized.

Estructura de un JWT explicada

Un JWT consiste en tres partes codificadas en Base64URL separadas por puntos:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjM0NTYiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3MDkzMDAwMDAsImV4cCI6MTcwOTMwMzYwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Las tres partes son:

1. Header

{
  "alg": "HS256",   // Algoritmo de firma (HS256, RS256, ES256)
  "typ": "JWT"      // Tipo de token
}

2. Payload (Claims)

{
  "userId": "123456",
  "email": "user@example.com",
  "role": "admin",
  "iat": 1709300000,   // Emitido en (timestamp Unix)
  "exp": 1709303600    // Expiración (1 hora desde la emisión)
}

Nombres de claims estándar: iss (emisor), sub (sujeto), aud (audiencia), exp (expiración), iat (emitido en), jti (ID de JWT para revocación). El payload está codificado en Base64URL, no cifrado — cualquiera puede decodificarlo. Nunca almacenes contraseñas, números de tarjetas de crédito u otros datos sensibles en el payload.

3. Firma

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secretKey
)

La firma prueba que el token fue emitido por tu servidor y no ha sido manipulado. Si cualquier carácter en el header o payload cambia, la verificación de la firma falla.

Implementando JWT en Node.js

Instala el paquete jsonwebtoken:

npm install jsonwebtoken bcryptjs express

Crea un ejemplo completo de login y ruta protegida:

// auth.js
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const express = require('express');
const router = express.Router();

const JWT_SECRET = process.env.JWT_SECRET; // Debe ser una cadena larga y aleatoria
const JWT_EXPIRES_IN = '1h';               // Mantener los access tokens de corta duración

// Ruta de login — emitir un JWT en autenticación exitosa
router.post('/login', async (req, res) => {
  const { email, password } = req.body;

  // 1. Buscar el usuario en tu base de datos
  const user = await User.findOne({ email });
  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  // 2. Comparar contraseña con el hash almacenado
  const validPassword = await bcrypt.compare(password, user.passwordHash);
  if (!validPassword) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  // 3. Firmar el JWT — incluir solo claims no sensibles
  const token = jwt.sign(
    { userId: user._id, email: user.email, role: user.role },
    JWT_SECRET,
    { expiresIn: JWT_EXPIRES_IN, algorithm: 'HS256' }
  );

  res.json({ token, expiresIn: 3600 });
});

// Middleware para verificar JWT en rutas protegidas
function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // "Bearer TOKEN"

  if (!token) {
    return res.status(401).json({ error: 'Access token required' });
  }

  try {
    // Especificar explícitamente el algoritmo para prevenir ataques de confusión de algoritmo
    const decoded = jwt.verify(token, JWT_SECRET, { algorithms: ['HS256'] });
    req.user = decoded;
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired' });
    }
    return res.status(403).json({ error: 'Invalid token' });
  }
}

// Ejemplo de ruta protegida
router.get('/profile', authenticateToken, (req, res) => {
  res.json({ userId: req.user.userId, email: req.user.email });
});

module.exports = { router, authenticateToken };

Almacenar JWT en React

Dónde almacenes el JWT en el navegador afecta significativamente la postura de seguridad de tu aplicación. Las dos opciones principales son localStorage y cookies httpOnly:

Propiedad localStorage Cookie httpOnly
Vulnerabilidad XSS Alta — cualquier script puede leerlo Baja — inaccesible para JavaScript
Vulnerabilidad CSRF Ninguna — mitigar con SameSite + token CSRF
Se envía automáticamente No — debe agregarse al encabezado manualmente Sí — el navegador lo envía con cada solicitud
Accesible desde JS No (flag httpOnly)
Recomendado para Apps de bajo riesgo, datos públicos Apps en producción con datos de usuario

Para aplicaciones en producción, prefiere cookies httpOnly. Tu servidor configura la cookie en el login y el navegador la envía automáticamente, sin que JavaScript toque nunca el valor del token.

Si usas localStorage (por ejemplo, por simplicidad durante el desarrollo), almacena el token en memoria cuando sea posible e implementa una Content Security Policy robusta para mitigar el riesgo de XSS:

// React — enviar JWT desde localStorage
async function fetchProfile() {
  const token = localStorage.getItem('authToken');

  const response = await fetch('/api/profile', {
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
  });

  if (response.status === 401) {
    // Token expirado — redirigir al login o refrescar
    localStorage.removeItem('authToken');
    window.location.href = '/login';
  }

  return response.json();
}

Estrategia de Refresh Token

Los access tokens de corta duración (15 minutos a 1 hora) minimizan la ventana de robo de tokens, pero forzar a los usuarios a iniciar sesión cada hora es una experiencia terrible. La solución es un par de refresh tokens:

  • Access token: De corta duración (15 min – 1 hora), enviado con cada solicitud API en el encabezado Authorization. Almacenado en memoria o localStorage.
  • Refresh token: De larga duración (7 – 30 días), usado solo para obtener nuevos access tokens. Siempre almacenado en una cookie httpOnly, Secure, SameSite=Strict. Debe rotarse en cada uso.
// Servidor: emitir ambos tokens en el login
router.post('/login', async (req, res) => {
  // ... validar credenciales ...

  const accessToken = jwt.sign(
    { userId: user._id, role: user.role },
    process.env.JWT_ACCESS_SECRET,
    { expiresIn: '15m' }
  );

  const refreshToken = jwt.sign(
    { userId: user._id },
    process.env.JWT_REFRESH_SECRET,
    { expiresIn: '30d' }
  );

  // Almacenar hash del refresh token en base de datos para soporte de revocación
  await RefreshToken.create({ userId: user._id, token: hashToken(refreshToken) });

  // Enviar refresh token como cookie httpOnly
  res.cookie('refreshToken', refreshToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: 30 * 24 * 60 * 60 * 1000, // 30 días en ms
  });

  res.json({ accessToken });
});

// Servidor: endpoint de refresh
router.post('/refresh', async (req, res) => {
  const refreshToken = req.cookies.refreshToken;
  if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });

  try {
    const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
    // Opcional: verificar que el hash del token existe en la BD (permite revocación)

    const newAccessToken = jwt.sign(
      { userId: decoded.userId },
      process.env.JWT_ACCESS_SECRET,
      { expiresIn: '15m' }
    );

    res.json({ accessToken: newAccessToken });
  } catch {
    res.status(403).json({ error: 'Invalid refresh token' });
  }
});

Mejores prácticas de seguridad JWT

  • Mantén los access tokens de corta duración: 15 minutos a 1 hora. Los tokens robados se vuelven inútiles después de expirar.
  • Usa secretos fuertes: Tu JWT_SECRET debe tener al menos 256 bits de datos aleatorios. Genéralo con openssl rand -hex 32. Nunca lo hardcodees.
  • Siempre especifica el algoritmo: Pasa { algorithms: ['HS256'] } a jwt.verify(). Esto previene el ataque de confusión de algoritmo (none) donde un atacante establece "alg": "none" en el header.
  • Solo HTTPS en producción: Los JWTs en tránsito son tan seguros como tu capa de transporte. Siempre usa TLS en producción.
  • Almacena refresh tokens en cookies httpOnly: No en localStorage ni sessionStorage.
  • Implementa revocación de tokens: Para logout y eventos de seguridad, mantén una lista de bloqueo de corta duración (Redis funciona bien) o usa refresh tokens rotativos con seguimiento en base de datos.
  • Valida todos los claims: Verifica exp (expiración), iss (emisor) y aud (audiencia) en cada solicitud. La librería jsonwebtoken lo hace automáticamente cuando pasas las opciones correctas.

Errores comunes con JWT

Almacenar JWTs en localStorage en apps de alto riesgo

Cualquier vulnerabilidad XSS — incluso un script de terceros que hayas cargado — puede leer localStorage y exfiltrar todos los tokens. Para aplicaciones que manejan pagos, datos personales o acciones de administrador, usa cookies httpOnly exclusivamente.

Sin expiración en los tokens

Los tokens sin un claim exp son válidos para siempre. Un token filtrado se convierte en una puerta trasera permanente. Siempre configura expiresIn al firmar.

El ataque de algoritmo none

Las primeras librerías JWT aceptaban "alg": "none" en el header, efectivamente aceptando tokens sin firmar como válidos. Siempre pasa la opción algorithms a jwt.verify() para restringir qué algoritmos son aceptables:

// Siempre haz esto — nunca permitas el algoritmo "none"
jwt.verify(token, secret, { algorithms: ['HS256'] });

Datos sensibles en el payload

El payload del JWT está codificado en Base64URL, no cifrado. Cualquier parte con el token puede decodificar y leer el payload sin conocer el secreto. Nunca incluyas contraseñas, números de seguro social, números de tarjetas de crédito o detalles internos del sistema en los claims JWT.

Usar el mismo secreto para access y refresh tokens

Si usas el mismo secreto para ambos tipos de token, un access token comprometido podría potencialmente usarse como refresh token. Usa secretos separados para cada tipo de token almacenados en variables de entorno separadas.

Decodifica tus tokens JWT

¿Necesitas inspeccionar un JWT para verificar sus claims, expiración o estructura? Pégalo en el Decodificador JWT de devbit.dev para decodificar instantáneamente el header y payload, verificar el tiempo de expiración y validar el formato del token — todo en tu navegador sin enviar datos a ningún servidor.

Decodifica e inspecciona cualquier JWT al instante

Pega un token JWT para decodificar su header, payload y claims. Verifica expiración, algoritmo y estructura. 100% del lado del cliente — tu token nunca sale de tu navegador.

Abrir decodificador JWT →

Herramientas de desarrollo relacionadas