toeverything/AFFiNE · critical · UnauthorizedException

Unauthorized

Error message

Unauthorized

What it means

UnauthorizedException thrown in MCP credential authenticate() when this.parse(token) returns null — the bearer token is not a syntactically valid 'id.secret' MCP credential token. Parsing fails before any database or hash work; the credential was never even looked up. The MCP HTTP controller catches it and returns JSON-RPC error code -32000 'Authentication failed' (HTTP 401).

Source

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

    }
    const result = await this.models.mcpCredential.revokeFamily(
      credential.familyId,
      userId,
      workspaceId
    );
    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
    );

View on GitHub (pinned to b4c8548c09)

Solutions

  1. Regenerate and copy the full revealed token (id + secret) exactly as issued by createMcpCredential
  2. Strip whitespace/newlines from the token before putting it in the Authorization header
  3. Confirm you are using the MCP credential token format, not a JWT or other API key
  4. If you control the client, validate the token shape (two non-empty segments) before connecting

Example fix

// before
headers: { Authorization: `Bearer ${jwt}` } // wrong token type

// after
headers: { Authorization: `Bearer ${mcpCredentialToken.trim()}` } // 'id.secret' from reveal
Defensive patterns

Strategy: validation

Validate before calling

const isValidMcpToken = (t: string) => {
  const parts = t.trim().split('.');
  return parts.length === 2 && parts[0].length > 0 && parts[1].length > 0;
};
if (!isValidMcpToken(token)) throw new Error('Token is not a valid MCP credential token');

Type guard

const isMcpCredentialToken = (t: unknown): t is string =>
  typeof t === 'string' && /^[^.]+\.[^.]+$/.test(t.trim());

Try / catch

try {
  await mcpClient.connect();
} catch (e) {
  if (e.code === -32000 && e.message === 'Authentication failed') {
    const revealed = await reissueMcpCredential();
    mcpClient.setToken(revealed.token);
    await mcpClient.connect();
  } else throw e;
}

Prevention

When it happens

Trigger: Sending an Authorization header that is not a well-formed MCP credential token (wrong scheme, base64 garbage, missing secret segment, empty string); using a JWT or API key where the MCP credential token belongs; token truncated by copy-paste.

Common situations: Client config pastes the wrong secret type into the MCP server URL/header; secrets manager mangles the token; UI shows the token with whitespace/newline that gets included.

Understand the failure class

Related errors


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