toeverything/AFFiNE · error · AuthSessionHttpError
AUTH_SESSION_REVOKED
AUTH_SESSION_REVOKED
Error message
The auth session has been revoked.
What it means
In SessionExchangeService.refresh, the rotation itself succeeded (status 'rotated'), but the follow-up authSessions.get(refreshed.authSessionId) returned nothing, so the response is surfaced as AUTH_SESSION_REVOKED. The session ceased to exist between rotation and lookup — revoked or deleted concurrently with the refresh.
Source
Thrown at packages/backend/server/src/core/auth/session-exchange.ts:128
async refresh(req: Request, refreshToken: string, appVersion?: string) {
if (!isNativeClientRequest(req)) throw new ActionForbidden();
const selector = refreshToken.split('.')[1];
if (selector) {
const rateKey = `auth:session-refresh-rate:${selector}`;
const attempts = await this.cache.increaseWithTtl(rateKey, 60_000);
if (attempts > 30) throw new TooManyRequest();
}
const refreshed = await this.authSessions.refresh(refreshToken, appVersion);
if (refreshed.status !== 'rotated') {
const status =
refreshed.code === AuthSessionErrorCode.temporarilyUnavailable
? HttpStatus.SERVICE_UNAVAILABLE
: HttpStatus.UNAUTHORIZED;
throw new AuthSessionHttpError(refreshed.code, status);
}
const session = await this.authSessions.get(refreshed.authSessionId);
if (!session) {
throw new AuthSessionHttpError(AuthSessionErrorCode.revoked);
}
return this.tokenPair(
session.userSession.userId,
refreshed.authSessionId,
refreshed.refreshToken,
refreshed.refreshExpiresAt,
session.absoluteExpiresAt
);
}
private async tokenPair(
userId: string,
authSessionId: string,
refreshToken: string,
refreshTokenExpiresAt: Date,
absoluteExpiresAt: Date
) {
const access = await this.accessTokens.sign(userId, authSessionId);View on GitHub (pinned to b4c8548c09)
Solutions
- Treat this as signed-out: clear the stored token pair and route the user to sign-in
- Ensure sign-out/revoke on other devices also clears this device's tokens via push/socket so the race window shrinks
- Do not retry with the same refresh token after a revoked result — the session is gone
Example fix
// before
return await sessionExchange.refresh(req, refreshToken);
// after
try {
return await sessionExchange.refresh(req, refreshToken);
} catch (e) {
if (e instanceof AuthSessionHttpError && e.code === 'AUTH_SESSION_REVOKED') {
await clearTokenStore();
router.push('/signin');
return null;
}
throw e;
} Defensive patterns
Strategy: fallback
Type guard
function isSessionRevoked(e: unknown): boolean {
return (
typeof e === 'object' &&
e !== null &&
'code' in e &&
(e as { code?: string }).code === 'AUTH_SESSION_REVOKED'
);
} Try / catch
Catch AUTH_SESSION_REVOKED, purge the stored token pair and session state, and fall back to the sign-in screen. Retrying the refresh with any token is pointless — the session is gone.
Prevention
- Propagate sign-out/password-change revocations to all devices so this race window shrinks
- Clear local tokens eagerly on any revoked signal instead of looping refresh
- In multi-device apps, expect revocation at any time and keep the re-login path smooth
When it happens
Trigger: A refresh racing a revoke: user clicks sign-out(-everywhere) on another device, changePasswordAndRevokeSessions runs, or an admin revokes the session while this device is mid-refresh.
Common situations: Multi-device apps where sign-out on one device lands exactly as another refreshes; password change triggering session revocation mid-refresh; user switching accounts while a background refresh is in flight.
Related errors
AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18).
Data as JSON: /api/errors/d339690a61da1431.
Report an issue: GitHub.