* XAA (Cross-App Access) auth. * * One IdP browser login is reused across all XAA-configured MCP servers: * 1. Acquire an id_token from the IdP (cached in keychain by issuer; if * missing/expired, runs a standard OIDC authorization_code+PKCE flow * — this is the one browser pop) * 2. Run
( serverName: string, serverConfig: McpSSEServerConfig | McpHTTPServerConfig, onAuthorizationUrl: (url: string) => void, abortSignal?: AbortSignal, skipBrowserOpen?: boolean, )
| 662 | * All errors are actionable — they tell the user what to run. |
| 663 | */ |
| 664 | async function performMCPXaaAuth( |
| 665 | serverName: string, |
| 666 | serverConfig: McpSSEServerConfig | McpHTTPServerConfig, |
| 667 | onAuthorizationUrl: (url: string) => void, |
| 668 | abortSignal?: AbortSignal, |
| 669 | skipBrowserOpen?: boolean, |
| 670 | ): Promise<void> { |
| 671 | if (!serverConfig.oauth?.xaa) { |
| 672 | throw new Error('XAA: oauth.xaa must be set') // guarded by caller |
| 673 | } |
| 674 | |
| 675 | // IdP config comes from user-level settings, not per-server. |
| 676 | const idp = getXaaIdpSettings() |
| 677 | if (!idp) { |
| 678 | throw new Error( |
| 679 | "XAA: no IdP connection configured. Run 'claude mcp xaa setup --issuer <url> --client-id <id> --client-secret' to configure.", |
| 680 | ) |
| 681 | } |
| 682 | |
| 683 | const clientId = serverConfig.oauth?.clientId |
| 684 | if (!clientId) { |
| 685 | throw new Error( |
| 686 | `XAA: server '${serverName}' needs an AS client_id. Re-add with --client-id.`, |
| 687 | ) |
| 688 | } |
| 689 | |
| 690 | const clientConfig = getMcpClientConfig(serverName, serverConfig) |
| 691 | const clientSecret = clientConfig?.clientSecret |
| 692 | if (!clientSecret) { |
| 693 | // Diagnostic context for serverKey mismatch debugging. Only computed |
| 694 | // on the error path so there's no perf cost on success. |
| 695 | const wantedKey = getServerKey(serverName, serverConfig) |
| 696 | const haveKeys = Object.keys( |
| 697 | getSecureStorage().read()?.mcpOAuthClientConfig ?? {}, |
| 698 | ) |
| 699 | const headersForLogging = Object.fromEntries( |
| 700 | Object.entries(serverConfig.headers ?? {}).map(([k, v]) => |
| 701 | k.toLowerCase() === 'authorization' ? [k, '[REDACTED]'] : [k, v], |
| 702 | ), |
| 703 | ) |
| 704 | logMCPDebug( |
| 705 | serverName, |
| 706 | `XAA: secret lookup miss. wanted=${wantedKey} have=[${haveKeys.join(', ')}] configHeaders=${jsonStringify(headersForLogging)}`, |
| 707 | ) |
| 708 | throw new Error( |
| 709 | `XAA: AS client secret not found for '${serverName}'. Re-add with --client-secret.`, |
| 710 | ) |
| 711 | } |
| 712 | |
| 713 | logMCPDebug(serverName, 'XAA: starting cross-app access flow') |
| 714 | |
| 715 | // IdP client secret lives in a separate keychain slot (keyed by IdP issuer), |
| 716 | // NOT the AS secret — different trust domain. Optional: if absent, PKCE-only. |
| 717 | const idpClientSecret = getIdpClientSecret(idp.issuer) |
| 718 | |
| 719 | // Acquire id_token (cached or via one OIDC browser pop at the IdP). |
| 720 | // Peek the cache first so we can report idTokenCacheHit in analytics before |
| 721 | // acquireIdpIdToken potentially writes a fresh one. |
no test coverage detected