Authentication

JWT

Un JSON Web Token es una cadena firmada y segura para URL que transporta claims sobre un usuario. Compacto y autónomo, escala perfectamente, aunque es fácil cometer errores en su implementación.

intermediate15 min readUpdated 16 sept 2026
verify.ts
ts
// verify.ts
import { jwtVerify, type JWTPayload } from "jose";

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

export async function verify(token: string): Promise<JWTPayload> {
  const { payload } = await jwtVerify(token, secret, {
    issuer: "https://auth.example.com",
    audience: "api.example.com",
    algorithms: ["HS256"],
  });
  return payload;
}
Primera especificación
2015
Codificación
Base64url
Algoritmo por defecto
HS256
Opción asimétrica
RS256 / ES256
Variante cifrada
JWE

Por que importa

Ventajas de usar un JWT

Credenciales autónomas

Cada claim que un servicio necesita viaja dentro del token, por lo que la validación es una operación local sin necesidad de consultas a la base de datos en el flujo crítico.

Evidencia de manipulación por diseño

La firma cubre el header y el payload, por lo que cambiar un solo carácter invalida el token y la verificación falla inmediatamente.

Ideal para la rotación

Combinar access tokens de vida corta con refresh tokens rotativos limita el daño de una filtración y proporciona una vía para la revocación.

La imagen completa

Las tres capas de un token

Un header define el algoritmo, un payload transporta los claims y una firma sella ambos contra manipulaciones.

Header

Declarar

Un pequeño objeto JSON que indica el algoritmo de firma y el ID de la clave, permitiendo que los verificadores busquen la clave correcta durante la rotación.

Payload

Transportar

Un objeto JSON con claims registrados y personalizados como sub, exp, iss, aud, scope y role.

Signature

Sellar

Un MAC criptográfico o firma sobre el header y payload codificados que hace que el token sea resistente a manipulaciones.

HTML5 de un vistazo

Dentro del token

Header

alg, typ y kid describen cómo fue firmado el token.

Claims

Nombres registrados como iss, sub, aud y exp, además de tus propios campos.

Signature

Resultado de HMAC o RSA/ECDSA que la verificación recalcula y compara.

Expiry

El claim exp limita la ventana de tiempo en la que un token robado es útil.

Rotation

Los refresh tokens son de un solo uso y se reemplazan en cada intercambio.

Verification

Fija el algoritmo y verifica iss, aud y exp antes de confiar en cualquier dato.

Flujo

Cómo se verifica un JWT

Cada solicitud protegida sigue el mismo camino. Cualquier verificación fallida termina en un 401.

  1. 1

    Extraer el token

    Leer el header Authorization y requerir el esquema Bearer. Rechazar la solicitud si no hay un token presente.

  2. 2

    Dividir el token

    Dividir la cadena en los puntos para obtener header, payload y firma. Un token sin exactamente tres partes está mal formado.

  3. 3

    Verificar la firma

    Recalcular la firma sobre las dos primeras partes usando la clave y el algoritmo fijado. Una discrepancia significa que el token fue manipulado.

  4. 4

    Validar los claims

    Verificar exp y nbf para el tiempo, iss para el emisor esperado y aud para la audiencia prevista. Un token de staging no debe pasar en producción.

  5. 5

    Adjuntar el principal

    Si tiene éxito, colocar el subject y el scope en la solicitud para que los manejadores posteriores puedan autorizar sin volver a parsear el token.

  6. 6

    Rechazar con 401

    Si cualquier paso falla, devolver un 401 con un error genérico. No expliques qué verificación falló a un cliente no confiable.

Una breve historia

Cómo los JWT se convirtieron en el estándar

  1. 2011

    Aparece el borrador de JWT

    El grupo de trabajo de OAuth propone un formato de token compacto para transportar claims entre servicios.

    11
  2. 2015

    RFC 7519 estandariza JWT

    JWT se publica junto con JWS, JWE, JWK y JWA, otorgando al formato una especificación estable.

    15
  3. 2015

    OpenID Connect lo adopta

    Los ID tokens se definen como JWTs, convirtiendo el formato en el estándar para los proveedores de identidad.

    15
  4. 2015

    Llegan los ataques clásicos

    Investigadores documentan "alg: none" y la confusión HS/RS, obligando a los verificadores a fijar los algoritmos.

    15
  5. 2020

    La rotación se vuelve la norma

    Los access tokens cortos y los refresh tokens rotativos reemplazan a los tokens de larga duración en la práctica común.

    20

