{"record":{"id":"2a7a3359d756a99c","repo":"toeverything/AFFiNE","slug":"invalid-auth-state-2a7a33","errorCode":"invalid_auth_state","errorMessage":"Invalid auth state. You might start the auth progress from another device.","messagePattern":"Invalid auth state\\. You might start the auth progress from another device\\.","errorType":"exception","errorClass":"InvalidAuthState","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/core/auth/session-exchange.ts","lineNumber":87,"sourceCode":"  ) {}\n\n  async createCode(req: Request, userId: string, clientVersion?: string) {\n    if (!isNativeClientRequest(req)) return;\n    return this.challenges.create<SessionExchangePayload>(\n      'auth_session_exchange',\n      { userId, clientVersion },\n      60 * 1000\n    );\n  }\n\n  @Transactional()\n  async exchange(req: Request, code: string, metadata: AuthSessionMetadata) {\n    if (!isNativeClientRequest(req)) throw new ActionForbidden();\n    const payload = await this.challenges.consume<SessionExchangePayload>(\n      'auth_session_exchange',\n      code\n    );\n    if (!payload?.userId) throw new InvalidAuthState();\n    const user = await this.models.user.lockForAuthIssuance(payload.userId);\n    if (!user || user.disabled) throw new InvalidAuthState();\n    const userSession = await this.auth.createUserSession(\n      payload.userId,\n      undefined,\n      undefined,\n      payload.clientVersion\n    );\n\n    const issued = await this.authSessions.create({\n      userSessionId: userSession.id,\n      ...metadata,\n    });\n    return this.tokenPair(\n      payload.userId,\n      issued.session.id,\n      issued.refreshToken,\n      issued.refreshExpiresAt,","sourceCodeStart":69,"sourceCodeEnd":105,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/auth/session-exchange.ts#L69-L105","documentation":"Thrown by SessionExchangeService.exchange when the consumed challenge payload has no userId. Category 'bad_request', code 'invalid_auth_state'. challenges.consume returned undefined or a SessionExchangePayload without userId, meaning the one-time 'auth_session_exchange' code was missing, expired (60s TTL), already consumed, or stored without a userId.","triggerScenarios":"Calling exchange (session-exchange.ts:83-87) with a code that the challenge store cannot resolve to a valid SessionExchangePayload, or whose payload.userId is absent. The code is created by createCode with a 60_000ms TTL.","commonSituations":"The user started auth on device A (which created the code) but the exchange is attempted after the 60s window elapsed; the code was already exchanged once (single-use); a code was crafted/tampered with so it lacks userId; createCode never ran because createCode itself is a no-op for non-native requests.","solutions":["Re-initiate the session exchange flow to obtain a fresh one-time code and exchange it promptly (within 60s).","Verify the code was actually created by createCode (which requires a native-client request) before calling exchange.","Ensure the challenge store backend (cache/redis) is reachable and not evicting keys prematurely."],"exampleFix":"// before: reuse a stale code\nconst { accessToken } = await exchange(req, oldCode, metadata);\n\n// after: mint a fresh code and exchange immediately\nconst { code } = await createCode(req, userId, clientVersion);\nif (!code) throw new Error('Native client required to create a code');\nconst { accessToken } = await exchange(req, code, metadata);","handlingStrategy":"retry","validationCode":null,"typeGuard":"function hasUserId(p: unknown): p is SessionExchangePayload {\n  return !!p && typeof (p as SessionExchangePayload).userId === 'string';\n}","tryCatchPattern":"try {\n  await exchange(req, code, metadata);\n} catch (e) {\n  if (e.code === 'invalid_auth_state') {\n    const fresh = await createCode(req, userId, clientVersion);\n    if (fresh) await exchange(req, fresh, metadata); // one retry with a fresh code\n  } else throw e;\n}","preventionTips":["Exchange the one-time code immediately after createCode (TTL is 60s).","Ensure createCode ran (it is a no-op for non-native requests).","Confirm the challenge store (cache) is healthy before issuing codes."],"tags":["auth","session","challenge","native-client","expiry"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}