StackPractices
intermediate Por Mathias Paulenko

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 HS256 solo cuando un único servicio firma y verifica. En cuanto un segundo servicio necesite verificar, cambia a RS256 asimé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.

sequenceDiagram diagram: participant C as Cliente
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 expired en tokens que parecen válidos: clock skew entre servicios. Permite un leeway pequeño (30–60s) en jwt.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 jti anterior — el patrón del flujo de refresh de arriba es donde aplicarlo.

Lo Que Funciona

  • Rota claves regularmente: implementa una versión de clave (kid en el header) para rotar sin invalidar todos los tokens activos.
  • Usa tokens de refresco: almacénalos en cookies HttpOnly, Secure, SameSite=Strict y 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 con alg: none pasa 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) o sub, 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 exp son 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

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.