Tokens JWT Explicados: Estructura, Seguridad y Mejores Prácticas

· 12 min de lectura

Los JSON Web Tokens (JWT) se han convertido en el estándar de facto para la autenticación sin estado en aplicaciones web modernas. Ya sea que estés construyendo una API REST, una arquitectura de microservicios o una aplicación de página única, entender cómo funcionan los JWT es esencial para implementar autenticación segura y escalable.

Esta guía completa desglosa todo lo que necesitas saber sobre los JWT—desde su estructura interna hasta prácticas de seguridad listas para producción. Al final, entenderás no solo cómo usar JWT, sino cuándo usarlos y cómo evitar errores comunes que conducen a vulnerabilidades de seguridad.

Tabla de Contenidos

¿Qué es un JWT?

Un JSON Web Token (JWT) es un medio compacto y seguro para URL de representar claims que se transferirán entre dos partes. Los claims en un JWT se codifican como un objeto JSON que está firmado digitalmente usando JSON Web Signature (JWS).

Piensa en un JWT como un contenedor a prueba de manipulaciones para información del usuario. Cuando un usuario inicia sesión, tu servidor crea un JWT que contiene su ID de usuario, roles y otros datos relevantes. Este token se envía luego al cliente, que lo incluye con cada solicitud subsiguiente. El servidor puede verificar la autenticidad del token sin consultar una base de datos, haciendo que los JWT sean ideales para arquitecturas sin estado y escalables.

Los JWT están definidos por RFC 7519 y son ampliamente soportados en lenguajes de programación y frameworks. Son particularmente populares en:

Consejo rápido: Usa nuestro Decodificador JWT para inspeccionar y depurar cualquier token JWT al instante. Es completamente del lado del cliente, por lo que tus tokens nunca salen de tu navegador.

Estructura JWT: Encabezado, Carga Útil y Firma

Un JWT consiste en tres partes codificadas en Base64URL separadas por puntos (.). Así es como se ve un JWT real:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Desglosando esto:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9           ← Encabezado
.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ  ← Carga Útil
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c    ← Firma
Parte Contiene Ejemplo (decodificado)
Encabezado Algoritmo + tipo de token {"alg": "HS256", "typ": "JWT"}
Carga Útil Claims (datos del usuario) {"sub": "1234567890", "name": "John Doe", "iat": 1516239022}
Firma Hash de verificación HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

El Encabezado

El encabezado típicamente consiste en dos partes: el tipo de token (typ) y el algoritmo de firma (alg). Los algoritmos comunes incluyen:

La Carga Útil

La carga útil contiene los claims—declaraciones sobre una entidad (típicamente el usuario) y metadatos adicionales. Los claims no están encriptados, solo codificados, así que nunca pongas información sensible como contraseñas en una carga útil JWT.

La Firma

La firma asegura que el token no ha sido manipulado. Se crea tomando el encabezado codificado, la carga útil codificada, una clave secreta, y aplicando el algoritmo especificado en el encabezado. Si alguien modifica el encabezado o la carga útil, la verificación de la firma fallará.

Consejo profesional: Genera JWT de prueba con claims personalizados usando nuestra herramienta Generador JWT. Perfecto para escenarios de desarrollo y pruebas.

Claims Estándar (RFC 7519)

RFC 7519 define varios claims estándar que proporcionan interoperabilidad entre diferentes implementaciones JWT. Aunque estos claims son opcionales, usarlos correctamente mejora la seguridad y compatibilidad.

Claim Nombre Propósito Ejemplo
iss Emisor Identifica quién creó el token "https://auth.example.com"
sub Sujeto Identifica sobre quién es el token (usualmente ID de usuario) "user_12345"
aud Audiencia Identifica para quién está destinado el token "https://api.example.com"
exp Expiración Cuándo expira el token (timestamp Unix) 1735689600
nbf No Antes De El token no es válido antes de este tiempo 1735603200
iat Emitido En Cuándo se creó el token 1735603200
jti ID JWT Identificador único para el token (útil para revocación) "a1b2c3d4-e5f6"

Claims Personalizados

Más allá de los claims estándar, puedes agregar claims personalizados específicos para tu aplicación. Ejemplos comunes incluyen:

Mantén tu carga útil pequeña—cada byte añade sobrecarga de red. Incluye solo los claims que necesitas para decisiones de autorización. Si necesitas datos adicionales del usuario, obtenlos de tu base de datos usando el claim sub como clave de búsqueda.

Cómo Funciona la Autenticación JWT

