makeplane/plane · critical · AppError

AUTH_USER_MISMATCH

AUTH_USER_MISMATCH

Error message

Authentication unsuccessful: User ID mismatch

What it means

Thrown by handleAuthentication in the live (HocusPocus / Yjs websocket) auth handler when the user ID resolved from the session cookie via UserService.currentUser() does not match the userId claim carried in the connection token. It is a deliberate identity-integrity check: the websocket connection must not be bound to a user other than the one the API says the cookie belongs to. The AppError carries code AUTH_USER_MISMATCH and is the strongest auth signal short of missing credentials.

Source

Thrown at apps/live/src/lib/auth.ts:81

  context.cookie = cookie ?? requestParameters.get("cookie") ?? "";
  context.documentType = requestParameters.get("documentType")?.toString() as TDocumentTypes;
  context.projectId = requestParameters.get("projectId");
  context.userId = userId;
  context.workspaceSlug = requestParameters.get("workspaceSlug");

  return await handleAuthentication({
    cookie: context.cookie,
    userId: context.userId,
  });
};

export const handleAuthentication = async ({ cookie, userId }: { cookie: string; userId: string }) => {
  // fetch current user info
  try {
    const userService = new UserService();
    const user = await userService.currentUser(cookie);
    if (user.id !== userId) {
      throw new AppError("Authentication unsuccessful: User ID mismatch", { code: "AUTH_USER_MISMATCH" });
    }

    return {
      user: {
        id: user.id,
        name: user.display_name,
      },
    };
  } catch (error) {
    const appError = new AppError(error, {
      context: { operation: "handleAuthentication" },
    });
    logger.error("Authentication failed", appError);
    throw new AppError("Authentication unsuccessful", { code: appError.code });
  }
};

View on GitHub (pinned to 1c8a60f858)

Solutions

  1. Force a full re-authentication on the client: clear the cookie/token (sign out) and sign in again so a fresh token+cookie pair is issued together.
  2. Verify the token payload the client sends to the websocket matches the authenticated session: inspect the JSON passed as the HocusPocus `token` parameter and confirm its `id` equals the API's current user id.
  3. If using a custom client or proxy, ensure it forwards the same cookie used for the REST `/users/me` call as the token's `id` claim, instead of a hard-coded or cached id.
  4. Check that UserService.currentUser is hitting the same backend/cookie domain as the REST app; a cross-domain cookie mismatch can resolve to a different user.

Example fix

// before: client stores token once and never refreshes it
const token = JSON.stringify({ id: cachedUserId, cookie: cachedCookie });
// after: always derive the token from the live session right before connecting
const me = await api.get('/users/me/');
const token = JSON.stringify({ id: me.id, cookie: document.cookie });
Defensive patterns

Strategy: validation

Validate before calling

// Client-side, before opening the websocket: confirm token id matches the live session
const me = await fetch('/api/users/me/', { credentials: 'include' }).then(r => r.ok ? r.json() : null);
const tokenPayload = JSON.parse(storedToken);
if (!me || me.id !== tokenPayload.id) {
  // refresh the token pair before connecting
  await refreshSession();
}

Type guard

const isUserDetailsToken = (t: unknown): t is { id: string; cookie: string } =>
  typeof t === 'object' && t !== null &&
  typeof (t as any).id === 'string' &&
  typeof (t as any).cookie === 'string';

Try / catch

try { await handleAuthentication({ cookie, userId }); }
catch (e) {
  if (e instanceof AppError && e.code === 'AUTH_USER_MISMATCH') {
    // force re-login: token does not match session
    await signOutAndRedirect();
  } else throw e;
}

Prevention

When it happens

Trigger: A websocket (live document) connection where the token's `id` field (parsed from the HocusPocus token JSON at auth.ts:41-43) disagrees with `user.id` returned by `userService.currentUser(cookie)`. Concretely: a stale token stored in the browser from a previous login, a token forged/mistuned by a custom client, the cookie belonging to a different (e.g. service) account than the token, or a token replayed in a different session after the user was re-created with a new ID.

Common situations: User logged in as account A, account was deleted and recreated, browser still holds the old token; multi-tab scenarios where one tab refreshed auth and another did not; SSO/impersonation flows that swap the cookie without reissuing the token; local/dev environments where a developer hand-crafted a token JSON with the wrong id.

Understand the failure class

Related errors


AI-assisted analysis of makeplane/plane@1c8a60f858 (2026-08-12). Data as JSON: /api/errors/55e692706f4d2bbb. Report an issue: GitHub.