Rotación del refresh token
Cada renovación entrega un refresh token nuevo y anula el anterior: cómo guardar siempre el último, qué pasa si se reutiliza uno viejo y cómo evitar renovaciones simultáneas.
Actualizado el . 4 min de lectura
UTILIA OS rota el refresh token en cada renovación, como recomienda OAuth 2.1, y detecta cuándo alguien reutiliza uno ya rotado. Para tu código, la consecuencia es una sola regla: después de cada renovación, guarda el refresh token que acabas de recibir y olvida el anterior. Esta página explica por qué y cómo hacerlo bien, también cuando varias partes de tu aplicación renuevan a la vez.
Qué cambia en cada renovación
Cuando llamas a POST /api/oauth/token con grant_type=refresh_token, recibes un access token nuevo, de 1 hora, y un refresh token nuevo que sustituye al que enviaste. Así vive cada refresh token:
Vida de un refresh token
Vigente
El último que recibiste. Es el único que sirve para renovar.Se llega desde Vigente cuando: Pasan 90 días desde la autorización
Caducado
Estado final
La renovación responde invalid_grant. Hay que volver a autorizar.Se llega desde Vigente cuando: POST /api/oauth/revoke o la persona retira el acceso
Revocado
Estado final
Deja de valer junto con su access token.
Se llega desde Vigente cuando: Se usa para renovar y llega uno nuevo
Anulado por rotación
Ya no sirve: guarda el nuevo.Se llega desde Anulado por rotación cuando: Alguien vuelve a usarlo: invalid_grant
Cadena revocada
Estado final
Caen todos los tokens de esa autorización. Hay que volver a autorizar.
El plazo de 90 días cuenta desde la autorización y no se alarga al rotar: si la persona autorizó el 1 de enero, la cadena caduca a principios de abril aunque hayas renovado cien veces. Después, tiene que volver a autorizar.
Por qué se rota
Si alguien roba un refresh token, sin rotación podría renovar indefinidamente sin que nadie lo notara. Con rotación, el refresh token robado y el legítimo acaban chocando: el segundo que se use ya está anulado, el servidor lo detecta y corta la cadena entera. El atacante se queda sin acceso y la persona solo tiene que volver a autorizar.
Qué pasa si reutilizas uno anulado
Si llega un refresh token que ya se rotó, UTILIA OS:
- Responde
400conerror: "invalid_grant"yerror_description: "Refresh token has been rotated". - Revoca todos los tokens de esa cadena, es decir, los que nacieron de la misma autorización, incluido el access token que tuvieras en uso.
A partir de ahí, la única salida es repetir el flujo de autorización.
Basta un descuido para que la persona tenga que volver a autorizar: guardar el refresh token una sola vez y no actualizarlo, tenerlo copiado en dos sitios o reintentar con el viejo después de un corte de red en el que la renovación sí llegó a hacerse.
Renovaciones simultáneas
Si dos procesos de tu aplicación (dos pestañas, dos trabajadores) renuevan con el mismo refresh token, el resultado depende de cuándo llega el segundo:
Dos renovaciones con el mismo refresh token
Se cruzan en el servidor
Exactamente a la vez
La primera ya terminó
Un instante después
Por eso no basta con reintentar; hay que evitar que ocurra:
- Serializa las renovaciones. En el navegador, usa
navigator.locks.requesto comparte una única promesa de renovación entre pestañas; en el servidor, un cerrojo por persona y cliente. Los SDK oficiales ya comparten una sola renovación cuando varias peticiones la necesitan a la vez. - Si aun así recibes
Refresh token rotated concurrently, lee de tu almacenamiento el refresh token que acaba de guardar el otro proceso y úsalo. No reintentes con el que tenías.
Cómo adaptar tu código
1. Guarda los dos tokens tras cada renovación
async function refreshTokens(storedRefreshToken, clientId) {
const response = await fetch("https://os.utilia.ai/api/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: storedRefreshToken,
client_id: clientId,
}),
});
const body = await response.json();
if (!response.ok) {
throw Object.assign(new Error(body.error_description), { code: body.error });
}
// Sustituye los DOS tokens en tu almacenamiento persistente.
await tokenStore.save({
accessToken: body.access_token,
refreshToken: body.refresh_token,
expiresAt: Date.now() + body.expires_in * 1000,
});
return body;
}tokenStore y token_store representan tu propio almacenamiento (base de datos, sesión, archivo).
2. Trata cada invalid_grant
async function getFreshAccessToken(clientId) {
const stored = await tokenStore.read();
try {
const tokens = await refreshTokens(stored.refreshToken, clientId);
return tokens.access_token;
} catch (err) {
if (err.code !== "invalid_grant") throw err;
if (err.message?.includes("rotated concurrently")) {
// Otro proceso acaba de renovar: usa lo que dejó guardado.
const fresh = await tokenStore.read();
return fresh.accessToken;
}
// Reutilización, caducidad, persona inactiva o sin alcance: volver a autorizar.
await tokenStore.clear();
throw new Error("REAUTHORIZE");
}
}Preguntas frecuentes
¿Puedo seguir usando un refresh token antiguo si lo guardé en varios sitios? No. Solo vale el último; cualquier otro revoca la cadena.
¿Cuánto vive la cadena? 90 días desde la autorización. Rotar no alarga ese plazo.
¿Y si mi aplicación está mucho tiempo sin conexión? Si al volver el refresh token ha caducado, la renovación responde invalid_grant (Refresh token has expired). Detéctalo y lanza de nuevo la autorización.
¿Al renovar puedo cambiar de servidor MCP? No. El token renovado sigue atado al mismo servidor. Para otro servidor, pide otra autorización con su resource.
Relación con otros mecanismos
| Mecanismo | Qué hace | Cuándo actúa |
|---|---|---|
| Rotación | Entrega un refresh token nuevo y anula el anterior | En cada renovación correcta |
| Detección de reutilización | Revoca toda la cadena de esa autorización | Al llegar un refresh token ya anulado |
| Revocación | Anula el access token y el refresh token de una emisión | Al llamar a POST /api/oauth/revoke |
| Retirada desde UTILIA OS | Revoca todos los tokens de tu aplicación para esa persona | Cuando la persona la quita en Perfil › Aplicaciones |
| Persona inactiva | Rechaza sus tokens aunque no hayan caducado | Mientras su cuenta no esté activa |
| Cliente desactivado | Invalida todos sus tokens y no deja renovar | Cuando se desactiva OAuth en la aplicación |