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

  1. Treat this as signed-out: clear the stored token pair and route the user to sign-in
  2. Ensure sign-out/revoke on other devices also clears this device's tokens via push/socket so the race window shrinks
  3. 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

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.