fix(auth): refresh cooldown 60s — auto-recover from sticky errors
Previous logic set token.error=RefreshAccessTokenError and never retried — once a refresh failed (likely a race during the early parallel-storm period), Marius's JWT cookie carried that error forever. New jwt calls all saw "blocked" → kept using the stale accessToken → api.gis.ac returned invalid_token on every call. Fix: store errorAt timestamp alongside the error flag. Block refresh attempts for 60s after a failure (avoids hot-loop on persistent Authentik issues), then unblock and retry. On the next failure, the 60s cooldown re-arms. For Marius's currently-stuck session: as soon as this deploys, his next jwt callback will pass the cooldown check (errorAt is hours ago) and trigger a fresh refresh. If Authentik is happy with his refresh_token, the error flag is cleared and he's back to normal — no relogin needed. Logs now show "blocked=true/false" alongside secLeft for visibility. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -64,7 +64,7 @@ async function refreshAuthentikToken(token: JWT): Promise<JWT> {
|
|||||||
};
|
};
|
||||||
if (!res.ok || !body.access_token) {
|
if (!res.ok || !body.access_token) {
|
||||||
console.warn("[auth] refresh failed:", res.status, body.error);
|
console.warn("[auth] refresh failed:", res.status, body.error);
|
||||||
return { ...token, error: "RefreshAccessTokenError" };
|
return { ...token, error: "RefreshAccessTokenError", errorAt: Date.now() };
|
||||||
}
|
}
|
||||||
console.log("[auth] refresh OK expires_in=%d", body.expires_in ?? 300);
|
console.log("[auth] refresh OK expires_in=%d", body.expires_in ?? 300);
|
||||||
return {
|
return {
|
||||||
@@ -77,7 +77,7 @@ async function refreshAuthentikToken(token: JWT): Promise<JWT> {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
console.warn("[auth] refresh error:", msg);
|
console.warn("[auth] refresh error:", msg);
|
||||||
return { ...token, error: "RefreshAccessTokenError" };
|
return { ...token, error: "RefreshAccessTokenError", errorAt: Date.now() };
|
||||||
} finally {
|
} finally {
|
||||||
inflightRefreshes.delete(refreshToken);
|
inflightRefreshes.delete(refreshToken);
|
||||||
}
|
}
|
||||||
@@ -142,18 +142,23 @@ export const authOptions: NextAuthOptions = {
|
|||||||
const exp = typeof token.accessTokenExpires === "number"
|
const exp = typeof token.accessTokenExpires === "number"
|
||||||
? token.accessTokenExpires
|
? token.accessTokenExpires
|
||||||
: 0;
|
: 0;
|
||||||
|
const errorAt = typeof token.errorAt === "number" ? token.errorAt : 0;
|
||||||
|
const cooldownDone = Date.now() - errorAt > 60_000;
|
||||||
|
const blocked =
|
||||||
|
token.error === "RefreshAccessTokenError" && !cooldownDone;
|
||||||
const secLeft = Math.round((exp - Date.now()) / 1000);
|
const secLeft = Math.round((exp - Date.now()) / 1000);
|
||||||
console.log(
|
console.log(
|
||||||
"[auth] jwt secLeft=%d hasAccess=%s hasRefresh=%s err=%s",
|
"[auth] jwt secLeft=%d hasAccess=%s hasRefresh=%s err=%s blocked=%s",
|
||||||
secLeft,
|
secLeft,
|
||||||
!!token.accessToken,
|
!!token.accessToken,
|
||||||
!!token.refreshToken,
|
!!token.refreshToken,
|
||||||
token.error ?? "none",
|
token.error ?? "none",
|
||||||
|
blocked,
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
token.accessToken &&
|
token.accessToken &&
|
||||||
token.refreshToken &&
|
token.refreshToken &&
|
||||||
token.error !== "RefreshAccessTokenError" &&
|
!blocked &&
|
||||||
Date.now() > exp - 30_000
|
Date.now() > exp - 30_000
|
||||||
) {
|
) {
|
||||||
return refreshAuthentikToken(token);
|
return refreshAuthentikToken(token);
|
||||||
|
|||||||
Reference in New Issue
Block a user