🎫 Comprensión Completa de JWT Token
📖 Definición
JWT(JSON Web Token) es un token basado en JSON para transmitir información de forma segura entre cliente y servidor. Consta de tres partes (Header, Payload, Signature), emitido por el servidor y enviado por el cliente en solicitudes para manejar autenticación. A diferencia de las sesiones, no almacena estado en el servidor (stateless), por lo que tiene excelente escalabilidad.
🎯 Comprender con Analogía
Boleto de Parque de Atracciones
Si comparamos JWT con un boleto de parque de atracciones:
Boleto Normal (Método de Sesión)
├─ Recibir pulsera al entrar
├─ Personal verifica pulsera cada vez
└─ Personal debe recordar todos los visitantes (carga del servidor)
Boleto VIP (Método JWT)
├─ Boleto con sello infalsificable al entrar
├─ Boleto registra nombre, validez, grado
├─ Solo mostrar boleto cada vez
└─ Personal solo verifica sello (poca carga del servidor)
JWT = Tarjeta de información con sello antifalsficación
⚙️ Principio de Funcionamiento
1. Estructura JWT
JWT = Header.Payload.Signature
Ejemplo:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywidXNlcm5hbWUiOiJraW0ifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
│ │ │
Header (Encabezado) Payload (Carga Útil) Signature (Firma)
Header (Encabezado)
{
"alg": "HS256", // Algoritmo de firma
"typ": "JWT" // Tipo de token
}
// Codificado en Base64
Payload (Carga Útil) - Datos Reales
{
"userId": 123,
"username": "kim",
"email": "kim@example.com",
"role": "admin",
"iat": 1640000000, // Issued At (Hora de emisión)
"exp": 1640086400 // Expiration (Hora de expiración)
}
// Codificado en Base64
Signature (Firma) - Prevención de Falsificación
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret // Clave secreta (solo el servidor la conoce)
)
2. Flujo de Autenticación JWT
1. Inicio de Sesión
Cliente → Servidor: username, password
Servidor: Autenticación exitosa → Genera y devuelve JWT
2. Incluir JWT en Solicitud
Cliente → Servidor: Authorization: Bearer <JWT>
3. Servidor Verifica JWT
- Verificar firma (si es falsificado)
- Verificar tiempo de expiración
- Extraer información de usuario del Payload
4. Respuesta
Servidor → Cliente: Datos solicitados
💡 Ejemplos Reales
Generación y Verificación de JWT (Node.js)
const jwt = require('jsonwebtoken');
// Clave secreta (se recomienda gestionar con variables de entorno)
const SECRET_KEY = 'your-secret-key-keep-it-safe';
// ========== Generación de JWT ==========
function generateToken(user) {
// Definir Payload
const payload = {
userId: user.id,
username: user.username,
email: user.email,
role: user.role
};
// Opciones
const options = {
expiresIn: '1h', // Expira después de 1 hora
// expiresIn: '7d', // 7 días
// expiresIn: '30m', // 30 minutos
issuer: 'my-app', // Emisor
subject: 'user-auth' // Propósito
};
// Generar JWT
const token = jwt.sign(payload, SECRET_KEY, options);
return token;
}
// ========== Verificación de JWT ==========
function verifyToken(token) {
try {
const decoded = jwt.verify(token, SECRET_KEY);
console.log('Verificación exitosa:', decoded);
/*
{
userId: 123,
username: 'kim',
email: 'kim@example.com',
role: 'admin',
iat: 1640000000,
exp: 1640003600
}
*/
return decoded;
} catch (error) {
if (error.name === 'TokenExpiredError') {
console.error('Token expirado');
} else if (error.name === 'JsonWebTokenError') {
console.error('Token inválido');
}
return null;
}
}
// ========== Ejemplo de Uso ==========
const user = {
id: 123,
username: 'kim',
email: 'kim@example.com',
role: 'admin'
};
const token = generateToken(user);
console.log('JWT Generado:', token);
const decoded = verifyToken(token);
console.log('Decodificado:', decoded);
Implementación de API de Inicio de Sesión
const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const app = express();
app.use(express.json());
const SECRET_KEY = process.env.JWT_SECRET || 'your-secret-key';
const users = []; // En realidad, usar base de datos
// ========== Registro ==========
app.post('/api/register', async (req, res) => {
const { username, password, email } = req.body;
// Hash de contraseña
const hashedPassword = await bcrypt.hash(password, 10);
const user = {
id: users.length + 1,
username,
email,
password: hashedPassword
};
users.push(user);
res.json({ message: 'Registro exitoso', userId: user.id });
});
// ========== Inicio de Sesión ==========
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;
// Buscar usuario
const user = users.find(u => u.username === username);
if (!user) {
return res.status(401).json({ error: 'Usuario no encontrado' });
}
// Verificar contraseña
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return res.status(401).json({ error: 'Contraseña incorrecta' });
}
// Generar JWT
const token = jwt.sign(
{
userId: user.id,
username: user.username,
email: user.email
},
SECRET_KEY,
{ expiresIn: '1h' }
);
// Emitir también Refresh Token (opcional)
const refreshToken = jwt.sign(
{ userId: user.id },
SECRET_KEY,
{ expiresIn: '7d' }
);
res.json({
message: 'Inicio de sesión exitoso',
accessToken: token,
refreshToken: refreshToken
});
});
// ========== Middleware de Autenticación ==========
function authenticateToken(req, res, next) {
// Extraer token del encabezado Authorization
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // "Bearer TOKEN"
if (!token) {
return res.status(401).json({ error: 'Sin token' });
}
jwt.verify(token, SECRET_KEY, (err, user) => {
if (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expirado' });
}
return res.status(403).json({ error: 'Token inválido' });
}
// Verificación exitosa - guardar información de usuario en req
req.user = user;
next();
});
}
// ========== Rutas Protegidas ==========
app.get('/api/profile', authenticateToken, (req, res) => {
// req.user es información extraída del JWT
res.json({
message: 'Información del perfil',
user: req.user
});
});
app.get('/api/admin', authenticateToken, (req, res) => {
// Verificación de permisos
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Se requieren permisos de administrador' });
}
res.json({ message: 'Página de administrador' });
});
app.listen(3000, () => {
console.log('Servidor ejecutándose: http://localhost:3000');
});
Usar JWT en Frontend
// ========== Inicio de Sesión ==========
async function login(username, password) {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (response.ok) {
// Guardar JWT en LocalStorage
localStorage.setItem('accessToken', data.accessToken);
localStorage.setItem('refreshToken', data.refreshToken);
console.log('¡Inicio de sesión exitoso!');
}
}
// ========== Incluir JWT en Solicitud API ==========
async function fetchProfile() {
const token = localStorage.getItem('accessToken');
const response = await fetch('/api/profile', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.status === 401) {
// Token expirado → Ir a página de inicio de sesión
window.location.href = '/login';
return;
}
const data = await response.json();
console.log('Perfil:', data);
}
// ========== Automatización con Interceptores de Axios ==========
import axios from 'axios';
// Interceptor de solicitud: Agregar JWT automáticamente a todas las solicitudes
axios.interceptors.request.use(
config => {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => Promise.reject(error)
);
// Interceptor de respuesta: Cierre de sesión automático en error 401
axios.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
localStorage.removeItem('accessToken');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
// ¡Ahora todas las solicitudes incluyen JWT automáticamente!
axios.get('/api/profile').then(response => {
console.log(response.data);
});
Implementación de Refresh Token
// ========== Patrón Access Token + Refresh Token ==========
// Access Token: Vida corta (15 minutos)
// Refresh Token: Vida larga (7 días)
app.post('/api/login', async (req, res) => {
// ... Verificación de inicio de sesión ...
const accessToken = jwt.sign(
{ userId: user.id, username: user.username },
SECRET_KEY,
{ expiresIn: '15m' } // 15 minutos
);
const refreshToken = jwt.sign(
{ userId: user.id },
REFRESH_SECRET_KEY,
{ expiresIn: '7d' } // 7 días
);
// Guardar Refresh Token en DB (opcional)
// await saveRefreshToken(user.id, refreshToken);
res.json({ accessToken, refreshToken });
});
// ========== Renovar Access Token ==========
app.post('/api/refresh', (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ error: 'Sin Refresh Token' });
}
jwt.verify(refreshToken, REFRESH_SECRET_KEY, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Refresh Token inválido' });
}
// Emitir nuevo Access Token
const accessToken = jwt.sign(
{ userId: user.userId },
SECRET_KEY,
{ expiresIn: '15m' }
);
res.json({ accessToken });
});
});
// ========== Frontend: Renovación Automática de Token ==========
axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// Obtener nuevo Access Token con Refresh Token
const refreshToken = localStorage.getItem('refreshToken');
const response = await axios.post('/api/refresh', { refreshToken });
const { accessToken } = response.data;
localStorage.setItem('accessToken', accessToken);
// Reintentar solicitud original
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return axios(originalRequest);
} catch (refreshError) {
// Refresh Token también expirado → Cerrar sesión
localStorage.clear();
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
🤔 Preguntas Frecuentes
Q1. ¿Cuál es la diferencia entre JWT y Sesión?
R:
// ========== Método de Sesión ==========
// Servidor guarda información de usuario
Iniciar sesión → Servidor guarda sesión
{
sessionId: 'abc123',
userId: 123,
username: 'kim'
}
Cliente solo guarda sessionId
Cookie: sessionId=abc123
Consultar sesión del servidor con sessionId en solicitud
Ventaja: El servidor puede invalidar inmediatamente
Desventaja: Requiere memoria/almacenamiento del servidor, baja escalabilidad
// ========== Método JWT ==========
// Cliente guarda token
Iniciar sesión → Generar y devolver JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Cliente guarda JWT completo
localStorage.setItem('token', jwt)
Incluir JWT en solicitud, servidor solo verifica firma
Ventaja: Sin carga del servidor, buena escalabilidad
Desventaja: Difícil invalidar token, tamaño grande
Q2. ¿Es seguro JWT?
R: Seguro si se usa correctamente:
// ✅ Uso Seguro
1. Usar HTTPS obligatorio
- HTTP permite interceptar JWT
2. Seguridad de clave secreta
- Gestionar con variables de entorno
- Absolutamente prohibido codificar en código
const SECRET = process.env.JWT_SECRET;
3. Tiempo de expiración corto
- Access Token: 15 minutos~1 hora
- Renovar con Refresh Token
4. Prohibir guardar información sensible
❌ { password: '1234', ssn: '123-45-6789' }
✅ { userId: 123, role: 'user' }
5. Prevención de XSS
- Se recomienda HttpOnly Cookie sobre LocalStorage
res.cookie('token', jwt, {
httpOnly: true, // Sin acceso JavaScript
secure: true, // Solo HTTPS
sameSite: 'strict'
});
// ❌ Uso Peligroso
1. Sin tiempo de expiración
2. Transmitir por HTTP
3. Guardar en LocalStorage (vulnerable a XSS)
4. Incluir información sensible como contraseña
Q3. ¿Cómo invalidar JWT?
R: Hay varios métodos:
// 1. Lista Negra (Blacklist)
// - Guardar token en DB/Redis al cerrar sesión
// - Verificar lista negra en solicitud
const blacklist = new Set();
app.post('/api/logout', authenticateToken, (req, res) => {
const token = req.headers.authorization.split(' ')[1];
blacklist.add(token);
res.json({ message: 'Cierre de sesión exitoso' });
});
function authenticateToken(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (blacklist.has(token)) {
return res.status(401).json({ error: 'Token invalidado' });
}
// ... Verificación JWT ...
}
// 2. Guardar en Redis (configurar tiempo de expiración)
const redis = require('redis');
const client = redis.createClient();
app.post('/api/logout', authenticateToken, async (req, res) => {
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.decode(token);
// Mantener en lista negra hasta el tiempo de expiración del token
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
await client.setEx(`blacklist:${token}`, ttl, 'true');
res.json({ message: 'Cierre de sesión exitoso' });
});
// 3. Vida corta + Refresh Token
// - Access Token: 15 minutos (no necesita invalidación)
// - Solo guardar y invalidar Refresh Token en DB
// 4. Gestión de Versión
// - Guardar versión de token de usuario en DB
// - Incrementar versión al cerrar sesión
const payload = {
userId: user.id,
tokenVersion: user.tokenVersion // Obtener de DB
};
// Cerrar sesión
await db.users.update(
{ id: userId },
{ $inc: { tokenVersion: 1 } } // Incrementar versión
);
Q4. ¿Dónde guardar JWT?
R:
// Opción 1: LocalStorage (simple pero vulnerable a XSS)
localStorage.setItem('token', jwt);
// ❌ Ataque XSS puede robar token
// Opción 2: SessionStorage (eliminar al cerrar pestaña)
sessionStorage.setItem('token', jwt);
// ❌ Vulnerable a XSS
// Opción 3: HttpOnly Cookie (más seguro, recomendado)
// Configurar en servidor
res.cookie('token', jwt, {
httpOnly: true, // Sin acceso JavaScript (prevenir XSS)
secure: true, // Solo HTTPS
sameSite: 'strict',// Prevenir CSRF
maxAge: 3600000 // 1 hora
});
// Cliente envía automáticamente
fetch('/api/profile', {
credentials: 'include' // Incluir Cookie
});
// Opción 4: Memoria (más seguro pero cierre de sesión al actualizar)
let token = null;
function login() {
// ... Iniciar sesión ...
token = response.accessToken; // Solo guardar en variable
}
// Comparación de ventajas y desventajas
LocalStorage: Simple pero vulnerable a XSS
Cookie: Seguro pero configuración compleja
Memory: Más seguro pero UX malo
Q5. ¿Access Token vs Refresh Token?
R:
// Access Token
// - Vida corta (15 minutos~1 hora)
// - Enviar en cada solicitud API
// - Expira rápido incluso si es robado
// Refresh Token
// - Vida larga (7 días~30 días)
// - Usar solo al renovar Access Token
// - Guardar y gestionar en DB
// Flujo
1. Iniciar sesión
→ Emitir Access Token (15 min) + Refresh Token (7 días)
2. Solicitud API
→ Usar Access Token
3. Access Token expirado (después de 15 min)
→ Obtener nuevo Access Token con Refresh Token
4. Refresh Token también expirado (después de 7 días)
→ Requiere reiniciar sesión
// Ejemplo de código
// Emitir tokens
const accessToken = jwt.sign(payload, SECRET, { expiresIn: '15m' });
const refreshToken = jwt.sign({ userId }, REFRESH_SECRET, { expiresIn: '7d' });
// Renovación automática
if (accessTokenExpired) {
const newAccessToken = await refreshAccessToken(refreshToken);
}
🎓 Próximos Pasos
Después de comprender JWT, aprende:
- ¿Qué es HTTPS? (documento en preparación) - Transmisión segura de tokens
- ¿Qué es CORS? (documento en preparación) - Seguridad de API
- ¿Qué es Cookie? - Sesión vs JWT
Herramientas Útiles
// Depurador JWT
// https://jwt.io
// - Decodificación y verificación de JWT
// Bibliotecas
// Node.js: jsonwebtoken
// Python: PyJWT
// Java: jjwt
// Go: jwt-go
// Prueba
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
const decoded = jwt.decode(token, { complete: true });
console.log(decoded);
/*
{
header: { alg: 'HS256', typ: 'JWT' },
payload: { userId: 123, ... },
signature: '...'
}
*/
🎬 Resumen
JWT es el método de autenticación estándar de la web moderna:
- Estructura: Header.Payload.Signature
- Ventajas: Stateless, escalabilidad, soporte multiplataforma
- Seguridad: HTTPS, HttpOnly Cookie, vida corta
- Patrón: Access Token + Refresh Token
¡Si se usa correctamente, se puede construir un sistema de autenticación seguro y eficiente! 🎫✨