JSON Web Tokens (JWT): Autenticación y Autorización Completa
Aprende a implementar autenticación con JWT. Incluye ejemplos en Node.js, Python, seguridad y mejores prácticas.
Big JSON Team
• Technical WriterExpert in JSON data manipulation, API development, and web technologies. Passionate about creating tools that make developers' lives easier.
# JSON Web Tokens (JWT): Autenticación y Autorización Completa
JWT es el estándar de facto para autenticación en aplicaciones modernas. Esta guía completa te enseñará todo sobre JWT, desde conceptos básicos hasta implementación segura.
¿Qué es JWT?
JSON Web Token (JWT) es un estándar abierto (RFC 7519) que define una forma compacta y autónoma de transmitir información de forma segura entre partes como un objeto JSON.Estructura de un JWT
Un JWT consta de tres partes separadas por puntos:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
[Header].[Payload].[Signature]
1. Header (Cabecera)
{
"alg": "HS256",
"typ": "JWT"
}
Codificado en Base64URL: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
2. Payload (Carga útil)
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622,
"role": "admin"
}
Codificado en Base64URL: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
3. Signature (Firma)
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
Claims Estándar del Payload
Registered Claims (Recomendados)
{
"iss": "https://mi-app.com", // Issuer (emisor)
"sub": "user123", // Subject (sujeto/usuario)
"aud": "https://api.mi-app.com", // Audience (audiencia)
"exp": 1735689600, // Expiration (expiración)
"nbf": 1735686000, // Not Before (no antes de)
"iat": 1735686000, // Issued At (emitido en)
"jti": "abc123" // JWT ID (identificador único)
}
Custom Claims (Personalizados)
{
"userId": "12345",
"email": "usuario@ejemplo.com",
"role": "admin",
"permissions": ["read", "write", "delete"],
"tenant": "empresa-xyz"
}
Implementación en Node.js
Instalación
npm install jsonwebtoken
Generar Token
const jwt = require('jsonwebtoken');
const SECRET_KEY = process.env.JWT_SECRET || 'tu-clave-secreta-muy-segura';
function generarToken(usuario) {
const payload = {
sub: usuario.id,
email: usuario.email,
role: usuario.role,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (60 60) // 1 hora
};
return jwt.sign(payload, SECRET_KEY);
}
// Uso
const usuario = {
id: '12345',
email: 'ana@ejemplo.com',
role: 'admin'
};
const token = generarToken(usuario);
console.log(token);
Opciones Avanzadas
const token = jwt.sign(
{ userId: usuario.id },
SECRET_KEY,
{
algorithm: 'HS256',
expiresIn: '1h', // 1 hora
issuer: 'mi-app',
audience: 'api.mi-app.com',
subject: usuario.id.toString(),
notBefore: '0', // Válido inmediatamente
jwtid: generarUUID() // ID único del token
}
);
Verificar Token
function verificarToken(token) {
try {
const decoded = jwt.verify(token, SECRET_KEY);
return {
valido: true,
datos: decoded
};
} catch (error) {
if (error.name === 'TokenExpiredError') {
return { valido: false, error: 'Token expirado' };
} else if (error.name === 'JsonWebTokenError') {
return { valido: false, error: 'Token inválido' };
} else {
return { valido: false, error: 'Error de verificación' };
}
}
}
// Uso
const resultado = verificarToken(token);
if (resultado.valido) {
console.log('Usuario:', resultado.datos);
} else {
console.error('Error:', resultado.error);
}
Decodificar Sin Verificar
// Solo para inspeccionar, NO para validar
const decoded = jwt.decode(token, { complete: true });
console.log('Header:', decoded.header);
console.log('Payload:', decoded.payload);
console.log('Signature:', decoded.signature);
Middleware de Autenticación (Express)
Middleware Básico
const jwt = require('jsonwebtoken');
function autenticar(req, res, next) {
// Obtener token del header
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // "Bearer TOKEN"
if (!token) {
return res.status(401).json({ error: 'Token no proporcionado' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.usuario = decoded; // Agregar datos a request
next();
} catch (error) {
return res.status(403).json({ error: 'Token inválido' });
}
}
// Uso en rutas
app.get('/api/perfil', autenticar, (req, res) => {
// req.usuario contiene los datos del token
res.json({
mensaje: 'Perfil del usuario',
usuario: req.usuario
});
});
Middleware con Roles
function requiereRol(...rolesPermitidos) {
return (req, res, next) => {
if (!req.usuario) {
return res.status(401).json({ error: 'No autenticado' });
}
if (!rolesPermitidos.includes(req.usuario.role)) {
return res.status(403).json({
error: 'No tienes permisos para esta acción'
});
}
next();
};
}
// Uso
app.delete('/api/usuarios/:id',
autenticar,
requiereRol('admin', 'superadmin'),
(req, res) => {
// Solo admin o superadmin pueden ejecutar esto
eliminarUsuario(req.params.id);
res.json({ mensaje: 'Usuario eliminado' });
}
);
Sistema Completo de Autenticación
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const app = express();
app.use(express.json());
const SECRET_KEY = process.env.JWT_SECRET;
// Base de datos simulada
const usuarios = [];
// Registro
app.post('/api/registro', async (req, res) => {
try {
const { email, password, nombre } = req.body;
// Validar
if (!email || !password || !nombre) {
return res.status(400).json({ error: 'Datos incompletos' });
}
// Verificar si existe
if (usuarios.find(u => u.email === email)) {
return res.status(400).json({ error: 'Email ya registrado' });
}
// Hash password
const passwordHash = await bcrypt.hash(password, 10);
// Crear usuario
const nuevoUsuario = {
id: usuarios.length + 1,
email,
nombre,
passwordHash,
role: 'usuario',
createdAt: new Date()
};
usuarios.push(nuevoUsuario);
// Generar token
const token = jwt.sign(
{
sub: nuevoUsuario.id,
email: nuevoUsuario.email,
role: nuevoUsuario.role
},
SECRET_KEY,
{ expiresIn: '7d' }
);
res.status(201).json({
mensaje: 'Usuario creado',
token,
usuario: {
id: nuevoUsuario.id,
email: nuevoUsuario.email,
nombre: nuevoUsuario.nombre
}
});
} catch (error) {
res.status(500).json({ error: 'Error en el servidor' });
}
});
// Login
app.post('/api/login', async (req, res) => {
try {
const { email, password } = req.body;
// Buscar usuario
const usuario = usuarios.find(u => u.email === email);
if (!usuario) {
return res.status(401).json({ error: 'Credenciales inválidas' });
}
// Verificar password
const passwordValido = await bcrypt.compare(password, usuario.passwordHash);
if (!passwordValido) {
return res.status(401).json({ error: 'Credenciales inválidas' });
}
// Generar token
const token = jwt.sign(
{
sub: usuario.id,
email: usuario.email,
role: usuario.role
},
SECRET_KEY,
{ expiresIn: '7d' }
);
res.json({
token,
usuario: {
id: usuario.id,
email: usuario.email,
nombre: usuario.nombre,
role: usuario.role
}
});
} catch (error) {
res.status(500).json({ error: 'Error en el servidor' });
}
});
// Ruta protegida
app.get('/api/perfil', autenticar, (req, res) => {
const usuario = usuarios.find(u => u.id === req.usuario.sub);
res.json({
id: usuario.id,
email: usuario.email,
nombre: usuario.nombre,
role: usuario.role
});
});
app.listen(3000, () => console.log('Servidor en puerto 3000'));
Implementación en Python
Instalación
pip install pyjwt cryptography
Generar y Verificar Token
import jwt
import datetime
from typing import Dict, Optional
SECRET_KEY = "tu-clave-secreta-muy-segura"
ALGORITHM = "HS256"
def generar_token(usuario: Dict) -> str:
"""Genera JWT para usuario"""
payload = {
'sub': str(usuario['id']),
'email': usuario['email'],
'role': usuario['role'],
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1),
'iat': datetime.datetime.utcnow()
}
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
def verificar_token(token: str) -> Optional[Dict]:
"""Verifica y decodifica JWT"""
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload
except jwt.ExpiredSignatureError:
print("Token expirado")
return None
except jwt.InvalidTokenError:
print("Token inválido")
return None
# Uso
usuario = {
'id': 123,
'email': 'ana@ejemplo.com',
'role': 'admin'
}
token = generar_token(usuario)
print(f"Token: {token}")
# Verificar
datos = verificar_token(token)
if datos:
print(f"Usuario: {datos['email']}")
Flask con JWT
from flask import Flask, request, jsonify
from functools import wraps
import jwt
import datetime
app = Flask(__name__)
app.config['SECRET_KEY'] = 'tu-clave-secreta'
def token_requerido(f):
"""Decorador para rutas protegidas"""
@wraps(f)
def decorated(args, *kwargs):
token = request.headers.get('Authorization')
if not token:
return jsonify({'error': 'Token faltante'}), 401
# Remover "Bearer " si existe
if token.startswith('Bearer '):
token = token[7:]
try:
datos = jwt.decode(
token,
app.config['SECRET_KEY'],
algorithms=['HS256']
)
request.usuario_actual = datos
except jwt.ExpiredSignatureError:
return jsonify({'error': 'Token expirado'}), 401
except jwt.InvalidTokenError:
return jsonify({'error': 'Token inválido'}), 401
return f(args, kwargs)
return decorated
@app.route('/api/login', methods=['POST'])
def login():
datos = request.get_json()
email = datos.get('email')
password = datos.get('password')
# Validar credenciales (simplificado)
if email == 'usuario@ejemplo.com' and password == 'password123':
token = jwt.encode(
{
'sub': '123',
'email': email,
'role': 'usuario',
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=24)
},
app.config['SECRET_KEY'],
algorithm='HS256'
)
return jsonify({'token': token})
return jsonify({'error': 'Credenciales inválidas'}), 401
@app.route('/api/protegido', methods=['GET'])
@token_requerido
def ruta_protegida():
return jsonify({
'mensaje': 'Acceso autorizado',
'usuario': request.usuario_actual
})
if __name__ == '__main__':
app.run(debug=True)
Refresh Tokens
¿Por Qué Refresh Tokens?
- Access Token: Corta duración (15 min - 1 hora)
- Refresh Token: Larga duración (7 días - 30 días)
Implementación
const jwt = require('jsonwebtoken');
const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET;
const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET;
// Almacenar refresh tokens (usar DB en producción)
const refreshTokens = new Set();
function generarTokens(usuario) {
const accessToken = jwt.sign(
{ sub: usuario.id, email: usuario.email },
ACCESS_TOKEN_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ sub: usuario.id },
REFRESH_TOKEN_SECRET,
{ expiresIn: '7d' }
);
refreshTokens.add(refreshToken);
return { accessToken, refreshToken };
}
// Login
app.post('/api/login', async (req, res) => {
// ... validar credenciales ...
const tokens = generarTokens(usuario);
res.json({
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken
});
});
// Refresh
app.post('/api/refresh', (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token requerido' });
}
if (!refreshTokens.has(refreshToken)) {
return res.status(403).json({ error: 'Refresh token inválido' });
}
try {
const decoded = jwt.verify(refreshToken, REFRESH_TOKEN_SECRET);
// Generar nuevo access token
const accessToken = jwt.sign(
{ sub: decoded.sub },
ACCESS_TOKEN_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken });
} catch (error) {
return res.status(403).json({ error: 'Refresh token expirado' });
}
});
// Logout
app.post('/api/logout', (req, res) => {
const { refreshToken } = req.body;
refreshTokens.delete(refreshToken);
res.json({ mensaje: 'Logout exitoso' });
});
Seguridad
1. Usa Algoritmos Seguros
// ✅ Recomendado
const token = jwt.sign(payload, secret, { algorithm: 'HS256' });
// ✅ Aún mejor (asimétrico)
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
// ❌ NUNCA uses 'none'
// const token = jwt.sign(payload, '', { algorithm: 'none' });
2. Claves Secretas Fuertes
// ❌ Débil
const SECRET = 'secret';
// ✅ Fuerte (al menos 256 bits)
const SECRET = crypto.randomBytes(32).toString('hex');
// o usar variables de entorno
const SECRET = process.env.JWT_SECRET;
3. Validar Siempre el Algoritmo
// Especificar algoritmos permitidos
jwt.verify(token, secret, { algorithms: ['HS256'] });
// Previene ataques de confusión de algoritmo
4. Usar HTTPS
❌ http://api.ejemplo.com → JWT visible en tráfico
✅ https://api.ejemplo.com → JWT encriptado en tráfico
5. Expiración Apropiada
// Tokens de corta duración
const accessToken = jwt.sign(payload, secret, { expiresIn: '15m' });
// Con refresh tokens
const refreshToken = jwt.sign(payload, secret, { expiresIn: '7d' });
6. No Almacenar Datos Sensibles
// ❌ NO hacer esto
const token = jwt.sign({
password: usuario.password,
creditCard: usuario.creditCard
}, secret);
// ✅ Solo datos necesarios no sensibles
const token = jwt.sign({
sub: usuario.id,
role: usuario.role
}, secret);
Almacenamiento en Cliente
Opciones
localStorage:
// ❌ Vulnerable a XSS
localStorage.setItem('token', token);
sessionStorage:
// ❌ También vulnerable a XSS
sessionStorage.setItem('token', token);
HttpOnly Cookies:*
// ✅ Más seguro (no accesible desde JavaScript)
res.cookie('token', token, {
httpOnly: true,
secure: true, // Solo HTTPS
sameSite: 'strict',
maxAge: 3600000 // 1 hora
});
Mejor Práctica: Cookie + Header
// Servidor envía en cookie HttpOnly
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict'
});
// Access token en respuesta JSON
res.json({ accessToken });
// Cliente guarda access token en memoria (no localStorage)
// y envía en Authorization header
Mejores Prácticas
1. Implementa Lista Negra para Logout
const listaNegraTokens = new Set();
app.post('/api/logout', autenticar, (req, res) => {
const token = req.headers.authorization.split(' ')[1];
listaNegraTokens.add(token);
res.json({ mensaje: 'Logout exitoso' });
});
// Middleware actualizado
function autenticar(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (listaNegraTokens.has(token)) {
return res.status(401).json({ error: 'Token revocado' });
}
// ... resto de verificación
}
2. Rate Limiting
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 60 * 1000, // 15 minutos
max: 5, // 5 intentos
message: 'Demasiados intentos de login'
});
app.post('/api/login', loginLimiter, async (req, res) => {
// ... lógica de login
});
3. Auditoría
function registrarAcceso(req, accion) {
console.log({
timestamp: new Date(),
usuario: req.usuario?.sub,
accion,
ip: req.ip,
userAgent: req.get('user-agent')
});
}
app.get('/api/datos-sensibles', autenticar, (req, res) => {
registrarAcceso(req, 'ACCESO_DATOS_SENSIBLES');
// ... resto de lógica
});
Conclusión
JWT es poderoso pero debe implementarse correctamente. La seguridad es crítica.
Puntos Clave
- Usa algoritmos seguros (HS256, RS256)
- Implementa refresh tokens
- Expira access tokens rápidamente
- Almacena en HttpOnly cookies cuando sea posible
- Siempre usa HTTPS
- No almacenes datos sensibles en payload
- Implementa rate limiting
- Valida todos los tokens
¡Implementa autenticación JWT de forma segura!
Artículos Relacionados
Python y JSON: Guía Completa del Módulo json
Aprende a trabajar con JSON en Python usando el módulo json. Incluye parsing, serialización, archivos JSON y mejores prácticas.
JavaScript y JSON: Guía Completa de JSON.parse() y JSON.stringify()
Domina el trabajo con JSON en JavaScript. Aprende JSON.parse(), JSON.stringify() y mejores prácticas para aplicaciones web modernas.
JSON en APIs REST: Guía Completa para Desarrolladores
Aprende a diseñar y consumir APIs REST con JSON. Incluye mejores prácticas, ejemplos en Node.js, Python y autenticación.