La guia completa

JWT: Todo lo que necesitas saber

Qué es realmente un JWT

Un JSON Web Token es una cadena compacta y segura para URL que codifica un conjunto de claims junto con una firma sobre ellos. Está definido por el RFC 7519 y se basa en dos especificaciones complementarias: JSON Web Signature (JWS) para la firma y JSON Web Encryption (JWE) para la confidencialidad. Casi todos los JWT que encontrarás en la práctica son JWS.

El token consta de tres partes codificadas en Base64url y separadas por puntos: header, payload y signature. Es autocontenido, lo que significa que todo lo que un servidor necesita para tomar una decisión viaja con la solicitud. Validarlo no requiere consultas a la base de datos, ni un almacén de sesiones compartido, ni llamadas al emisor.

Anatomy of a JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzQyIiwic2NvcGUiOiJyZWFkOnBvc3RzIiwiZXhwIjoxNzYwMDAwMDAwfQ.3vJ1o0Q8m4nQ7bKc9xP2sT6wYqL0dRfH8uZgWmA1eJk
headerthe algorithm and token type, Base64url
payloadthe claims, also Base64url and readable by anyone
signatureHMAC over the first two parts, proving they were not changed

Vale la pena ser precisos sobre qué es lo que esto demuestra. Un JWT demuestra que quien lo emitió firmó exactamente ese payload. No demuestra que el token estuviera destinado a ti a menos que verifiques la audiencia. No demuestra que el usuario todavía tenga permiso para actuar a menos que verifiques los scopes y roles. Y no oculta nada a menos que utilices JWE. Cada propiedad de seguridad que te interese debe ser verificada explícitamente.

Las tres partes, decodificadas

El header es un pequeño objeto JSON que indica el algoritmo de firma y el tipo de token. El campo alg es el más crítico. typ es casi siempre JWT, y kid identifica qué clave se utilizó para que los verificadores puedan encontrar la correcta durante la rotación de claves.

{ "alg": "HS256", "typ": "JWT", "kid": "2026-09" }

El payload es un objeto JSON de claims. Los registered claims tienen nombres estándar definidos por la especificación: iss para el emisor (issuer), sub para el sujeto (subject), aud para la audiencia (audience), exp para la expiración (expiration), nbf para el “no antes de” (not before), iat para la fecha de emisión (issued at), y jti para un ID de token único. Todo lo demás es un custom claim que tú defines.

{
  "iss": "https://auth.example.com",
  "sub": "user_42",
  "aud": "api.example.com",
  "exp": 1760000000,
  "iat": 1759999100,
  "scope": "read:posts write:posts",
  "role": "editor"
}

La signature se calcula sobre el Base64url del header y el payload. Si cambias un solo carácter en cualquiera de los dos, la firma recalculada no coincidirá. Esta es la propiedad que hace que un JWT sea seguro para entregarlo a un cliente no confiable, y es lo único que separa un claim de una falsificación.

Firmados, no cifrados

El malentendido más común sobre los JWT es que son secretos. No lo son. El payload está en Base64url, que es una codificación, no un cifrado. Cualquier persona que posea el token puede decodificar cada claim con una sola línea de código.

const [, payload] = token.split(".");
console.log(JSON.parse(atob(payload)));
// { sub: "user_42", role: "editor", scope: "read:posts" }

Esto tiene dos consecuencias. Primero, nunca pongas secretos, contraseñas o datos personales en un JWT que no te sentirías cómodo mostrando al cliente. Segundo, trata el token en sí mismo como una credencial: poseerlo es suficiente para actuar como el sujeto, razón por la cual el almacenamiento y el transporte son tan importantes más adelante en esta guía.

Si realmente necesitas ocultar el payload al cliente, utiliza JSON Web Encryption y una librería que lo soporte. Para la gran mayoría de los sistemas, un JWS firmado sobre TLS es la respuesta correcta, y añadir cifrado solo añade una pieza móvil más al sistema.

Algoritmos de firma: HS256 vs RS256

Dos familias de algoritmos dominan los despliegues reales.

