JWT para Desarrolladores: Decodifica y Valida Tokens de Autenticación
Aprende la estructura de JWT, cómo decodificar tokens, entender claims y usarlos en aplicaciones React/Next.js.
La frustración del desarrollador junior: ¿por qué mi "token" no funciona?
Recuerdo perfectamente mi primera experiencia con JWT. Estaba construyendo una API REST con Express y React, y seguí un tutorial que decía "usa JWT para autenticación, es fácil". Generé un token, lo guardé en localStorage, lo envié en un header llamado "Token" (no "Authorization"), y me pasé tres horas preguntándome por qué el servidor siempre respondía 401 Unauthorized.
El problema no era el token. Era que no entendía qué estaba haciendo. Veía JWT como una caja negra mágica: metes datos, sacas un string largo, lo pasas al servidor, y funciona. Hasta que no funciona.
Este tutorial es lo que desearía haber leído entonces. No solo te diré cómo usar JWT, sino que te explicaré exactamente qué está pasando en cada paso, para que cuando algo falle (y fallará), sepas dónde mirar.
¿Qué es realmente un JWT y por qué lo necesitas?
Cuando empecé a programar backends, la autenticación era simple: login con usuario/contraseña, crear una sesión en el servidor, guardar el ID de sesión en una cookie. Funcionaba, pero tenía problemas:
- Escalabilidad: Si tienes 5 servidores, ¿cómo comparten las sesiones? ¿Redis? ¿Sticky sessions? Cada opción añade complejidad.
- APIs móviles: Las cookies son complicadas en apps nativas. Cada plataforma las maneja diferente.
- Microservicios: Cada servicio necesita verificar la sesión con el servicio central de autenticación.
JWT resuelve esto de forma elegante: el servidor no guarda nada. El estado de la sesión va dentro del token. Cualquier servidor que tenga la clave secreta puede verificar el token sin preguntar a nadie más.
Esto es lo que hace que las arquitecturas serverless y microservicios sean viables. No necesitas un "servidor de sesiones" central. Cada función Lambda, cada microservicio, puede verificar tokens independientemente.
Desmontando un JWT: tres partes, tres responsabilidades
Un JWT parece un galimatías de letras y números:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Pero esas tres partes separadas por puntos tienen propósitos muy específicos:
Parte 1: Header — "Cómo verificar esto"
// Decodificado:
{
"alg": "HS256", // Algoritmo: HMAC con SHA-256
"typ": "JWT" // Tipo: JWT (siempre JWT)
}
El header le dice al receptor cómo debe verificar la firma. ¿Es HS256 (clave simétrica)? ¿RS256 (RSA con clave pública/privada)? ¿ES256 (ECDSA)?
Importante: El header no está firmado. Un atacante podría cambiar "HS256" a "none" (un ataque real que existió). Por eso siempre debes verificar el algoritmo esperado, no confiar ciegamente en el header.
Parte 2: Payload — "Qué sabemos de este usuario"
// Decodificado:
{
"sub": "1234567890", // Subject: ID del usuario
"name": "John Doe", // Nombre
"iat": 1516239022, // Issued At: cuando se creó
"exp": 1516242622, // Expiration: cuando expira
"iss": "mi-api", // Issuer: quién emitió el token
"aud": "mi-app-web", // Audience: para quién es
"roles": ["user", "admin"] // Claims personalizados
}
Aquí está la información útil. Pero recuerda: esto no está cifrado, solo codificado en Base64. Cualquiera puede leerlo (puedes verificarlo en nuestra herramienta JWT Decoder). No pongas contraseñas, números de tarjeta, o información sensible aquí.
Los claims estándar (los de tres letras) tienen significados definidos:
- sub: Identificador único del sujeto (generalmente el user ID)
- iss: Quién emitió el token (útil si tienes múltiples servicios)
- aud: Quién debe aceptar este token (evita que un token de desarrollo funcione en producción)
- exp: Timestamp Unix de expiración. Pasado este tiempo, el token es inválido.
- iat: Timestamp de emisión. Útil para detectar tokens muy antiguos.
- nbf: "Not Before" — el token no es válido antes de esta hora.
- jti: JWT ID único. Útil para blacklist de tokens específicos.
Parte 3: Signature — "Que nadie haya manipulado esto"
La firma se calcula así:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret_key
)
Es decir: tomas el header codificado, un punto, el payload codificado, y calculas un HMAC-SHA256 usando tu clave secreta.
Esto crea un sello digital. Si alguien cambia un solo bit del header o payload, la firma ya no coincidirá. El servidor detecta la manipulación inmediatamente.
Decodificar vs Validar: la distinción que confunde a todos
Aquí está el error más común que veo en código de producción:
// ❌ INCORRECTO - Solo decodifica, no valida
function getUserFromToken(token) {
const payload = JSON.parse(atob(token.split('.')[1]));
return { id: payload.sub, name: payload.name };
}
Este código funciona aparentemente. El usuario "hace login". Pero cualquiera puede crear un token falso:
// Crear un token falso válido
const fakePayload = btoa(JSON.stringify({
sub: "admin",
name: "Attacker",
roles: ["admin"]
}));
const fakeToken = "header." + fakePayload + ".fake_signature";
// Este token funcionará en el código incorrecto de arriba
Decodificar es solo convertir Base64 a JSON. Cualquiera puede hacerlo. Validar es verificar que el token fue creado por tu servidor y no ha sido modificado.
Nuestra herramienta JWT Decoder te permite ver el contenido decodificado, pero recuerda: eso no significa que el token sea válido.
Cómo decodificar un JWT (para debugging)
Aunque no sea suficiente para validar, decodificar es útil para debugging. Aquí están tres formas:
Forma 1: JavaScript nativo
function decodeJWT(token) {
// Split en los tres puntos
const parts = token.split('.');
if (parts.length !== 3) throw new Error('Token inválido');
// Decodificar header y payload
const header = JSON.parse(atob(parts[0]));
const payload = JSON.parse(atob(parts[1]));
return { header, payload };
}
// Uso
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
const { header, payload } = decodeJWT(token);
console.log('Algoritmo:', header.alg);
console.log('Usuario:', payload.sub);
console.log('Expira:', new Date(payload.exp * 1000));
Forma 2: Línea de comandos
# Extraer payload (segunda parte)
echo "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ" | base64 -d
Forma 3: Nuestra herramienta web
Simplemente pega el token en nuestra herramienta JWT Decoder. Verás el contenido formateado con colores, y podrás copiarlo fácilmente.
Validación correcta en Node.js
Para validar, necesitas verificar la firma criptográficamente. La librería más popular es jsonwebtoken:
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET; // Nunca hardcodees esto
// Función de verificación
function verifyToken(token) {
try {
// verify() decodifica Y valida simultáneamente
const decoded = jwt.verify(token, JWT_SECRET);
return { valid: true, payload: decoded };
} catch (err) {
// Errores específicos que puedes manejar
if (err.name === 'TokenExpiredError') {
return { valid: false, error: 'Token expirado' };
}
if (err.name === 'JsonWebTokenError') {
return { valid: false, error: 'Token inválido' };
}
return { valid: false, error: 'Error desconocido' };
}
}
// Middleware para Express
function authMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Token requerido' });
}
const token = authHeader.substring(7); // Quitar "Bearer "
const result = verifyToken(token);
if (!result.valid) {
return res.status(401).json({ error: result.error });
}
req.user = result.payload; // Disponible en rutas protegidas
next();
}
Fíjate en el detalle del header: debe empezar con "Bearer " (con espacio). Muchos errores 401 son simplemente porque el frontend envía "Token: xyz" en lugar de "Authorization: Bearer xyz".
JWT en React/Next.js: la vida real
Dónde guardar el token: el gran debate
Hay tres opciones, cada una con compromisos:
Opción 1: localStorage
// Después del login
localStorage.setItem('token', response.token);
// Al hacer peticiones
fetch('/api/data', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
Ventajas: Simple, persiste entre sesiones del navegador, funciona en cualquier entorno. Desventajas: Vulnerable a XSS (Cross-Site Scripting). Si un atacante inyecta JavaScript en tu página, puede leer localStorage y robar el token.
Opción 2: Cookie httpOnly
// En el backend (Node.js/Express)
res.cookie('token', jwtToken, {
httpOnly: true, // No accesible desde JavaScript
secure: true, // Solo HTTPS
sameSite: 'strict', // Protección CSRF
maxAge: 3600000 // 1 hora
});
// El token se envía automáticamente con cada petición
// No necesitas hacer nada en el frontend
Ventajas: Protegido contra XSS (httpOnly), el navegador lo maneja automáticamente. Desventajas: Requiere configuración backend más compleja, vulnerable a CSRF si no usas SameSite.
Opción 3: Memoria (React Context/State)
const [token, setToken] = useState(null);
// Guardar después del login
setToken(response.token);
// Usar en peticiones
Ventajas: Más seguro contra XSS (no está en storage persistente). Desventajas: El usuario pierde la sesión al recargar la página. Necesitas refresh tokens.
Mi recomendación práctica: Para apps empresariales, usa cookies httpOnly. Para apps simples o APIs que necesitan ser consumidas desde múltiples dominios, localStorage está bien si sanitizas bien tu código contra XSS.
Interceptor de Axios: automatizando el envío
Si usas Axios (lo cual recomiendo sobre fetch para apps complejas), configura un interceptor:
import axios from 'axios';
// Crear instancia
const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL
});
// Interceptor de request: añade token automáticamente
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Interceptor de response: maneja errores 401
api.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
// Token expirado o inválido
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default api;
Ahora todas tus peticiones automáticamente incluyen el token, y los 401 te redirigen al login.
Manejo de expiración: refresh tokens
Los access tokens deberían ser cortos (15-30 minutos). Pero no quieres que el usuario tenga que hacer login cada media hora. La solución son los refresh tokens:
// El backend emite dos tokens
const loginResponse = {
accessToken: 'jwt_corto_15min', // Para acceso a recursos
refreshToken: 'jwt_largo_7dias' // Para obtener nuevos access tokens
};
// En el interceptor de Axios
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// Si es 401 y no es un retry
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// Intentar refresh
const refreshToken = localStorage.getItem('refreshToken');
const response = await axios.post('/api/refresh', { refreshToken });
// Guardar nuevo access token
localStorage.setItem('token', response.data.accessToken);
// Reintentar petición original
originalRequest.headers.Authorization = `Bearer ${response.data.accessToken}`;
return api(originalRequest);
} catch (refreshError) {
// Refresh falló, redirigir a login
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
Esto da la mejor UX: el usuario permanece logueado por días, pero los tokens de acceso son cortos para minimizar el daño si son robados.
Errores que me han hecho perder horas de debugging
1. "Token inválido" cuando parece correcto
Verifica que estás usando el algoritmo correcto. Si el token fue firmado con HS256 pero tu servidor espera RS256, fallará.
// Especificar explícitamente el algoritmo
jwt.verify(token, secret, { algorithms: ['HS256'] });
2. La fecha de expiración parece aleatoria
Los timestamps JWT son en segundos Unix, no milisegundos como JavaScript Date.
// ❌ Incorrecto
const expDate = new Date(payload.exp); // Multiplica por 1000!
// ✅ Correcto
const expDate = new Date(payload.exp * 1000);
console.log(expDate.toLocaleString()); // "9/6/2026, 14:30:22"
3. El token funciona en Postman pero no en el navegador
Revisa CORS. Si tu API está en otro dominio, debe incluir:
Access-Control-Allow-Headers: Authorization
4. "jwt malformed" en tokens que parecen válidos
El encoding es Base64URL, no Base64 estándar. Diferencias:
- Base64 usa + y /
- Base64URL usa - y _
JavaScript atob() espera Base64 estándar. Para JWT necesitas convertir:
function base64UrlDecode(str) {
// Convertir Base64URL a Base64 estándar
str += new Array(5 - str.length % 4).join('='); // Añadir padding
return atob(str.replace(/-/g, '+').replace(/_/g, '/'));
}
Aunque en la práctica, usa una librería como jwt-decode que lo hace por ti.
Conclusión: JWT no es magia, es ingeniería
Cuando entiendes que un JWT es simplemente un JSON codificado con una firma criptográfica, deja de ser intimidante. Los errores que parecen misteriosos ("token inválido") suelen tener explicaciones simples una vez que entiendes la estructura.
Las tres reglas de oro:
- Header + Payload son públicos: No pongas secretos ahí. Cualquiera puede leerlos.
- Siempre valida en el servidor: Nunca confíes en lo que el cliente te envía sin verificar la firma.
- Las cookies httpOnly son más seguras: Pero localStorage es más fácil. Elige según tu threat model.
Y recuerda: nuestra herramienta JWT Decoder está ahí para cuando necesites debuggear un token rápidamente sin escribir código.
Artículos relacionados
UUIDs: La Guía Definitiva para IDs Distribuidos sin Dolor de Cabeza
Descubre por qué los IDs secuenciales fallan en sistemas modernos, cómo funcionan realmente los UUIDs (v4, v7), y cuándo usar cada tipo sin sacrificar rendimiento.
Base64: El Puente Entre Binario y Texto que Todo Desarrollador Necesita
Entiende por qué existe Base64, cómo funciona realmente, y cuándo usarlo para data URIs, JWTs, y APIs. Con ejemplos prácticos de navegador y Node.js.
Generación de Contraseñas Seguras: Más Allá de los Símbolos Raros
Descubre por qué la longitud vence a la complejidad, qué es la entropía real, y cómo crear políticas de contraseñas que los usuarios no odien.