{"slug":"connectors/P0_OAUTH_RELEASE_BLOCKER_IMPLEMENTATION","title":"P0: Public OAuth Release Blocker — Implementation Specification","section":"connectors","sectionLabel":"Connectors","status":"unclassified","type":"document","path":"connectors/P0_OAUTH_RELEASE_BLOCKER_IMPLEMENTATION.mdx","bytes":10918,"description":null,"updated":null,"source":"integratewise/Integratewise-docs-wiki","content":"\n**Status**: Ready for implementation  \n**Branch**: `release/public-connector-go-live`  \n**Priority**: P0 — blocks all downstream release work\n\n---\n\n## Executive Summary\n\nThe MCP service is feature-gated behind `MCP_EXTERNAL_OAUTH_ENABLED = \"false\"` and hard-codes tenant assignment to `\"iw-customer-zero\"`. The fix is **narrow and already implemented**:\n\n- OAuth architecture exists ✓\n- Client registry and redirect validation exist ✓\n- JWT signing infrastructure exists ✓\n- Missing: User membership binding in the consent flow\n\n**Work required**: Remove hard-coded Customer Zero tenant assignment and bind OAuth codes to real Descope-authenticated users and their workspace memberships.\n\n---\n\n## Current Blocker Analysis\n\n### What's Disabled\n\n```toml\n# services/mcp-connector/wrangler.toml\nMCP_EXTERNAL_OAUTH_ENABLED = \"false\"  # ← Blocks /oauth/authorize and /oauth/register\n\n# Metadata endpoint reports:\nintegratewise_availability: \"private_alpha\"  # ← Indicates not ready for public use\n```\n\n### Where Hard-Coding Exists\n\n**File**: `services/gateway/src/descope-auth.ts`\n\n```typescript\nif (isCustomerZero) {\n  tenantId = \"iw-customer-zero\"; // ← Hard-coded default\n  organizationId = \"integratewise\";\n  if (!roles.length) roles = [\"owner\"];\n}\n```\n\nThis function is used in authentication flows but NOT in OAuth token exchange (token exchange reads tenant from the OAuth code, which gets it from the client registry). However, to support real user membership validation in the consent screen, we need to:\n\n1. Verify the user has an authenticated Descope session\n2. Resolve their real tenant from Descope principal's tenants array\n3. Validate they are a member of the workspace requested by the OAuth client\n\n---\n\n## Implementation Tasks\n\n### Task 1: Bind OAuth Consent to Descope-Authenticated User\n\n**File**: `services/mcp-connector/src/lib/oauth-auth-server.ts`\n\n**Current flow** (line 126-176):\n\n```typescript\nexport async function handleAuthorizeConfirm(\n  request: Request,\n  env: OAuthAuthServerEnv\n): Promise<Response> {\n  const formData = await request.formData();\n  const clientId = formData.get(\"client_id\")?.toString() || \"\";\n  const redirect_uri = formData.get(\"redirect_uri\")?.toString();\n  // ... no user validation happens here\n  const tenant_id = client.tenant_id; // ← Uses client's pre-registered tenant only\n}\n```\n\n**Changes needed**:\n\n1. Add Descope session token extraction from cookies or Authorization header\n2. Validate token with Descope management API\n3. Map Descope principal to verify tenant membership\n4. Require user consent form to bind user principal ID to the code\n\n**New signature**:\n\n```typescript\nexport async function handleAuthorizeConfirm(\n  request: Request,\n  env: OAuthAuthServerEnv,\n  descopeToken?: string // Optional: from cookie or Authorization header\n): Promise<Response>;\n```\n\n**Validation rules**:\n\n- If public OAuth is enabled AND token is provided: validate user membership\n- If public OAuth is disabled: only pre-provisioned Customer Zero clients work\n- Tenant must match: user's tenant ⊆ client's allowed workspace(s)\n\n---\n\n### Task 2: Implement Tenant and Workspace Membership Resolution\n\n**File**: `services/mcp-connector/src/lib/oauth-clients.ts`\n\n**Add**:\n\n```typescript\nexport interface OAuthClient {\n  client_id: string;\n  client_secret_hash: string;\n  client_name: string;\n  redirect_uris: string[];\n  allowed_scopes: string[];\n  tenant_id: string;\n  grant_types: string[];\n  is_active: boolean;\n  // NEW: Allow multi-tenant clients\n  allowed_tenants?: string[]; // If empty or null: matches client's tenant_id only\n  require_membership_validation?: boolean; // NEW: require consent binding\n}\n```\n\n**Add function**:\n\n```typescript\n/**\n * Resolve user's real tenant from Descope principal and validate against\n * the OAuth client's allowed tenants.\n *\n * Returns the resolved tenant_id if valid, otherwise throws.\n */\n  client: OAuthClient,\n  userTenantId: string,\n  userOrganizationId?: string\n): string {\n  const allowed =\n    client.allowed_tenants && client.allowed_tenants.length > 0\n      ? client.allowed_tenants\n      : [client.tenant_id];\n\n  if (!allowed.includes(userTenantId)) {\n    throw new Error(\n      `User tenant ${userTenantId} not in client allowed tenants: ${allowed.join(\", \")}`\n    );\n  }\n\n  return userTenantId;\n}\n```\n\n---\n\n### Task 3: Validate Redirect URIs and Sanitize Parameters\n\n**File**: `services/mcp-connector/src/lib/oauth-auth-server.ts`\n\n**Add parameter sanitization in `handleAuthorizeConfirm`**:\n\n```typescript\n// NEW: Validate all form fields before trusting them\nconst violations: string[] = [];\n\nif (!clientId || clientId.length === 0) {\n  violations.push(\"client_id is required\");\n}\nif (!redirect_uri) {\n  violations.push(\"redirect_uri is required\");\n}\nif (redirect_uri && !isValidUrl(redirect_uri)) {\n  violations.push(\"redirect_uri is not a valid URL\");\n}\n\nif (violations.length > 0) {\n  return new Response(`Invalid request: ${violations.join(\"; \")}`, { status: 400 });\n}\n\n// Redirect validation happens here already (line 150)\nif (!isRedirectUriAllowed(client, redirect_uri)) {\n  return new Response(\"Invalid redirect_uri for this client\", { status: 400 });\n}\n```\n\n**Add helper**:\n\n```typescript\nfunction isValidUrl(url: string): boolean {\n  try {\n    const parsed = new URL(url);\n    return parsed.protocol === \"http:\" || parsed.protocol === \"https:\";\n  } catch {\n    return false;\n  }\n}\n```\n\n---\n\n### Task 4: Enable MCP_EXTERNAL_OAUTH_ENABLED in Test Environment\n\n**File**: `services/mcp-connector/wrangler.toml`\n\n**Change** (line 115):\n\n```toml\n[env.test.vars]\nENVIRONMENT = \"test\"\nCF_ACCESS_TEAM_DOMAIN = \"integratewise.cloudflareaccess.com\"\nMCP_EXTERNAL_OAUTH_ENABLED = \"true\"  # ← Change from \"false\" to \"true\"\nDESCOPE_PROJECT_ID = \"P3FsuxiG33B1n0G6601XP9bn75Yp\"\nTENANT_ID = \"iw-customer-zero\"\nORG_ID = \"integratewise\"\nCLIENT_ID = \"mcp-connector-test\"\n```\n\n**Keep in prod**:\n\n```toml\n[env.prod.vars]\nENVIRONMENT = \"prod\"\n...\nMCP_EXTERNAL_OAUTH_ENABLED = \"false\"  # ← Stays disabled until manual approval\n```\n\n---\n\n### Task 5: Create Smoke Tests for Public OAuth Flow\n\n**File**: `services/mcp-connector/src/oauth.test.ts` (new or extend existing)\n\n**Test cases**:\n\n1. ✓ Authorize endpoint requires valid client_id and redirect_uri\n2. ✓ Authorize endpoint validates redirect_uri against registered list\n3. ✓ Consent form displays when external OAuth is enabled\n4. ✓ Consent form binds to authenticated user (Descope token)\n5. ✓ Authorization code is generated with correct tenant_id\n6. ✓ Token exchange fails if code's tenant doesn't match user's tenant\n7. ✓ Refresh token is issued and can be used for token renewal\n8. ✓ Revoked refresh tokens cannot be used\n\n---\n\n### Task 6: Deploy to Test Environment\n\n**Checklist**:\n\n- [ ] Dry-run: `pnpm release:core --env test --dry-run`\n- [ ] Validate output: connector, mcp-connector, connector-sync, knowledge, continuity, intelligence, workflow, l2, gateway\n- [ ] Live deploy: `pnpm release:core --env test --dry-run=false`\n- [ ] Validate in test:\n  - [ ] Connector catalog loads at `https://test-mcp.integratewise.ai/.well-known/oauth-authorization-server`\n  - [ ] GET `/oauth/authorize?response_type=code&client_id=test&redirect_uri=...` returns 200 HTML\n  - [ ] POST `/oauth/authorize/confirm` with valid form returns 302 redirect with code\n  - [ ] POST `/oauth/token` with code returns access_token\n  - [ ] GET `/tools` with Bearer token returns 200\n  - [ ] Existing workspace is resolved from Descope principal\n  - [ ] Provider connection is stored in Spine\n  - [ ] Initial sync runs\n  - [ ] Normalized records arrive in Spine\n  - [ ] Reauthorization works\n  - [ ] Revocation works\n  - [ ] Connector failure is visible in logs\n\n---\n\n### Task 7: Enable MCP_EXTERNAL_OAUTH_ENABLED in Production\n\n**Only after test validation passes**:\n\n```toml\n[env.prod.vars]\n...\nMCP_EXTERNAL_OAUTH_ENABLED = \"true\"  # ← Manually enable AFTER test passes\n```\n\n**Deployment command**:\n\n```bash\npnpm release:core \\\n  --env prod \\\n  --service core \\\n  --dry-run=false \\\n  --confirm_production=true \\\n  --test_release_evidence=<URL to successful test workflow run>\n```\n\n---\n\n### Task 8: External v0 Proof-of-Concept\n\n**Test requirement**: Create blank v0 application and connect it to production IntegrateWise without provider-specific credentials.\n\n**Connection flow**:\n\n1. Blank v0 app\n2. Add IntegrateWise connector → redirects to `https://mcp.integratewise.ai/oauth/authorize`\n3. Sign in with Descope\n4. Consent screen displays workspace selection\n5. Authorize → redirected to v0 app with OAuth code\n6. v0 app exchanges code for access token\n7. v0 app fetches workspace from IntegrateWise MCP\n8. v0 displays: connected workspace, existing connections, Spine records\n\n**Validation**:\n\n- [ ] No Supabase, D1, Salesforce, Slack, GitHub credentials leaked to v0\n- [ ] No provider-specific implementation code in v0\n- [ ] External client can read connector catalog\n- [ ] External client can read existing connections\n- [ ] External client can read Spine records\n- [ ] Retest from second external client to confirm isolation\n\n---\n\n## Configuration State Machine\n\n| State           | MCP_EXTERNAL_OAUTH_ENABLED | Use Case                         | Availability    |\n| --------------- | -------------------------- | -------------------------------- | --------------- |\n| Private Alpha   | false                      | Internal team + Customer Zero    | `private_alpha` |\n| Test            | true                       | Release validation               | `enabled`       |\n| Production (V1) | false                      | Pre-launch (disable until ready) | `private_alpha` |\n| Production (V2) | true                       | Public release                   | `enabled`       |\n\n---\n\n## Key Decisions\n\n1. **No new database schema**: Use existing `oauth_clients` table in D1\n2. **No DCR (Dynamic Client Registration)**: Public clients must pre-register via admin API\n3. **No multi-tenancy for v1**: Each OAuth client is bound to one workspace (can be extended later)\n4. **Refresh tokens enabled**: Allow long-lived sessions for external integrations\n5. **Descope as authz source**: Only Descope-authenticated principals can consent\n\n---\n\n## Rollback Plan\n\nIf production OAuth fails:\n\n1. Set `MCP_EXTERNAL_OAUTH_ENABLED = \"false\"` in prod wrangler.toml\n2. Deploy: `pnpm release:core --env prod --service core --dry-run=false`\n3. Metadata endpoint will report `availability: \"private_alpha\"` again\n4. External clients get 401 on protected endpoints\n\n---\n\n## Success Criteria\n\n- [ ] Test deployment runs green\n- [ ] External v0 can connect without provider credentials\n- [ ] No data leaks to external clients (audit logs clean)\n- [ ] Existing internal flows still work (Customer Zero clients)\n- [ ] Marketplace submission can proceed\n"}