HS256 es HMAC con SHA-256. El mismo secreto se utiliza para firmar y verificar. Es rápido, sencillo e ideal cuando un único servicio tanto emite como verifica sus propios tokens.

import { SignJWT, jwtVerify } from "jose";

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

const token = await new SignJWT({ role: "editor" })
  .setProtectedHeader({ alg: "HS256" })
  .setSubject("user_42")
  .setExpirationTime("15m")
  .sign(secret);

await jwtVerify(token, secret, { algorithms: ["HS256"] });

RS256 es RSA con SHA-256, y ES256 es ECDSA sobre una curva prima. El emisor posee una clave privada y firma; todos los demás poseen la clave pública y verifican. Esa asimetría es la razón por la cual los sistemas grandes lo prefieren: un servidor de recursos puede verificar tokens sin poder emitirlos. ES256 ofrece la misma garantía con claves y firmas mucho más pequeñas.

import { importPKCS8, importSPKI, SignJWT, jwtVerify } from "jose";

const privateKey = await importPKCS8(process.env.JWT_PRIVATE_KEY!, "RS256");
const publicKey = await importSPKI(process.env.JWT_PUBLIC_KEY!, "RS256");

const token = await new SignJWT({ role: "editor" })
  .setProtectedHeader({ alg: "RS256", kid: "2026-09" })
  .setSubject("user_42")
  .setExpirationTime("15m")
  .sign(privateKey);

await jwtVerify(token, publicKey, {
  algorithms: ["RS256"],
  issuer: "https://auth.example.com",
  audience: "api.example.com",
});

La regla que importa más que la elección: el verificador debe fijar el algoritmo esperado. Nunca permitas que el propio header del token seleccione cómo se debe verificar.

Claims: registrados y personalizados

Los registered claims están reservados por la especificación y son reconocidos por cualquier librería seria.

  • iss — quién emitió el token. Verifícalo para rechazar tokens provenientes de otro entorno.
  • sub — a quién pertenece el token, generalmente el id del usuario.
  • aud — para quién es el token. Un token emitido para tu API pública no debería ser aceptado por tu API de administración.
  • exp — cuándo expira el token, como un Unix timestamp. Configúralo siempre.
  • nbf — no antes de. Rara vez es necesario, pero es útil para despliegues graduales (staged rollouts).
  • iat — cuándo fue emitido. Útil para comprobaciones de edad máxima y depuración.
  • jti — un id único para este token, utilizado para la detección de replay y listas de denegación.

Los custom claims transportan datos de la aplicación: scope, role, tenant_id, email. Mantenlos pequeños y sin información sensible. Un token viaja en cada solicitud, por lo que un payload inflado es un impuesto permanente al ancho de banda y a la latencia.

Existe una fuerte tentación de incluir todo el perfil del usuario en el token para evitar una lectura en la base de datos. Resiste esa tentación. Los claims quedan obsoletos en el momento en que cambia un rol, y no puedes anular la emisión de un token que el cliente ya posee. Incluye solo lo que el verificador realmente necesite y consulta el resto en la base de datos.

Creación y verificación de tokens

En Node, jose es la opción moderna. Está basada en promesas, funciona en cualquier runtime incluyendo Cloudflare Workers y Deno, y expone una API pequeña y meticulosa. jsonwebtoken es la librería más antigua, basada en callbacks, y sigue siendo común en el código existente.

import { SignJWT } from "jose";

export async function issueAccessToken(userId: string, scope: string) {
  return new SignJWT({ scope })
    .setProtectedHeader({ alg: "HS256", typ: "JWT" })
    .setSubject(userId)
    .setIssuer("https://auth.example.com")
    .setAudience("api.example.com")
    .setIssuedAt()
    .setExpirationTime("15m")
    .setJti(crypto.randomUUID())
    .sign(new TextEncoder().encode(process.env.JWT_SECRET));
}

La verificación es donde reside la seguridad. Un verificador debe comprobar la firma, fijar el algoritmo y validar exp, iss y aud. Una librería decodificará alegremente un token sin verificarlo, y ese payload decodificado es una entrada controlada por el atacante.

import { jwtVerify, type JWTPayload } from "jose";

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

export async function verifyAccessToken(token: string): Promise<JWTPayload> {
  const { payload } = await jwtVerify(token, secret, {
    algorithms: ["HS256"],
    issuer: "https://auth.example.com",
    audience: "api.example.com",
  });
  return payload;
}

