{"record":{"id":"e9c640a960e953f2","repo":"toeverything/AFFiNE","slug":"the-refresh-token-is-invalid-the-auth-session-h","errorCode":null,"errorMessage":"The refresh token is invalid. | The auth session has expired. | The auth session has been revoked. | The refresh token has already been used. | Auth session service is temporarily unavailable.","messagePattern":"The refresh token is invalid\\. \\| The auth session has expired\\. \\| The auth session has been revoked\\. \\| The refresh token has already been used\\. \\| Auth session service is temporarily unavailable\\.","errorType":"http","errorClass":"AuthSessionHttpError","httpStatus":401,"severity":"error","filePath":"packages/backend/server/src/core/auth/session-exchange.ts","lineNumber":124,"sourceCode":"      issued.session.absoluteExpiresAt\n    );\n  }\n\n  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,","sourceCodeStart":106,"sourceCodeEnd":142,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/auth/session-exchange.ts#L106-L142","documentation":"Thrown by SessionExchangeService.refresh when authSessions.refresh returns a status other than 'rotated'. It is an AuthSessionHttpError whose `code` (lowercased) and HTTP status derive from refreshed.code: AUTH_SESSION_TEMPORARILY_UNAVAILABLE -> 503 network_error; any of REFRESH_TOKEN_INVALID / AUTH_SESSION_EXPIRED / AUTH_SESSION_REVOKED / REFRESH_TOKEN_REUSED -> 401 authentication_required. The aggregated message lists all five possibilities because the catalog covers the union; the concrete instance carries exactly one code.","triggerScenarios":"Calling refresh (session-exchange.ts:118-124) with a refresh token that authSessions.refresh rejects: token structurally invalid or not found (invalid), session TTL elapsed (expired), session revoked by user/admin (revoked), token already used once and not rotated (reused), or the underlying store (cache/db) is unreachable (temporarily_unavailable).","commonSituations":"App resumed after a long sleep and the session expired; user clicked 'Sign out everywhere' on another device (revoked); a token-reuse attack triggered automatic revocation; a Redis outage makes the session store temporarily unavailable; a stale token persisted across an app reinstall.","solutions":["On 401 codes (invalid/expired/revoked/reused), clear local credentials and route the user to sign-in.","On 503 (temporarily_unavailable), retry with exponential backoff after confirming the cache/store is healthy.","Inspect the concrete `code` on the error to choose between 'sign in again' vs. 'try again later'."],"exampleFix":"// before: treat every failure as a network blip and retry\nconst t = await refresh();\n\n// after: branch on the concrete auth code\ntry {\n  const t = await refresh();\n} catch (e) {\n  if (e.code === 'auth_session_temporarily_unavailable') { await backoffRetry(); }\n  else { await signOutAndShowLogin(); } // invalid|expired|revoked|reused\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isTransient(code: string): boolean {\n  return code === 'auth_session_temporarily_unavailable';\n}\nfunction isRecoverableBySignIn(code: string): boolean {\n  return ['refresh_token_invalid', 'auth_session_expired', 'auth_session_revoked', 'refresh_token_reused'].includes(code);\n}","tryCatchPattern":"try {\n  await refresh(req, refreshToken, appVersion);\n} catch (e) {\n  if (!(e instanceof AuthSessionHttpError)) throw e;\n  if (e.authCode === AuthSessionErrorCode.temporarilyUnavailable) {\n    await backoffRetry(); // 503 path\n  } else {\n    await clearCredentialsAndSignIn(); // 401 path: invalid|expired|revoked|reused\n  }\n}","preventionTips":["Branch on the concrete authCode: 503 -> retry, 401 -> sign in again.","Detect refresh-token reuse (reused) as a security event and revoke the session family.","Surface 'sign in again' rather than a raw token error to end users."],"tags":["auth","session","refresh","token","authentication"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}