{"record":{"id":"283fa5b9987391db","repo":"toeverything/AFFiNE","slug":"authentication-required","errorCode":"authentication_required","errorMessage":"You must sign in first to access this resource.","messagePattern":"You must sign in first to access this resource\\.","errorType":"http","errorClass":"AuthenticationRequired","httpStatus":401,"severity":"error","filePath":"packages/backend/server/src/base/websocket/adapter.ts","lineNumber":64,"sourceCode":"              ),\n            callback\n          );\n        },\n        credentials: true,\n        methods: CORS_ALLOWED_METHODS,\n        allowedHeaders: CORS_ALLOWED_HEADERS,\n      },\n    });\n\n    if (config.canActivate) {\n      server.use((socket, next) => {\n        config\n          .canActivate(socket)\n          .then(pass => {\n            if (pass) {\n              next();\n            } else {\n              throw new AuthenticationRequired();\n            }\n          })\n          .catch(e => {\n            next(e);\n          });\n      });\n    }\n\n    const pubClient = this.app.get(SocketIoRedis);\n    const subClient = pubClient.duplicate();\n\n    server.adapter(createAdapter(pubClient, subClient));\n    const close = server.close;\n\n    server.close = async fn => {\n      await close.call(server, fn);\n      // NOTE(@forehalo):\n      //   the lifecycle of duplicated redis client will not be controlled by nestjs lifecycle","sourceCodeStart":46,"sourceCodeEnd":82,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/base/websocket/adapter.ts#L46-L82","documentation":"Thrown by the Socket.IO server's connection middleware when the configured `canActivate` handshake guard resolves to false. The guard runs `AuthGuard.signIn` against the WS upgrade request; if no valid session cookie or JWT bearer is present (or `signIn` caught an internal error and returned null), the socket is rejected before the connection is established. This is the WebSocket equivalent of HTTP 401 Unauthorized.","triggerScenarios":"Opening a WebSocket connection to the sync endpoint without an `affine_session` cookie, without an `Authorization: Bearer <jwt>` header (or `handshake.auth.token` for socket.io), with an expired/revoked session, or while the backend's session lookup throws (the guard swallows errors and treats them as unauthenticated).","commonSituations":"Client connects from a fresh context (no cookies persisted), cross-origin WS without `withCredentials`, JWT expired between HTTP page load and WS upgrade, native app forgot to attach the token to the socket handshake, or the Redis session store is unreachable so `getUserSessionFromRequest` returns null.","solutions":["Ensure the WS client sends credentials: for browsers set the session cookie (same-site, `credentials: 'include'` on the polling fallback); for native clients pass `auth: { tokenType: 'jwt', token }` so the server rewrites it to an Authorization header.","Verify the session is still valid by hitting `GET /api/auth/session` first; if it returns no user, re-authenticate before connecting the socket.","Check that the backend Redis (`SocketIoRedis` / session cache) is reachable — an unreachable cache makes every otherwise-valid session look absent.","Confirm the client version passes `AuthGuard.checkUserSessionClientVersion`; an unsupported version revokes/voids the session and yields no authed user."],"exampleFix":"// before\nconst socket = io('/sync');\n\n// after (browser, cookie-based)\nconst socket = io('/sync', { withCredentials: true });\n\n// after (native, JWT-based)\nconst socket = io('/sync', {\n  auth: { tokenType: 'jwt', token: accessToken },\n});","handlingStrategy":"validation","validationCode":"// Before opening the socket, confirm a session exists.\nasync function hasSession(): Promise<boolean> {\n  const r = await fetch('/api/auth/session', { credentials: 'include' });\n  if (!r.ok) return false;\n  const { user } = await r.json();\n  return !!user;\n}\nif (!(await hasSession())) { location.href = '/sign-in'; }\nelse { io('/sync', { withCredentials: true }); }","typeGuard":"function hasWsAuth(opts: { withCredentials?: boolean; auth?: { tokenType?: string; token?: string } }): boolean {\n  return opts.withCredentials === true || (!!opts.auth?.token && opts.auth.tokenType === 'jwt');\n}","tryCatchPattern":"// socket.io v4\nsocket.on('connect_error', (err) => {\n  if (err.message.includes('sign in first') || err.data?.code === 'authentication_required') {\n    redirectToLogin();\n  }\n});","preventionTips":["Always pass `withCredentials: true` (browser) or `auth: { tokenType: 'jwt', token }` (native) when connecting.","Validate the session via `/api/auth/session` before establishing the socket.","Refresh access tokens proactively so the WS handshake always has valid credentials."],"tags":["websocket","authentication","session","socket-io"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}