Ten en cuenta que jwtVerify aplica exp y nbf automáticamente, pero solo comprueba iss y aud cuando se los pasas. Omitirlos es una vulnerabilidad real: un token emitido para un servicio de staging sería aceptado por producción, y un token emitido para una aplicación diferente también sería aceptado.

Tokens de acceso y de refresco

Un único token de larga duración es conveniente, pero peligroso. La solución estándar es utilizar un par de tokens con diferentes tiempos de vida y diferentes audiencias.

  • El access token es de corta duración, típicamente de 5 a 15 minutos. Se envía en cada llamada a la API y es el único token que el servidor de recursos llega a ver.
  • El refresh token es de larga duración, desde días hasta meses. Se envía únicamente al servidor de autorización para obtener un nuevo access token.

Esta división limita el daño. Si un access token se filtra, quedará inservible en cuestión de minutos. El refresh token, que es mucho más sensible, nunca viaja hacia los servidores de recursos y puede ser rotado y revocado, que es donde reside el control real.

Rotación de refresh tokens

La rotación significa que cada solicitud de refresco emite un nuevo refresh token e invalida el anterior. Si un atacante roba un refresh token y lo utiliza, el cliente legítimo presentará más tarde un token que ya ha sido usado; en ese momento, el servidor puede detectar la reutilización y revocar toda la familia de tokens.

import { randomUUID } from "node:crypto";

export async function rotateRefreshToken(presented: string) {
  const stored = await db.refreshToken.findUnique({ where: { token: presented } });

  if (!stored || stored.revokedAt || stored.expiresAt < new Date()) {
    if (stored) await revokeFamily(stored.familyId);
    throw new Error("invalid_refresh_token");
  }

  await db.refreshToken.update({
    where: { id: stored.id },
    data: { revokedAt: new Date(), replacedBy: randomUUID() },
  });

  return issueTokenPair(stored.userId, stored.familyId);
}

Almacena los refresh tokens mediante hashing, exactamente igual que lo harías con las contraseñas. Una filtración de la base de datos no debería entregar credenciales activas a un atacante, ya que un refresh token es, en esencia, una contraseña que permite saltarse el formulario de inicio de sesión.

Dónde almacenar tokens en el navegador

No existe un lugar perfecto, solo compromisos entre cross-site scripting y cross-site request forgery.

  • localStorage es legible por cualquier script en la página. Un solo bug de XSS y el atacante exfiltra todos los tokens. Este es el error que sigue apareciendo en los reportes de brechas de seguridad.
  • En memoria, mediante una variable en un módulo, es seguro frente a XSS persistente pero se pierde al recargar la página, por lo que generalmente se combina con un refresh token en una cookie httpOnly.
  • Una cookie httpOnly, Secure y SameSite es ilegible para JavaScript, lo que neutraliza el robo de tokens vía XSS. Esto reintroduce el CSRF, el cual se mitiga mediante SameSite=Lax o Strict junto con un token CSRF.

Para una aplicación de navegador, la opción pragmática por defecto es un access token de corta duración mantenido en memoria y un refresh token rotativo en una cookie httpOnly limitada al endpoint de refresco. Los clientes nativos y del lado del servidor no tienen esta restricción y pueden guardar los tokens en almacenamiento seguro o simplemente en memoria.

El compromiso de la ausencia de estado y la revocación

El principal atractivo de los JWT es que son stateless (sin estado). Cualquier servidor puede verificar un token sin necesidad de un estado compartido, lo que permite un escalado excepcional entre regiones y hace que el escalado horizontal sea trivial. El coste es que la revocación es genuinamente difícil. Un token firmado es válido hasta que expira, independientemente de si has eliminado al usuario, cambiado su rol o cerrado su sesión. No existe un registro central que se pueda borrar.

Puedes recuperar parte del control de la siguiente manera:

  • Mantén los access tokens con una duración corta para que la ventana de revocación sea de minutos en lugar de días.
  • Mantén una deny-list de valores jti para los casos excepcionales de cierre de sesión inmediato, verificándola en cada solicitud. Esto reintroduce el estado, así que mantenla pequeña y con tiempo de expiración.
  • Añade un claim de token_version por usuario y rechaza los tokens cuya versión esté obsoleta. Cambiar la contraseña o el rol incrementaría este valor.