Entender el flujo completo de autenticación te ayuda a implementar JWT correctamente y de forma segura. Aquí está la secuencia típica:

  1. Inicio de Sesión del Usuario: El usuario envía credenciales (nombre de usuario + contraseña) a tu endpoint de autenticación
  2. Verificación de Credenciales: El servidor valida las credenciales contra tu base de datos de usuarios
  3. Generación de Token: El servidor crea un JWT que contiene claims del usuario y lo firma con una clave secreta
  4. Entrega de Token: El servidor devuelve el JWT al cliente (típicamente en el cuerpo de la respuesta o como una cookie httpOnly)
  5. Almacenamiento de Token: El cliente almacena el JWT de forma segura (más sobre esto en la sección Opciones de Almacenamiento)
  6. Solicitudes Autenticadas: El cliente incluye el JWT en el encabezado Authorization para solicitudes subsiguientes
  7. Verificación de Token: El servidor verifica la firma y comprueba la expiración—no se necesita búsqueda en base de datos
  8. Acceso Concedido: Si es válido, el servidor procesa la solicitud usando claims del token

Aquí hay una representación visual:

┌────────┐                                  ┌────────┐
│ Cliente│                                  │Servidor│
└───┬────┘                                  └───┬────┘
    │                                           │
    │  POST /login {username, password}         │
    │──────────────────────────────────────────>│
    │                                           │
    │                                    [Verificar credenciales]
    │                                    [Generar JWT]
    │                                           │
    │  200 OK {token: "eyJhbG..."}              │
    │<──────────────────────────────────────────│
    │                                           │
[Almacenar token]                               │
    │                                           │
    │  GET /api/profile                         │
    │  Authorization: Bearer eyJhbG...          │
    │──────────────────────────────────────────>│
    │                                           │
    │                                    [Verificar firma]
    │                                    [Comprobar expiración]
    │                                    [Extraer claims]
    │                                           │
    │  200 OK {datos de usuario}                │
    │<──────────────────────────────────────────│
    │                                           │

Consejo profesional: La belleza de los JWT es que el paso 7 (verificación de token) no requiere una consulta a la base de datos. El servidor puede verificar la autenticidad y extraer información del usuario directamente del token, haciendo tu API altamente escalable.

Ejemplos de Implementación

Veamos implementaciones prácticas en lenguajes de programación y frameworks populares.

Node.js con jsonwebtoken

const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');

// Endpoint de inicio de sesión
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  
  // Buscar usuario en base de datos
  const user = await User.findOne({ username });
  if (!user) {
    return res.status(401).json({ error: 'Credenciales inválidas' });
  }
  
  // Verificar contraseña
  const validPassword = await bcrypt.compare(password, user.passwordHash);
  if (!validPassword) {
    return res.status(401).json({ error: 'Credenciales inválidas' });
  }
  
  // Crear JWT
  const token = jwt.sign(
    { 
      sub: user.id,
      email: user.email,
      role: user.role 
    },
    process.env.JWT_SECRET,
    { 
      expiresIn: '15m',
      issuer: 'https://api.example.com',
      audience: 'https://example.com'
    }
  );
  
  res.json({ token });
});

// Middleware de ruta protegida
const 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: 'No se proporcionó token' });
  }
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    return res.status(403).json({ error: 'Token inválido o expirado' });
  }
};

// Usar el middleware
app.get('/api/profile', authenticateToken, (req, res) => {
  res.json({ userId: req.user.sub, email: req.user.email });
});

Python con PyJWT

import jwt
import datetime
from flask import Flask, request, jsonify

app = Flask(__name__)
SECRET_KEY = 'tu-clave-secreta'

@app.route('/login', methods=['POST'])
def login():
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')
    
    # Verificar credenciales (simplificado)
    user = verify_credentials(username, password)
    if not user:
        return jsonify({'error': 'Credenciales inválidas'}), 401
    
    # Crear JWT
    payload = {
        'sub': user['id'],
        'email': user['email'],
        'role': user['role'],
        'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=15),
        'iat': datetime.datetime.utcnow()
    }
    
    token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
    return jsonify({'token': token})

def token_required(f):
    def decorator(*args, **kwargs):
        token = request.headers.get('Authorization')
        
        if not token:
            return jsonify({'error': 'No se proporcionó token'}), 401
        
        try:
            # Remover prefijo 'Bearer '
            token = token.split(' ')[1]
            decoded = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
            request.user = decoded
        except jwt.ExpiredSignatureError:
            return jsonify({'error': 'Token expirado'}), 403
        except jwt.InvalidTokenError:
            return jsonify({'error': 'Token inválido'}), 403
        
        return f(*args, **kwargs)
    
    return decorator

@app.route('/api/profile')
@token_required
def profile():
    return jsonify({'userId': request.user['sub']})

Go con golang-jwt

package main

import (
    "time"
    "github.com/golang-jwt/jwt/v5"
)

type Claims struct {
    UserID string `json:"sub"`
    Email  string `json:"email"`
    Role   string `json:"role"`
    jwt.RegisteredClaims
}

func generateToken(userID, email, role string) (string, error) {
    claims := Claims{
        UserID: userID,
        Email:  emai