Autenticación JWT: Cómo Funcionan los JSON Web Tokens

· 12 min de lectura

Tabla de Contenidos

¿Qué es un JSON Web Token?

Un JSON Web Token (JWT) es una forma compacta y segura para URL de representar reclamaciones entre dos partes. Los JWTs se han convertido en el estándar de facto para autenticación y autorización en aplicaciones web modernas, APIs y arquitecturas de microservicios.

Cuando un usuario inicia sesión, el servidor genera un JWT que contiene información del usuario y lo envía al cliente. El cliente luego incluye este token en solicitudes posteriores para probar su identidad. Piénsalo como un pasaporte digital que contiene tus credenciales y puede ser verificado sin llamar de vuelta a la autoridad emisora cada vez.

A diferencia de la autenticación tradicional basada en sesiones donde el servidor mantiene el estado de la sesión en memoria o una base de datos, los JWTs son sin estado. Toda la información necesaria para verificar el token está contenida dentro del token mismo. Esta decisión arquitectónica trae varias ventajas:

Puedes inspeccionar y decodificar cualquier JWT usando nuestra herramienta de Decodificador JWT para ver su contenido sin necesidad de escribir código. Esto es particularmente útil al depurar problemas de autenticación o entender qué datos contienen tus tokens.

Consejo profesional: Los JWTs están firmados, no cifrados por defecto. Cualquiera puede decodificar y leer el contenido de un JWT. Nunca almacenes información sensible como contraseñas, números de tarjetas de crédito o números de seguridad social en las cargas útiles de JWT.

Estructura de JWT Explicada

Un JWT consiste en tres partes distintas separadas por puntos (.): el Encabezado, la Carga Útil y la Firma. Así es como se ve un JWT real:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZW1haWwiOiJqb2huQGV4YW1wbGUuY29tIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDI2MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Desglosemos cada componente para entender cómo trabajan juntos para crear un token seguro y verificable.

1. Encabezado

El encabezado típicamente contiene dos campos críticos que definen cómo debe procesarse el token:

{
  "alg": "HS256",
  "typ": "JWT"
}

Este objeto JSON luego se codifica en Base64Url para formar la primera parte del JWT. La codificación lo hace seguro para URL y compacto para transmisión en encabezados HTTP.

2. Carga Útil

La carga útil contiene reclamaciones — declaraciones sobre el usuario y metadatos adicionales. Aquí es donde almacenas los datos reales que deseas transmitir:

{
  "sub": "1234567890",
  "name": "John Doe",
  "email": "[email protected]",
  "role": "admin",
  "iat": 1516239022,
  "exp": 1516242622
}

Las reclamaciones se dividen en tres categorías:

Reclamaciones registradas (estandarizadas y recomendadas):

Reclamaciones públicas son reclamaciones personalizadas que deben definirse en el Registro IANA de JSON Web Token o usar nombres resistentes a colisiones (como URLs).

Reclamaciones privadas son reclamaciones personalizadas acordadas entre las partes, como role, permissions o department.

3. Firma

La firma es lo que hace que los JWTs sean seguros y a prueba de manipulaciones. Se crea tomando el encabezado codificado, la carga útil codificada, una clave secreta y aplicando el algoritmo especificado en el encabezado:

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

La firma cumple dos propósitos críticos:

  1. Verificación de integridad: Asegura que el token no ha sido modificado desde que fue emitido
  2. Autenticación: Prueba que el token fue creado por alguien con acceso a la clave secreta

Si alguien intenta modificar el encabezado o la carga útil, la verificación de la firma fallará y el token será rechazado.

Cómo Funciona la Autenticación JWT

Entender el flujo completo de autenticación es esencial para implementar JWTs correctamente. Aquí está el proceso paso a paso:

Paso 1: Inicio de Sesión del Usuario

El usuario envía sus credenciales (nombre de usuario y contraseña) al endpoint de autenticación. El servidor valida estas credenciales contra la base de datos.

Paso 2: Generación del Token

Si las credenciales son válidas, el servidor genera un JWT que contiene información del usuario y reclamaciones. El servidor firma el token usando una clave secreta (para algoritmos simétricos) o una clave privada (para algoritmos asimétricos).

Paso 3: Entrega del Token

El servidor envía el JWT de vuelta al cliente, típicamente en el cuerpo de la respuesta. El cliente almacena este token para solicitudes futuras.

Paso 4: Solicitudes Autenticadas

Para solicitudes posteriores, el cliente incluye el JWT en el encabezado Authorization usando el esquema Bearer:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Paso 5: Verificación del Token

El servidor recibe la solicitud, extrae el JWT del encabezado y verifica la firma usando la clave secreta o pública. Si es válido, el servidor extrae la información del usuario de la carga útil y procesa la solicitud.

Paso 6: Expiración del Token

Cuando el token expira, el cliente debe obtener un nuevo token, ya sea reautenticándose o usando un token de actualización (cubierto en la siguiente sección).

Consejo rápido: Siempre valida la reclamación exp en el lado del servidor, incluso si verificas la expiración en el cliente. Las verificaciones del lado del cliente pueden ser evadidas, pero la validación del lado del servidor es autoritativa.

Tokens de Acceso vs Tokens de Actualización

Un sistema robusto de autenticación JWT típicamente usa dos tipos de tokens trabajando juntos: tokens de acceso y tokens de actualización. Entender la distinción es crucial para construir aplicaciones seguras.