Este es el resumen honesto: los JWT sacrifican la facilidad de revocación en favor de la facilidad de escalado. Si necesitas revocación instantánea en todas partes, las cookies de sesión respaldadas por un almacén pueden encajar mejor, como se explica en Session Auth.

Confusión de algoritmos y alg none

Hay dos ataques lo suficientemente antiguos como para ser de libro de texto y, aun así, seguir encontrando víctimas.

El primero es alg: none. Un atacante edita el header a {"alg":"none"} y elimina la firma. Un verificador ingenuo que confía en el header acepta el token falsificado. El segundo es la confusión HS/RS. Un servicio que espera tokens RS256 es engañado para aceptar un token HS256 firmado con la clave pública RSA, la cual es pública por definición.

Ambos tienen la misma solución: el verificador decide el algoritmo, no el token.

// Good: the verifier decides.
await jwtVerify(token, secret, { algorithms: ["HS256"] });

// Bad: the token decides.
const { header } = decodeProtectedHeader(token);
await jwtVerify(token, secret, { algorithms: [header.alg] });

Rechaza alg: none rotundamente, nunca derives la clave de una fuente no confiable y trata el header como datos, no como instrucciones.

Scopes y autorización

La autenticación responde a quién es el usuario, y los scopes responden a qué puede hacer. Un claim de scope es una lista de permisos delimitados por espacios, y el middleware lo verifica antes de que se ejecute un handler.

export function requireScope(required: string) {
  return (req, res, next) => {
    const granted = String(req.user.scope ?? "").split(" ");
    if (!granted.includes(required)) {
      return res.status(403).json({ error: "insufficient_scope" });
    }
    next();
  };
}

Mantén los scopes generales y estables, y haz que se cumplan en el servidor. Un token sin el scope correcto debe fallar con un 403, no un 401: el emisor está autenticado, simplemente no tiene permiso. Para modelos de acceso más complejos basados en roles y atributos, consulta RBAC.

JWT vs tokens opacos

Un JWT es un bearer token que lleva su propia validación. Un token opaco es una cadena aleatoria sin significado propio, por lo que el servidor debe consultarlo para obtener cualquier información.

Los tokens opacos ganan en términos de revocación y privacidad. Puedes eliminar la sesión al instante y el token no revela nada si llega a filtrarse. A cambio, requieren un viaje de ida y vuelta a la base de datos o caché en cada solicitud. Los JWT ganan en escalabilidad e independencia. Los servicios los verifican localmente y no necesitan un almacenamiento compartido, a costa de tener una ventana de tiempo antes de que la revocación sea efectiva.

Muchos sistemas en producción utilizan ambos: un access token en formato JWT para ganar velocidad y un refresh token opaco para mantener el control. Este modelo híbrido es el que implementan la mayoría de los proveedores de identidad hoy en día, y es una excelente opción por defecto cuando no sabes cuál elegir.

Rotación de claves con kid

Las claves de firma no deben ser eternas. La rotación limita el daño de una clave comprometida, y el campo de cabecera kid es lo que hace que la rotación sea invisible para los clientes: el verificador lee kid, selecciona la clave correspondiente y comprueba la firma.

import { SignJWT, jwtVerify, createLocalJWKSet } from "jose";

const jwks = createLocalJWKSet({
  keys: [{ kty: "oct", kid: "2026-09", k: process.env.JWT_SECRET }],
});

// The verifier resolves the key from the header's kid.
await jwtVerify(token, jwks, { algorithms: ["HS256"] });

Cuando realices una rotación, publica la nueva clave junto a la antigua, firma los nuevos tokens con el nuevo kid y sigue verificando la clave antigua hasta que todos los tokens firmados con ella hayan expirado. Si la eliminas demasiado pronto, cerrarás la sesión de todos los usuarios activos a la vez. En el caso de claves asimétricas, publica un documento JWKS en una URL conocida y permite que los servidores de recursos lo almacenen en caché.

Duración de los tokens en la práctica

