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
- Verify the MCP endpoint URL's workspaceId matches the workspace the credential was issued in
- If the credential was rotated, use the newest token in the family (old secret dies after graceEndsAt)
- If revoked/expired, issue a new credential and update the client config
- 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
- Bind the credential to the same workspaceId that appears in the MCP endpoint URL
- After rotation or revocation, sweep all client configs for the old credential id
- Treat -32000 on connect as 'credentials/workspace mismatch' and resync rather than retry
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
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Unauthorized
- MCP credential not found
- Unsupported MCP credential expiration
- MCP credential name is required
- MCP write tools are not available
AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18).
Data as JSON: /api/errors/85440c82a6ecb987.
Report an issue: GitHub.