toeverything/AFFiNE · error · AuthenticationRequired

authentication_required

authentication_required

Error message

You must sign in first to access this resource.

What it means

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.

Source

Thrown at packages/backend/server/src/base/websocket/adapter.ts:64

              ),
            callback
          );
        },
        credentials: true,
        methods: CORS_ALLOWED_METHODS,
        allowedHeaders: CORS_ALLOWED_HEADERS,
      },
    });

    if (config.canActivate) {
      server.use((socket, next) => {
        config
          .canActivate(socket)
          .then(pass => {
            if (pass) {
              next();
            } else {
              throw new AuthenticationRequired();
            }
          })
          .catch(e => {
            next(e);
          });
      });
    }

    const pubClient = this.app.get(SocketIoRedis);
    const subClient = pubClient.duplicate();

    server.adapter(createAdapter(pubClient, subClient));
    const close = server.close;

    server.close = async fn => {
      await close.call(server, fn);
      // NOTE(@forehalo):
      //   the lifecycle of duplicated redis client will not be controlled by nestjs lifecycle

View on GitHub (pinned to 26c515e050)

Solutions

  1. 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.
  2. Verify the session is still valid by hitting `GET /api/auth/session` first; if it returns no user, re-authenticate before connecting the socket.
  3. Check that the backend Redis (`SocketIoRedis` / session cache) is reachable — an unreachable cache makes every otherwise-valid session look absent.
  4. Confirm the client version passes `AuthGuard.checkUserSessionClientVersion`; an unsupported version revokes/voids the session and yields no authed user.

Example fix

// before
const socket = io('/sync');

// after (browser, cookie-based)
const socket = io('/sync', { withCredentials: true });

// after (native, JWT-based)
const socket = io('/sync', {
  auth: { tokenType: 'jwt', token: accessToken },
});
Defensive patterns

Strategy: validation

Validate before calling

// Before opening the socket, confirm a session exists.
async function hasSession(): Promise<boolean> {
  const r = await fetch('/api/auth/session', { credentials: 'include' });
  if (!r.ok) return false;
  const { user } = await r.json();
  return !!user;
}
if (!(await hasSession())) { location.href = '/sign-in'; }
else { io('/sync', { withCredentials: true }); }

Type guard

function hasWsAuth(opts: { withCredentials?: boolean; auth?: { tokenType?: string; token?: string } }): boolean {
  return opts.withCredentials === true || (!!opts.auth?.token && opts.auth.tokenType === 'jwt');
}

Try / catch

// socket.io v4
socket.on('connect_error', (err) => {
  if (err.message.includes('sign in first') || err.data?.code === 'authentication_required') {
    redirectToLogin();
  }
});

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


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