toeverything/AFFiNE · critical · UnauthorizedException

-32000

-32000

Error message

Authentication failed

What it means

UnauthorizedException in authenticate() when models.mcpCredential.authenticate(parsed.id, workspaceId) returns no row: the token parses fine, but no live credential with that id exists for that workspace (unknown id, wrong workspace, revoked, replaced past grace, or expired). The MCP controller maps it to JSON-RPC -32000 'Authentication failed' with HTTP 401.

Source

Thrown at packages/backend/server/src/plugins/copilot/mcp/credential.ts:172

    if (result.count) {
      this.event.emit('mcp.credential.revoked', {
        credentialId: id,
        userId,
        workspaceId,
      });
    }
    return result.count > 0;
  }

  async authenticate(token: string, workspaceId: string) {
    const parsed = this.parse(token);
    if (!parsed) throw new UnauthorizedException();

    const credential = await this.models.mcpCredential.authenticate(
      parsed.id,
      workspaceId
    );
    if (!credential) throw new UnauthorizedException();

    const actualHash = this.crypto.sha256(parsed.secret).toString('hex');
    if (!this.crypto.compare(actualHash, credential.secretHash)) {
      throw new UnauthorizedException();
    }

    const now = new Date();
    await this.models.mcpCredential.touch(
      credential.id,
      new Date(now.getTime() - LAST_USED_WRITE_INTERVAL_MS),
      now
    );
    return credential;
  }

  private async issue(
    input: IssueMcpCredential & {
      familyId?: string;

View on GitHub (pinned to b4c8548c09)

Solutions

  1. Verify the MCP endpoint URL's workspaceId matches the workspace the credential was issued in
  2. If the credential was rotated, use the newest token in the family (old secret dies after graceEndsAt)
  3. If revoked/expired, issue a new credential and update the client config
  4. Check the credential still exists and is active via the GraphQL credential list

Example fix

// before
const mcp = new McpClient({ url: `.../mcp/${otherWorkspaceId}`, token: oldToken });

// after
const active = await findActiveCredential(credential.workspaceId);
const mcp = new McpClient({
  url: `.../mcp/${active.workspaceId}`,
  token: revealedTokenFor(active),
});
Defensive patterns

Strategy: validation

Validate before calling

const cred = await models.mcpCredential.get(parsedToken.id);
const usable = !!cred && !cred.revokedAt && !cred.replacedById && cred.expiresAt > new Date();
if (!usable || cred.workspaceId !== urlWorkspaceId) {
  throw new Error('Credential not active for this workspace — reissue or fix URL');
}

Try / catch

try {
  await mcpClient.connect();
} catch (e) {
  if (e.code === -32000) {
  // id unknown for this workspace: fix workspace mismatch or reissue credential
    await syncCredentialWithWorkspace();
    await mcpClient.connect();
  } else throw e;
}

Prevention

When it happens

Trigger: Connecting with a token from workspace A against workspace B's MCP endpoint; credential revoked or expired (outside rotation grace); credential deleted; id segment corrupted so the lookup misses.

Common situations: Workspace-scoped MCP URL changed after workspace migration; old token kept in client config after rotation dropped grace; credential cleaned up by an admin.

Understand the failure class

Related errors


AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18). Data as JSON: /api/errors/85440c82a6ecb987. Report an issue: GitHub.