Saltar al contenido principal

🎫 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 (7as)

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 7as)
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:

  1. ¿Qué es HTTPS? (documento en preparación) - Transmisión segura de tokens
  2. ¿Qué es CORS? (documento en preparación) - Seguridad de API
  3. ¿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! 🎫✨