La expiración es un equilibrio entre seguridad y conveniencia. No existe una respuesta universal correcta, pero el esquema de un valor predeterminado sensato es consistente.

  • Access tokens: de 5 a 15 minutos. Lo suficientemente cortos para que una filtración pierda valor rápidamente, y lo suficientemente largos para no tener que refrescar en cada solicitud.
  • Refresh tokens: de 7 a 30 días, con rotación y una ventana deslizante (sliding window). Lo suficientemente duraderos para mantener a los usuarios conectados, pero lo suficientemente cortos para que un token abandonado eventualmente expire.
  • Límite absoluto de sesión: de 30 a 90 días. Una edad máxima tras la cual el usuario debe autenticarse de nuevo, independientemente de con qué frecuencia refresque el token.

Si tus usuarios se quejan de que se cierra la sesión, la solución es un silent refresh más fluido, no un access token más largo. Un access token de 24 horas es una falla de revocación esperando a suceder.

Depurar un token sin confiar en él

Cuando una solicitud falla con un error 401, es probable que quieras ver los claims del token sin debilitar la verificación. Decodificar el token es seguro siempre y cuando trates el resultado como datos no confiables.

import { decodeJwt, decodeProtectedHeader } from "jose";

const header = decodeProtectedHeader(token);
const claims = decodeJwt(token);

console.log({ alg: header.alg, kid: header.kid });
console.log({
  sub: claims.sub,
  iss: claims.iss,
  aud: claims.aud,
  exp: new Date((claims.exp ?? 0) * 1000).toISOString(),
  expired: (claims.exp ?? 0) * 1000 < Date.now(),
});

Los dos fallos que verás con más frecuencia son un desajuste de aud tras renombrar un servicio y un desajuste de iss al cambiar entre entornos. Ambos son problemas de configuración y ambos son invisibles hasta que imprimes los claims.

Probando tokens

Deberías poder probar una ruta autenticada sin necesidad de levantar un proveedor de identidad. Dado que un JWT es simplemente una cadena firmada, basta con un helper de prueba que firme un token con el mismo secreto de prueba.

import { SignJWT } from "jose";
import request from "supertest";
import app from "../app.js";

const secret = new TextEncoder().encode("test-secret");

async function tokenFor(scope = "read:posts") {
  return new SignJWT({ scope })
    .setProtectedHeader({ alg: "HS256" })
    .setSubject("user_1")
    .setIssuer("https://auth.example.com")
    .setAudience("api.example.com")
    .setExpirationTime("5m")
    .sign(secret);
}

test("rejects a request without a token", async () => {
  await request(app).get("/posts").expect(401);
});

test("accepts a request with a valid token", async () => {
  const token = await tokenFor();
  await request(app)
    .get("/posts")
    .set("authorization", `Bearer ${token}`)
    .expect(200);
});

Prueba también los casos negativos: un token expirado, un token con el audience incorrecto y un token firmado con una clave diferente. Esas son las comprobaciones que te protegen, por lo que merecen tener cobertura tanto como el camino feliz (happy path).

Mejores prácticas

  • Configura siempre exp; mantén los tokens de acceso en 15 minutos o menos.
  • Fija el algoritmo en el verificador y rechaza alg: none.
  • Valida iss, aud, exp y nbf; nunca confíes en un claim que no hayas comprobado.
  • Mantén los secretos y las claves privadas fuera del control de versiones y cárgalos desde un gestor de secretos.
  • Almacena los refresh tokens con hash y rotalos en cada uso.
  • Mantén los claims pequeños y sin información sensible; el payload es legible.
  • En los navegadores, prefiere cookies httpOnly o memoria sobre localStorage.
  • Planifica la revocación mediante vidas útiles cortas, una deny-list de jti o una versión del token.
  • Usa jose para código nuevo; está basado en promesas y es portable entre runtimes.

Errores comunes

  • Asumir que el payload está cifrado solo porque parece texto sin sentido.
  • Leer los claims con decode y tratarlos como si estuvieran verificados.
  • Permitir que el header alg del token elija el algoritmo de verificación.
  • Aceptar un token sin verificar aud, permitiendo que tokens de staging funcionen en producción.
  • Usar un secreto débil o compartido, o subirlo al repositorio.
  • Establecer la expiración en 30 días porque refrescar el token es molesto.
  • Guardar los tokens en localStorage y dar el trabajo por terminado.
  • Incluir un rol en el token y nunca invalidarlo cuando el rol cambia.
  • Tratar un error 401 y un 403 como si fueran el mismo error.

