Tokens portadores opacos
Una tarjeta de acceso te deja entrar a un edificio de oficinas. No tiene nombre, no tiene foto, y no tiene nada legible adentro. La acercas, un lector la verifica contra una lista, y la puerta se abre o no se abre.
Un token portador opaco es esa tarjeta. Una cadena aleatoria e imposible de adivinar, sin nada adentro que se pueda leer.
"Sin estado" no significa que el servidor olvide todo
La palabra confunde a la gente, así que vale la pena aclararla primero.
Que algo sea "sin estado" significa que el servidor no mantiene ninguna sesión: ningún objeto que rastree a un usuario entre solicitudes. El servidor sigue teniendo una base de datos, sigue teniendo usuarios, sigue almacenando cosas. Lo que deja de mantener es un registro de que esta persona está conectada en este momento.
La solicitud tiene que traer todo lo que el servidor necesita para determinar quién la está haciendo.
El servidor deja de recordarte a ti; el cliente empieza a demostrar quién es cada vez.
Dos palabras explican el nombre. Opaco significa que el servidor no puede leer nada dentro del token, porque no hay nada ahí adentro. No está codificado ni estructurado, es una cadena aleatoria. Portador significa que quien lo tiene en su poder, lo usa.
Lo único que deja de mantener es una nota que diga que estás conectado en este momento. Esa nota es lo que el token reemplaza.
El flujo
Seis pasos, y la forma te resultará familiar:
- El usuario envía un nombre de usuario y una contraseña.
- El servidor los verifica.
- Si son correctos, el servidor genera un token aleatorio.
- El servidor guarda una pequeña asociación de token a usuario, y luego envía el token al cliente.
- El cliente lo guarda y lo adjunta a cada solicitud posterior en el encabezado
Authorization. - El servidor busca el token, encuentra al usuario, y la solicitud queda autenticada.
GET /api/orders
Authorization: Bearer 7f3a9c1e5b28d4a06e91f7c3Comparado con sesiones y cookies, las diferencias se ubican en tres lugares:
| Sesión con estado | Token portador opaco | |
|---|---|---|
| El servidor almacena | Un objeto de sesión completo | Una fila de token a usuario |
| El cliente almacena | Una cookie con un id de sesión | El token mismo |
| Viaja | Automáticamente, gracias al navegador | Explícitamente, en código que tú escribes |
| Le conviene a | Aplicaciones web tradicionales | APIs y apps móviles |
Esa tercera fila importa más de lo que parece. Una cookie viaja sola sin que nadie la pida. Un token portador lo adjunta tu propio código de cliente, en cada solicitud, de forma deliberada.
La diferencia está en cuánto guarda el servidor y quién se encarga de enviarlo. El mismo esqueleto, con el peso repartido de otra forma.
Cinco formas en que esto falla
Casi ningún riesgo está en cómo funciona el modelo. Está en cómo se guarda el token, cuánto dura, y qué tan bien se cuida la tabla de búsqueda.
| Qué falla | Por qué importa | La solución | |
|---|---|---|---|
| 1 | Almacenamiento inseguro | Un token en localStorage, en una variable simple o en una cookie sin protección puede leerlo cualquier script en la página. Un solo bug de XSS y el atacante queda indistinguible del usuario | Guárdalo en un lugar más difícil de alcanzar, y limita cuánto se expone |
| 2 | Tokens de vida larga | Un token válido por semanas significa que un token filtrado sigue siendo válido por semanas | Ventanas de expiración cortas |
| 3 | Sin revocación | Los tokens no expiran solos ni dejan de funcionar al cerrar sesión. Una expiración que nadie verifica es solo decoración | Verifica la expiración en cada solicitud, elimínalo al cerrar sesión |
| 4 | Acumulación en la tabla de búsqueda | Cada inicio de sesión escribe una fila, así que las filas se acumulan y las búsquedas se vuelven lentas. Un atacante puede forzar esto bombardeando el endpoint de inicio de sesión, lo que constituye una denegación de servicio | Una tarea programada que elimine los tokens expirados |
| 5 | Una tabla de búsqueda manipulada | La asociación es la fuente de verdad. El acceso de escritura mediante inyección SQL o credenciales filtradas puede redirigir un token hacia otro usuario | Protege la base de datos y las herramientas administrativas con el mismo cuidado que la aplicación |
El problema 5 es el que vale la pena analizar con calma. Nada cambia en el token en sí; lo que cambia es lo que significa. Eso es una elevación de privilegios lograda sin tocar la credencial para nada.
El token está bien. Todo lo que lo rodea es donde está el verdadero trabajo.
Ponlo en práctica
Una API emite tokens así:
const token = Math.random().toString(36).slice(2)
tokens[token] = { userId: 4821 }
res.json({ token })Y los verifica así:
const token = req.headers.authorization?.replace('Bearer ', '')
const entry = tokens[token]
if (!entry) return res.status(401).json({ error: 'Unauthorized' })
req.userId = entry.userIdEncuentra cuatro problemas.
Compara tus respuestas
1. Math.random no es criptográficamente seguro. Su resultado se puede predecir a partir de valores anteriores, así que un atacante que reúna algunos tokens puede deducir otros. crypto.randomBytes(32).toString('hex') es la solución, y solo este problema ya hace que todo el esquema se pueda falsificar.
2. No hay expiración en ningún lado. No se guarda nada sobre cuándo se emitió el token y nada lo verifica al llegar, así que todo token emitido es válido para siempre. Guarda una fecha de expiración y verifícala en cada solicitud.
3. Los tokens se guardan en texto plano. tokens[token] conserva la cadena original, así que cualquiera que lea ese almacenamiento puede usar cada token que contiene. Guarda un hash y busca aplicando el hash a lo que llega.
4. tokens es un objeto simple, así que la búsqueda es permeable. tokens['toString'] devuelve Object.prototype.toString, un valor verdadero (truthy), así que una solicitud con el token literal toString pasa la verificación if (!entry) sin problema.
entry.userId termina siendo undefined. Que esto se convierta en un error o en una solicitud autenticada como el usuario undefined depende del código que viene después. Usa un Map, Object.create(null) u Object.hasOwn().
El número 4 es el que parece inofensivo a primera vista. Los otros tres son cosas que hay que recordar; ese último es una propiedad de los objetos de JavaScript que ha aparecido en sistemas reales.
Hacia dónde va esto
La tabla de búsqueda es lo que hace viable este modelo y también lo que lo limita. Te da capacidad de revocación, pero significa que cada solicitud toca almacenamiento compartido, justo lo que la identidad sin estado se suponía que iba a evitar.
Los JSON Web Tokens llevan la idea hasta su conclusión eliminando la búsqueda por completo, y colocando la identidad dentro del propio token.

