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
- Regenerate and copy the full revealed token (id + secret) exactly as issued by createMcpCredential
- Strip whitespace/newlines from the token before putting it in the Authorization header
- Confirm you are using the MCP credential token format, not a JWT or other API key
- 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
- Validate the id.secret shape client-side before the first request
- Copy tokens from the credential reveal output verbatim, stripping whitespace
- Never substitute a JWT or other API key for the MCP credential token
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
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- -32000
- 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/21b45813d91b6437.
Report an issue: GitHub.