Tokens de Acceso

Los tokens de acceso son JWTs de corta duración (típicamente 15 minutos a 1 hora) que otorgan acceso a recursos protegidos. Contienen identidad del usuario y permisos necesarios para autorizar solicitudes.

Características:

Tokens de Actualización

Los tokens de actualización son tokens de larga duración (días a meses) usados exclusivamente para obtener nuevos tokens de acceso. Se almacenan de forma segura y solo se envían al endpoint de actualización de tokens.

Características:

¿Por Qué Usar Ambos?

Este enfoque de doble token equilibra seguridad y experiencia del usuario:

Aspecto Solo Token de Acceso Tokens de Acceso + Actualización
Seguridad Menor (tokens de larga duración en riesgo) Mayor (tokens de acceso de corta duración)
Experiencia del Usuario Mejor (sin reautenticación) Mejor (actualización de token sin interrupciones)
Revocación Difícil (sin estado) Posible (tokens de actualización en BD)
Sobrecarga de Red Menor Ligeramente mayor (solicitudes de actualización)
Complejidad de Implementación Simple Moderada

Flujo de Actualización de Token

  1. El cliente detecta que el token de acceso ha expirado o está por expirar
  2. El cliente envía el token de actualización al endpoint /auth/refresh
  3. El servidor valida el token de actualización contra la base de datos
  4. El servidor genera un nuevo token de acceso (y opcionalmente un nuevo token de actualización)
  5. El cliente recibe y almacena los nuevos tokens
  6. El cliente reintenta la solicitud original con el nuevo token de acceso

Consejo profesional: Implementa rotación de tokens de actualización — emite un nuevo token de actualización cada vez que se use uno e invalida el antiguo. Esto limita el daño si un token de actualización se ve comprometido.

Guía de Implementación en Node.js

Construyamos un sistema completo de autenticación JWT usando Node.js, Express y la biblioteca jsonwebtoken. Esta implementación incluye tanto tokens de acceso como de actualización.

Instalación

npm install express jsonwebtoken bcrypt dotenv

Configuración del Entorno

Crea un archivo .env con tus claves secretas:

ACCESS_TOKEN_SECRET=tu-clave-de-acceso-super-secreta-cambia-esto
REFRESH_TOKEN_SECRET=tu-clave-de-actualizacion-super-secreta-cambia-esto
ACCESS_TOKEN_EXPIRY=15m
REFRESH_TOKEN_EXPIRY=7d

Generación de Tokens

const jwt = require('jsonwebtoken');

function generateAccessToken(user) {
  return jwt.sign(
    { 
      userId: user.id,
      email: user.email,
      role: user.role 
    },
    process.env.ACCESS_TOKEN_SECRET,
    { expiresIn: process.env.ACCESS_TOKEN_EXPIRY }
  );
}

function generateRefreshToken(user) {
  return jwt.sign(
    { userId: user.id },
    process.env.REFRESH_TOKEN_SECRET,
    { expiresIn: process.env.REFRESH_TOKEN_EXPIRY }
  );
}

Endpoint de Inicio de Sesión

const bcrypt = require('bcrypt');

app.post('/auth/login', async (req, res) => {
  try {
    const { email, password } = req.body;
    
    // Buscar usuario en la base de datos
    const user = await User.findOne({ email });
    if (!user) {
      return res.status(401).json({ error: 'Credenciales inválidas' });
    }
    
    // Verificar contraseña
    const validPassword = await bcrypt.compare(password, user.password);
    if (!validPassword) {
      return res.status(401).json({ error: 'Credenciales inválidas' });
    }
    
    // Generar tokens
    const accessToken = generateAccessToken(user);
    const refreshToken = generateRefreshToken(user);
    
    // Almacenar token de actualización en la base de datos
    await RefreshToken.create({
      token: refreshToken,
      userId: user.id,
      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
    });
    
    res.json({
      accessToken,
      refreshToken,
      user: {
        id: user.id,
        email: user.email,
        name: user.name
      }
    });
  } catch (error) {
    res.status(500).json({ error: 'Error del servidor' });
  }
});

Middleware de Autenticación

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 de acceso requerido' });
  }
  
  jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
    if (err) {
      return res.status(403).json({ error: 'Token inválido o expirado' });
    }
    
    req.user = user;
    next();
  });
}

// Uso
app.get('/api/protected', authenticateToken, (req, res) => {
  res.json({ 
    message: 'Datos protegidos',
    user: req.user 
  });
});

Endpoint de Actualización de Token

app.post('/auth/refresh', async (req, res) => {
  const { refreshToken } = req.body;
  
  if (!refreshToken) {
    return res.status(401).json({ error: 'Token de actualización requerido' });
  }
  
  try {
    // Verificar que el token de actualización existe en la base de datos
    const storedToken = await RefreshToken.findOne({ token: refreshToken });
    if (!storedToken) {
      return res.status(403).json({ error: 'Token de actualización inválido' });
    }
    
    // Verificar firma del token
    jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, async (err, user) => {
      if (err) {
        return res.status(403).json({ error: 'Token de actualización inválido' });
      }
      
      // Obtener datos del usuario
      const userData = await User.findById(user.userId);
      
      // Generar nuevo token de acceso
      const accessToken = generateAccessToken(userData);
      
      res.json({ accessToken });