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?
- Estructura JWT: Encabezado, Carga Útil y Firma
- Claims Estándar (RFC 7519)
- Cómo Funciona la Autenticación JWT
- Ejemplos de Implementación
- Mejores Prácticas de Seguridad
- Tokens de Actualización y Rotación de Tokens
- JWT vs Autenticación Basada en Sesiones
- Errores Comunes a Evitar
- Dónde Almacenar los JWT
- Preguntas Frecuentes
- Artículos Relacionados
¿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:
- Sistemas de Single Sign-On (SSO) donde los usuarios se autentican una vez y acceden a múltiples servicios
- Arquitecturas de microservicios donde los servicios necesitan verificar la identidad del usuario sin almacenamiento de sesión compartido
- Aplicaciones móviles donde mantener sesiones del lado del servidor es poco práctico
- Autenticación de API donde la verificación sin estado mejora la escalabilidad
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:
- HS256 (HMAC con SHA-256): Algoritmo simétrico usando un secreto compartido
- RS256 (RSA con SHA-256): Algoritmo asimétrico usando pares de claves pública/privada
- ES256 (ECDSA con SHA-256): Algoritmo asimétrico con tamaños de clave más pequeños
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:
roleoroles: Permisos del usuario (ej.,"admin","user")email: Dirección de correo electrónico del usuarioname: Nombre para mostrar del usuarioscope: Ámbitos OAuth 2.0 para acceso a APItenant_id: Identificador de aplicación multi-inquilino
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:
- Inicio de Sesión del Usuario: El usuario envía credenciales (nombre de usuario + contraseña) a tu endpoint de autenticación
- Verificación de Credenciales: El servidor valida las credenciales contra tu base de datos de usuarios
- Generación de Token: El servidor crea un JWT que contiene claims del usuario y lo firma con una clave secreta
- Entrega de Token: El servidor devuelve el JWT al cliente (típicamente en el cuerpo de la respuesta o como una cookie httpOnly)
- Almacenamiento de Token: El cliente almacena el JWT de forma segura (más sobre esto en la sección Opciones de Almacenamiento)
- Solicitudes Autenticadas: El cliente incluye el JWT en el encabezado
Authorizationpara solicitudes subsiguientes - Verificación de Token: El servidor verifica la firma y comprueba la expiración—no se necesita búsqueda en base de datos
- 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