{"record":{"id":"45625033fc40858e","repo":"toeverything/AFFiNE","slug":"auth-session-revoked-456250","errorCode":"auth_session_revoked","errorMessage":"The auth session has been revoked.","messagePattern":"The auth session has been revoked\\.","errorType":"http","errorClass":"AuthSessionHttpError","httpStatus":401,"severity":"error","filePath":"packages/backend/server/src/core/auth/session-exchange.ts","lineNumber":128,"sourceCode":"  async refresh(req: Request, refreshToken: string, appVersion?: string) {\n    if (!isNativeClientRequest(req)) throw new ActionForbidden();\n    const selector = refreshToken.split('.')[1];\n    if (selector) {\n      const rateKey = `auth:session-refresh-rate:${selector}`;\n      const attempts = await this.cache.increaseWithTtl(rateKey, 60_000);\n      if (attempts > 30) throw new TooManyRequest();\n    }\n    const refreshed = await this.authSessions.refresh(refreshToken, appVersion);\n    if (refreshed.status !== 'rotated') {\n      const status =\n        refreshed.code === AuthSessionErrorCode.temporarilyUnavailable\n          ? HttpStatus.SERVICE_UNAVAILABLE\n          : HttpStatus.UNAUTHORIZED;\n      throw new AuthSessionHttpError(refreshed.code, status);\n    }\n    const session = await this.authSessions.get(refreshed.authSessionId);\n    if (!session) {\n      throw new AuthSessionHttpError(AuthSessionErrorCode.revoked);\n    }\n    return this.tokenPair(\n      session.userSession.userId,\n      refreshed.authSessionId,\n      refreshed.refreshToken,\n      refreshed.refreshExpiresAt,\n      session.absoluteExpiresAt\n    );\n  }\n\n  private async tokenPair(\n    userId: string,\n    authSessionId: string,\n    refreshToken: string,\n    refreshTokenExpiresAt: Date,\n    absoluteExpiresAt: Date\n  ) {\n    const access = await this.accessTokens.sign(userId, authSessionId);","sourceCodeStart":110,"sourceCodeEnd":146,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/auth/session-exchange.ts#L110-L146","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Treat this exactly like a revoked session: discard the refresh token and require a fresh sign-in.","Investigate concurrent refreshers if it happens repeatedly (multiple devices/tabs sharing one token).","If caused by store replication lag, verify the cache/DB read-after-write consistency for auth sessions."],"exampleFix":"// before: assume rotation success means a usable session\nconst t = await refresh(); // may throw auth_session_revoked post-rotate\n\n// after: handle the post-rotate revoked race\ntry {\n  const t = await refresh();\n} catch (e) {\n  if (e.code === 'auth_session_revoked') { await forceSignIn(); }\n  else { throw e; }\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isPostRotateRevoked(e: unknown): boolean {\n  return e instanceof AuthSessionHttpError && e.authCode === AuthSessionErrorCode.revoked;\n}","tryCatchPattern":"try {\n  return await refresh(req, refreshToken, appVersion);\n} catch (e) {\n  if (e instanceof AuthSessionHttpError && e.authCode === AuthSessionErrorCode.revoked) {\n    await forceSignIn(); // session vanished between rotate and read\n  } else throw e;\n}","preventionTips":["Treat a post-rotate revoked race the same as any revocation: re-authenticate.","Avoid concurrent refreshers sharing one refresh token (multiple devices/tabs).","Verify read-after-write consistency of the auth-session store if this recurs."],"tags":["auth","session","refresh","revocation","race-condition"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}