{"record":{"id":"af63ae7c0dfe41aa","repo":"ruvnet/ruflo","slug":"mcp-caller-auth-verification-failed","errorCode":"mcp-caller-auth-verification-failed","errorMessage":"mcp-caller-auth-verification-failed:${result.reason}","messagePattern":"mcp-caller-auth-verification-failed:(.+?)","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/services/policy-runtime.ts","lineNumber":370,"sourceCode":"  if (!encodedToken || !publicKeyHex) {\n    throw new Error('mcp-caller-auth-enabled-but-no-token');\n  }\n\n  const token = decodeTokenEnvelope(encodedToken);\n  if (!token) {\n    throw new Error('mcp-caller-auth-enabled-but-no-token');\n  }\n\n  let publicKey;\n  try {\n    publicKey = publicKeyFromHex(publicKeyHex);\n  } catch {\n    throw new Error('mcp-caller-auth-enabled-but-no-token');\n  }\n\n  const result = verifyInvocationToken(token, publicKey, {});\n  if (!result.valid) {\n    throw new Error(`mcp-caller-auth-verification-failed:${result.reason}`);\n  }\n\n  return { id: token.callerId, type: 'agent' };\n}\n\nexport async function authorizeMcpTool(\n  toolName: string,\n  input: Record<string, unknown>,\n  context: Record<string, unknown> = {},\n  attributes: Readonly<{\n    actionType?: string;\n    network?: boolean;\n    destructive?: boolean;\n    namespaceAccess?: 'read' | 'write';\n    envelope?: CapabilityEnvelope;\n    costUsd?: number;\n    tokens?: number;\n    concurrency?: number;","sourceCodeStart":352,"sourceCodeEnd":388,"githubUrl":"https://github.com/ruvnet/ruflo/blob/2602b642d92234c710ffbe96bfb33007d481ceab/v3/@claude-flow/cli/src/services/policy-runtime.ts#L352-L388","documentation":"The final check in resolveMcpCallerIdentity cryptographically verifies the invocation token against the caller's public key. When verifyInvocationToken returns valid=false, the error includes the verifier's reason (e.g. expired, bad signature) as mcp-caller-auth-verification-failed:<reason>, so the suffix tells you exactly which verification step failed.","triggerScenarios":"A well-formed token and pubkey are both set, but verifyInvocationToken rejects them: signature mismatch, expired token, wrong audience/nonce, or the token was signed by a different key than the configured pubkey.","commonSituations":"Rotating the caller keypair without re-issuing the token; clock skew making a fresh token appear expired; reusing a token past its TTL; configuring the pubkey of a different agent than the token's signer.","solutions":["Read the reason suffix in the error message and address that specific failure","Re-issue the invocation token signed by the key matching CLAUDE_FLOW_MCP_CALLER_PUBKEY","Check for clock skew (NTP) if the reason indicates expiry","Rotate both token and pubkey together after key rotation"],"exampleFix":"// before\n# token signed by old key, pubkey = new key\n// after\n# re-issue token with new key, export matching CLAUDE_FLOW_MCP_INVOCATION_TOKEN and CLAUDE_FLOW_MCP_CALLER_PUBKEY","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { await callMcpTool(tool, args); } catch (e) { const m = /^mcp-caller-auth-verification-failed:(.+)$/.exec(e.message); if (m) { const reason = m[1]; if (reason.includes('expired')) reissueToken(); else if (reason.includes('signature')) rotateKeyAndToken(); } throw e; }","preventionTips":["Re-issue tokens after any keypair rotation","Run NTP so token TTLs are evaluated correctly","Ensure token callerId and configured pubkey belong to the same identity","Monitor error reasons in logs to catch recurring expiry vs signature mismatches"],"tags":["authentication","mcp","signature","token","policy"],"backgroundTag":"jwt-token-expired","analyzedSha":"2602b642d92234c710ffbe96bfb33007d481ceab","analyzedAt":"2026-09-15T22:58:14.805Z","contentChangedAt":"2026-09-15T22:58:14.805Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}