Rotación del refresh token
Cada renovación entrega un refresh token nuevo y anula el anterior: guarda siempre el último y qué pasa si reutilizas uno viejo o renuevas a la vez.
Actualizado el . 5 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 que debe servir 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. Durante un minuto, otra renovación con él recibe su propio par de tokens en la misma cadena.Se llega desde Anulado por rotación cuando: Se reutiliza pasado un minuto: 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.
El único margen es de un minuto desde la rotación, para no confundir con un robo a dos partes de tu propia aplicación que renuevan a la vez (lo explica Renovaciones simultáneas).
Qué pasa si reutilizas uno anulado
Si llega un refresh token que se rotó hace más de un minuto, o cuya cadena ya no está viva, 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. Las demás autorizaciones de la misma persona con tu aplicación (otro servidor MCP, otro aparato) no se tocan.
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, pasado el minuto, 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
0 s
El primero renueva
Recibe un par nuevo; el refresh que envió queda anulado.Antes de 60 s
El segundo, con el mismo
Recibe su propio par en la misma cadena. No se revoca nada.Después de 60 s
Alguien lo reutiliza
invalid_grant con «Refresh token has been rotated» y la cadena entera revocada.
Si las dos peticiones se cruzan en el servidor en el mismo instante, UTILIA OS la reintenta por dentro y el segundo proceso recibe también su propio par: tu código no ve ningún error.
El margen salva las renovaciones simultáneas, no el desorden: un proceso que se queda con el refresh viejo y renueva con él más tarde corta la cadena de todos. Por eso:
- 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. - Lee el almacenamiento justo antes de renovar. Si otro proceso ya renovó mientras esperabas el cerrojo, su access token sigue vigente: úsalo y no renueves.
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. Renueva una sola vez y trata invalid_grant
async function getFreshAccessToken(clientId) {
// withRefreshLock: tu cerrojo por persona y cliente (navigator.locks en el
// navegador, un cerrojo en tu base de datos o en Redis en el servidor).
return withRefreshLock(async () => {
const stored = await tokenStore.read();
// Otro proceso pudo renovar mientras esperabas el cerrojo.
if (stored.expiresAt - Date.now() > 60_000) return stored.accessToken;
try {
const tokens = await refreshTokens(stored.refreshToken, clientId);
return tokens.access_token;
} catch (err) {
if (err.code !== "invalid_grant") throw err;
// Reutilización fuera de plazo, caducidad, persona inactiva o sin alcance:
// volver a autorizar.
await tokenStore.clear();
throw new Error("REAUTHORIZE");
}
});
}withRefreshLock y _refresh_lock representan tu cerrojo; tokenStore y token_store, tu almacenamiento, los mismos del paso anterior.
Preguntas frecuentes
¿Puedo seguir usando un refresh token antiguo si lo guardé en varios sitios? No. Solo vale el último. Uno anterior solo sirve en el minuto siguiente a su rotación; después, 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 |
| Renovación simultánea | Entrega otro par en la misma cadena, sin revocar nada | Al llegar un refresh token anulado hace menos de un minuto, con la cadena viva |
| Detección de reutilización | Revoca toda la cadena de esa autorización | Al llegar un refresh token anulado hace más de un minuto |
| 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 Mi perfil › IA y 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 |