Próximos pasos

Los JWT son solo una herramienta dentro de un conjunto más amplio de gestión de identidades. La guía de OAuth 2.0 muestra cómo se obtienen realmente los tokens mediante la autorización delegada, Session Auth cubre la alternativa basada en cookies para cuando necesitas una revocación instantánea, y API Keys explica las credenciales de larga duración para clientes automáticos. Si quieres ver este código de verificación implementado en un servidor real, lee la sección de Node.js.

En la practica

Emitir, verificar, rotar, inspeccionar

Las cuatro operaciones que escribirás primero, usando jose.

tokens.ts
import { SignJWT } from "jose";

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

export async function issueAccessToken(userId: string, scope: string) {
  return new SignJWT({ scope })
    .setProtectedHeader({ alg: "HS256", typ: "JWT" })
    .setSubject(userId)
    .setIssuer("https://auth.example.com")
    .setAudience("api.example.com")
    .setIssuedAt()
    .setExpirationTime("15m")
    .setJti(crypto.randomUUID())
    .sign(secret);
}

HS256 vs RS256

El simétrico es más simple cuando un solo servicio emite y verifica. El asimétrico vale la pena una vez que la verificación se distribuye entre varios servicios.

HS256
import { SignJWT, jwtVerify } from "jose";

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

const token = await new SignJWT({ role: "editor" })
  .setProtectedHeader({ alg: "HS256" })
  .setSubject("user_42")
  .setExpirationTime("15m")
  .sign(secret);

await jwtVerify(token, secret, { algorithms: ["HS256"] });
RS256
import { importPKCS8, importSPKI, SignJWT, jwtVerify } from "jose";

const privateKey = await importPKCS8(process.env.JWT_PRIVATE_KEY!, "RS256");
const publicKey = await importSPKI(process.env.JWT_PUBLIC_KEY!, "RS256");

const token = await new SignJWT({ role: "editor" })
  .setProtectedHeader({ alg: "RS256", kid: "2026-09" })
  .setSubject("user_42")
  .setExpirationTime("15m")
  .sign(privateKey);

await jwtVerify(token, publicKey, { algorithms: ["RS256"] });

Cookie httpOnly vs localStorage

JavaScript no puede leer una cookie httpOnly, lo que elimina la ruta más común desde un bug de XSS hasta la toma total de la cuenta.

Preferir
Set-Cookie: access_token=eyJhbGciOi...;
  HttpOnly;
  Secure;
  SameSite=Lax;
  Path=/;
  Max-Age=900
Evitar
localStorage.setItem("access_token", token);

// Any injected script can now read the token and
// exfiltrate it. Persistent XSS becomes account takeover.

Compromisos

¿Vale la pena la autenticación stateless?

Los JWT sacrifican la facilidad de revocación por la facilidad de escalado. Decide cuál necesita realmente tu producto.

Strengths

  • Sin almacenamiento de sesión compartido

    Cualquier instancia puede verificar un token con una clave que ya posee, por lo que el escalado horizontal y los despliegues multi-región se mantienen simples.

  • Compacto y portable

    Un solo header transporta identidad, scopes y expiración entre servicios, lenguajes y runtimes sin necesidad de una capa de traducción.

  • Ideal para comunicación entre servicios

    Los servicios independientes pueden verificar tokens localmente, eliminando una dependencia síncrona del servidor de autorización.

Trade-offs

  • La revocación es la parte difícil

    Un token firmado es válido hasta que expira. Cerrar la sesión de un usuario o revocar un rol no puede afectar a un token que ya está en manos del cliente.

  • El payload es público

    Base64url no es cifrado. Cualquier cosa que pongas en los claims es legible para el poseedor y para cualquiera que lo intercepte.

  • Errores pequeños tienen consecuencias graves

    Confiar en el algoritmo del header, omitir la verificación de la audiencia o guardar tokens en localStorage convierte una conveniencia en una brecha de seguridad.

Preguntas frecuentes

Preguntas frecuentes

Keep learning

Related topics from the roadmap.

$ comienza a aprender

Listo para aprender JWT (JSON Web Tokens)?

Nuestro tutorial interactivo te guia a traves de JWT (JSON Web Tokens) paso a paso — con quizzes y codigo real que puedes ejecutar en el navegador.