toeverything/AFFiNE · error · AuthSessionHttpError

auth_session_revoked

auth_session_revoked

Error message

The auth session has been revoked.

What it means

Thrown by SessionExchangeService.refresh when authSessions.refresh returned 'rotated' (success) but the subsequent authSessions.get(refreshed.authSessionId) returns null. It is an AuthSessionHttpError with code AUTH_SESSION_REVOKED and default HTTP 401. This handles the narrow race where the session is revoked between the rotation step and the read-back.

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 26c515e050)

Solutions

  1. Treat this exactly like a revoked session: discard the refresh token and require a fresh sign-in.
  2. Investigate concurrent refreshers if it happens repeatedly (multiple devices/tabs sharing one token).
  3. If caused by store replication lag, verify the cache/DB read-after-write consistency for auth sessions.

Example fix

// before: assume rotation success means a usable session
const t = await refresh(); // may throw auth_session_revoked post-rotate

// after: handle the post-rotate revoked race
try {
  const t = await refresh();
} catch (e) {
  if (e.code === 'auth_session_revoked') { await forceSignIn(); }
  else { throw e; }
}
Defensive patterns

Strategy: try-catch

Type guard

function isPostRotateRevoked(e: unknown): boolean {
  return e instanceof AuthSessionHttpError && e.authCode === AuthSessionErrorCode.revoked;
}

Try / catch

try {
  return await refresh(req, refreshToken, appVersion);
} catch (e) {
  if (e instanceof AuthSessionHttpError && e.authCode === AuthSessionErrorCode.revoked) {
    await forceSignIn(); // session vanished between rotate and read
  } else throw e;
}

Prevention

When it happens

Trigger: Calling refresh (session-exchange.ts:126-129): rotation succeeds, but between rotation and get() the session is deleted/revoked (concurrent 'sign out everywhere', admin action, or reuse-detection on another device).

Common situations: Two devices refreshing the same session near-simultaneously where one triggers revocation; an admin or security policy revoking sessions in the moment between rotate and read; a DB/cache consistency lag where the just-written session is not yet readable.

Related errors


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/45625033fc40858e. Report an issue: GitHub.