Autenticación JWT
Firma, verifica y refresca JWTs en Python, JavaScript y Java — flujos access/refresh, pares de claves RS256 y revocación de tokens bien hecha.
Visión General
Los JSON Web Tokens (JWT) siguen siendo la forma estándar de hacer autenticación sin estado en APIs modernas. Un JWT es una cadena compacta y segura para URLs que transporta claims firmados — como la identidad del usuario y el tiempo de expiración — entre un cliente y un servidor.
Abajo: cómo firmar, verificar y refrescar JWTs de forma segura en Python, JavaScript y Java — incluyendo las partes de refresh-flow y revocación que la mayoría de tutoriales se saltan.
Cuándo Usarlo
Usa JWTs cuando:
- Construyas una API REST sin estado donde las sesiones no deben almacenarse del lado del servidor
- Autentiques microservicios que se llaman entre sí internamente
- Emitas tokens de acceso de corta duración con tokens de refresco de mayor duración
- Agregues SSO o login de terceros (OAuth2 / OpenID Connect)
Evita JWTs cuando:
- Necesites revocación inmediata de tokens del lado del servidor (usa sesiones + una lista de bloqueo)
- El payload sea muy grande (los JWTs se envían en cada solicitud)
- No estés preparado para rotar claves de firma de forma segura
Solución
Python (PyJWT)
import jwt
from datetime import datetime, timedelta, UTC
SECRET = "your-256-bit-secret" # almacenar en env, nunca en código
ALGORITHM = "HS256"
def create_token(user_id: str, expires_minutes: int = 15) -> str:
payload = {
"sub": user_id,
"iat": datetime.now(UTC),
"exp": datetime.now(UTC) + timedelta(minutes=expires_minutes),
}
return jwt.encode(payload, SECRET, algorithm=ALGORITHM)
def verify_token(token: str) -> dict:
try:
return jwt.decode(token, SECRET, algorithms=[ALGORITHM])
except jwt.ExpiredSignatureError:
raise ValueError("El token ha expirado")
except jwt.InvalidTokenError:
raise ValueError("Token inválido")
# Uso
token = create_token("user-123")
claims = verify_token(token)
print(claims["sub"]) # user-123
JavaScript (jsonwebtoken)
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET; // secreto de 256 bits desde env
function createToken(userId, expiresIn = '15m') {
return jwt.sign({ sub: userId }, SECRET, { expiresIn });
}
function verifyToken(token) {
try {
return jwt.verify(token, SECRET);
} catch (err) {
if (err.name === 'TokenExpiredError') throw new Error('El token ha expirado');
throw new Error('Token inválido');
}
}
// Uso
const token = createToken('user-123');
const claims = verifyToken(token);
console.log(claims.sub); // user-123
Java (JJWT 0.12+)
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
public class JwtUtil {
private static final SecretKey KEY =
Keys.hmacShaKeyFor(System.getenv("JWT_SECRET").getBytes(StandardCharsets.UTF_8));
private static final long ACCESS_TTL = 15 * 60 * 1000L; // 15 min
public String createToken(String userId) {
Date now = new Date();
return Jwts.builder()
.subject(userId)
.issuedAt(now)
.expiration(new Date(now.getTime() + ACCESS_TTL))
.signWith(KEY) // HS256 inferido desde la clave
.compact();
}
public Claims verifyToken(String token) {
try {
return Jwts.parser()
.verifyWith(KEY)
.build()
.parseSignedClaims(token)
.getPayload();
} catch (ExpiredJwtException e) {
throw new IllegalArgumentException("El token ha expirado");
} catch (JwtException e) {
throw new IllegalArgumentException("Token inválido");
}
}
}
Explicación
Un JWT tiene tres partes separadas por puntos: header.payload.signature.
- Header: indica al verificador qué algoritmo (
alg) produjo la firma y el tipo de token (typ). - Payload: el conjunto de claims — de quién es el token (
sub), cuándo se emitió (iat) y cuándo expira (exp). - Signature: el chequeo de integridad. Se obtiene firmando
base64(header) + "." + base64(payload)con tu clave secreta — un solo bit alterado en el payload y la verificación falla.
Notas de seguridad:
- Siempre usa HTTPS en producción — los JWTs son tokens bearer; interceptar uno es catastrófico.
- Almacena la clave de firma en una variable de entorno o gestor de secretos (AWS Secrets Manager, HashiCorp Vault, etc.).
- Prefiere tiempos de expiración cortos (5–15 minutos) y emite tokens de refresco para sesiones más largas.
- Usa
HS256solo cuando un único servicio firma y verifica. En cuanto un segundo servicio necesite verificar, cambia aRS256asimétrico con un par de claves pública/privada.
Variantes
Flujo de Refresh Token
Los tokens de acceso se mantienen de corta duración — el rango habitual es de 5 a 15 minutos. Cuando expiran, el cliente envía el refresh token para obtener un nuevo token de acceso sin re-autenticarse.
import jwt
import secrets
from datetime import datetime, timedelta, UTC
SECRET = "your-256-bit-secret"
ALGORITHM = "HS256"
REFRESH_TTL_DAYS = 7
# Almacena refresh tokens en una base de datos o Redis indexados por user ID
refresh_store = {} # En producción, usa Redis o una base de datos
def create_access_token(user_id: str) -> str:
payload = {
"sub": user_id,
"type": "access",
"iat": datetime.now(UTC),
"exp": datetime.now(UTC) + timedelta(minutes=15),
}
return jwt.encode(payload, SECRET, algorithm=ALGORITHM)
def create_refresh_token(user_id: str) -> str:
token_id = secrets.token_urlsafe(32)
payload = {
"sub": user_id,
"type": "refresh",
"jti": token_id,
"iat": datetime.now(UTC),
"exp": datetime.now(UTC) + timedelta(days=REFRESH_TTL_DAYS),
}
token = jwt.encode(payload, SECRET, algorithm=ALGORITHM)
refresh_store[token_id] = user_id
return token
def refresh_access_token(refresh_token: str) -> str:
try:
claims = jwt.decode(refresh_token, SECRET, algorithms=[ALGORITHM])
except jwt.ExpiredSignatureError:
raise ValueError("El refresh token ha expirado")
except jwt.InvalidTokenError:
raise ValueError("Refresh token inválido")
if claims.get("type") != "refresh":
raise ValueError("No es un refresh token")
token_id = claims.get("jti")
if token_id not in refresh_store:
raise ValueError("El refresh token ha sido revocado")
return create_access_token(claims["sub"])
def revoke_refresh_token(refresh_token: str) -> None:
try:
claims = jwt.decode(refresh_token, SECRET, algorithms=[ALGORITHM])
except jwt.InvalidTokenError:
return
token_id = claims.get("jti")
if token_id and token_id in refresh_store:
del refresh_store[token_id]
# Uso
access = create_access_token("user-123")
refresh = create_refresh_token("user-123")
# Cuando el access token expira, obtén uno nuevo
new_access = refresh_access_token(refresh)
# En logout, revoca el refresh token
revoke_refresh_token(refresh)
Variante RS256 (Asimétrico)
Cuando más de un servicio necesita verificar tokens, usa RS256 para que cada uno solo necesite la clave pública.
from cryptography.hazmat.primitives import serialization
import jwt
from datetime import datetime, timedelta, UTC
# Cargar clave privada (solo en el servicio de auth)
with open("private.pem", "rb") as f:
private_key = serialization.load_pem_private_key(f, password=None)
# Cargar clave pública (en cada servicio que verifica)
with open("public.pem", "rb") as f:
public_key = serialization.load_pem_public_key(f)
def create_token_rs256(user_id: str) -> str:
payload = {"sub": user_id, "exp": datetime.now(UTC) + timedelta(minutes=15)}
return jwt.encode(payload, private_key, algorithm="RS256")
def verify_token_rs256(token: str) -> dict:
return jwt.decode(token, public_key, algorithms=["RS256"])
const jwt = require('jsonwebtoken');
const fs = require('fs');
const privateKey = fs.readFileSync('private.pem');
const publicKey = fs.readFileSync('public.pem');
function createTokenRS256(userId) {
return jwt.sign({ sub: userId }, privateKey, { algorithm: 'RS256', expiresIn: '15m' });
}
function verifyTokenRS256(token) {
return jwt.verify(token, publicKey, { algorithms: ['RS256'] });
}
Lista de Bloqueo de Tokens con Redis
import redis
import jwt
from datetime import datetime, UTC
r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)
def verify_with_blocklist(token: str, secret: str) -> dict:
claims = jwt.decode(token, secret, algorithms=["HS256"])
jti = claims.get("jti")
if jti and r.exists(f"blocklist:{jti}"):
raise ValueError("El token ha sido revocado")
return claims
def revoke_token(token: str, secret: str) -> None:
claims = jwt.decode(token, secret, algorithms=["HS256"])
jti = claims.get("jti")
if jti:
# exp es un timestamp UNIX; conserva la entrada solo hasta expirar
ttl = int(claims["exp"] - datetime.now(UTC).timestamp())
if ttl > 0:
r.setex(f"blocklist:{jti}", ttl, "revoked")
Troubleshooting
Signature verification failed: secreto/clave incorrecta, o desajuste de algoritmo — un verificador que espera RS256 rechazando un token HS256. Lista explícitamente los algoritmos aceptados en cada verificador.Token has expireden tokens que parecen válidos: clock skew entre servicios. Permite unleewaypequeño (30–60s) enjwt.decode/jwt.verify, y mantén los servidores con NTP.- HTTP 431 Request Header Fields Too Large: el JWT creció demasiado para el presupuesto de headers de la petición (típico al meter muchos roles/claims). Recorta claims o acorta el token.
- Replay de refresh token: un refresh token robado permite emitir access tokens indefinidamente. Rota los refresh tokens en cada uso e invalida el
jtianterior — el patrón del flujo de refresh de arriba es donde aplicarlo.
Lo Que Funciona
- Rota claves regularmente: implementa una versión de clave (
kiden el header) para rotar sin invalidar todos los tokens activos. - Usa tokens de refresco: almacénalos en cookies
HttpOnly,Secure,SameSite=Stricty mantén los tokens de acceso solo en memoria — nada legible desde JS. - Valida el algoritmo: fija la lista permitida (
algorithms=['HS256']) — si no, un token falsificado conalg: nonepasa sin problemas. - Nunca pongas secretos en el payload: los JWTs están codificados en base64, no encriptados. Cualquiera puede leer el payload.
- Registra IDs de token, no tokens: si logueas eventos de autenticación, registra
jti(ID de token) osub, nunca la cadena completa del token.
Errores Comunes
- Meter secretos en el payload del JWT: cualquier dato sensible es legible por quien intercepte el token.
- Saltarse el chequeo del algoritmo: aceptar
alg: none, o dejar que el token elija su propio algoritmo, es la falsificación JWT clásica. - Tokens de vida infinita: tokens sin
expson peligrosos — si se filtran, son válidos para siempre. - Usar secretos débiles: un secreto corto deja la fuerza bruta de la firma HMAC al alcance de cualquiera.
- Confiar en que el cliente elimine tokens: aplica siempre la expiración del lado del servidor; los clientes pueden verse comprometidos.
Lecturas Adicionales
- RFC 7519 — JSON Web Token (JWT) — la especificación: claims, formatos de firma, JWS/JWE
- jwt.io debugger — pega un token e inspecciona header/payload/signature interactivamente
- Documentación de PyJWT — validación de claims,
leeway, opciones de algoritmo - OWASP JSON Web Token cheat sheet — checklist de hardening para Java/JWT
- Repo complementario — flujos de tokens ejecutables para Python, JavaScript y Java
Preguntas frecuentes
¿Debería almacenar JWTs en localStorage o cookies?
Los tokens de acceso viven en memoria (una variable). Los tokens de refresco van en cookies HttpOnly, Secure, SameSite=Strict para que XSS no pueda leerlos.
¿Cómo revoco un JWT antes de que expire?
Mantén una lista de bloqueo de tokens (ej. Redis con TTL igual a la expiración del token) y verifícala en cada validación. Alternativamente, mantén sesiones del lado del servidor.
¿Cuál es la diferencia entre HS256 y RS256?
HS256 es simétrico — el mismo secreto firma y verifica, así que quien lo tenga también puede falsificar. RS256 separa los roles: una clave privada firma y cualquier servicio con la clave pública solo puede verificar. Opta por RS256 en cuanto dos o más servicios verifiquen tokens — se firma con la clave privada y se verifica con la pública.
¿Cuánto debería durar un JWT access token?
Mantén los access tokens en 5–15 minutos; los refresh tokens suelen estar en el rango de 7–30 días según lo estricto que sea tu modelo de seguridad. Un access token corto hace que uno filtrado sea inútil en minutos.
¿Puedo usar JWTs con WebSockets?
Sí. Pasa el JWT como query parameter durante el handshake del WebSocket (wss://example.com/ws?token=...) o en el header Sec-WebSocket-Protocol. Valida el token antes de aceptar la conexión — la validación inicial es suficiente, no reenvíes tokens en mensajes regulares.
¿Qué claims debería incluir en un JWT?
Claims estándar: sub (user ID), iat (emitido en), exp (expiración), jti (ID único de token para revocación). Opcionales: iss (emisor), aud (audiencia), roles, scope. Evita PII o secretos en el payload — está codificado en base64, no encriptado.
¿Cómo roto claves de firma sin invalidar todos los tokens?
Usa el header claim kid (key ID). Durante la rotación, acepta tokens firmados con ambas claves (vieja y nueva) durante un período de transición. Una vez que todos los tokens viejos hayan expirado, elimina la clave vieja. Publica claves públicas vía un endpoint JWKS para RS256.
Recursos Relacionados
Implementar Gestión de Sesiones Segura
Cómo crear, validar y expirar sesiones de usuario de forma segura usando cookies HTTP-only, almacenamiento en Redis y protección CSRF.
RecipeInicio de Sesión OAuth 2.0
Cómo implementar autenticación OAuth 2.0 con Google, GitHub y otros proveedores. Cubre el flujo Authorization Code con PKCE, validación de state y refresh tokens.
RecipeCómo hashear contraseñas (Python, JavaScript, Java)
Aprendé a hashear y verificar contraseñas con bcrypt, Argon2 y PBKDF2. Ejemplos prácticos en Python, JavaScript y Java, más pasos de migración y trade-offs de parámetros.
RecipeAutenticación Segura con API Keys para Servicios y Clientes
Cómo generar, distribuir, validar y rotar API keys para autenticación machine-to-machine usando firmas HMAC, scopes y políticas de rate limiting.
RecipeAutenticación de Dos Factores (2FA / TOTP)
Cómo implementar autenticación de dos factores con contraseñas de un solo uso basadas en tiempo (TOTP) para login seguro.
GuideGuía de Mejores Prácticas de Seguridad
Una Referencia Detallada de seguridad de aplicaciones: autenticación, autorización, validación de inputs, gestión de secretos y prevención de vulnerabilidades comunes.