Unclassified

P0: Public OAuth Release Blocker — Implementation Specification

Status: Ready for implementation
Branch: release/public-connector-go-live
Priority: P0 — blocks all downstream release work


Executive Summary

The 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:

  • OAuth architecture exists ✓
  • Client registry and redirect validation exist ✓
  • JWT signing infrastructure exists ✓
  • Missing: User membership binding in the consent flow

Work required: Remove hard-coded Customer Zero tenant assignment and bind OAuth codes to real Descope-authenticated users and their workspace memberships.


Current Blocker Analysis

What's Disabled

# services/mcp-connector/wrangler.toml
MCP_EXTERNAL_OAUTH_ENABLED = "false"  # ← Blocks /oauth/authorize and /oauth/register

# Metadata endpoint reports:
integratewise_availability: "private_alpha"  # ← Indicates not ready for public use

Where Hard-Coding Exists

File: services/gateway/src/descope-auth.ts

if (isCustomerZero) {
  tenantId = "iw-customer-zero"; // ← Hard-coded default
  organizationId = "integratewise";
  if (!roles.length) roles = ["owner"];
}

This 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:

  1. Verify the user has an authenticated Descope session
  2. Resolve their real tenant from Descope principal's tenants array
  3. Validate they are a member of the workspace requested by the OAuth client

Implementation Tasks

Task 1: Bind OAuth Consent to Descope-Authenticated User

File: services/mcp-connector/src/lib/oauth-auth-server.ts

Current flow (line 126-176):

export async function handleAuthorizeConfirm(
  request: Request,
  env: OAuthAuthServerEnv
): Promise<Response> {
  const formData = await request.formData();
  const clientId = formData.get("client_id")?.toString() || "";
  const redirect_uri = formData.get("redirect_uri")?.toString();
  // ... no user validation happens here
  const tenant_id = client.tenant_id; // ← Uses client's pre-registered tenant only
}

Changes needed:

  1. Add Descope session token extraction from cookies or Authorization header
  2. Validate token with Descope management API
  3. Map Descope principal to verify tenant membership
  4. Require user consent form to bind user principal ID to the code

New signature:

export async function handleAuthorizeConfirm(
  request: Request,
  env: OAuthAuthServerEnv,
  descopeToken?: string // Optional: from cookie or Authorization header
): Promise<Response>;

Validation rules:

  • If public OAuth is enabled AND token is provided: validate user membership
  • If public OAuth is disabled: only pre-provisioned Customer Zero clients work
  • Tenant must match: user's tenant ⊆ client's allowed workspace(s)

Task 2: Implement Tenant and Workspace Membership Resolution

File: services/mcp-connector/src/lib/oauth-clients.ts

Add:

export interface OAuthClient {
  client_id: string;
  client_secret_hash: string;
  client_name: string;
  redirect_uris: string[];
  allowed_scopes: string[];
  tenant_id: string;
  grant_types: string[];
  is_active: boolean;
  // NEW: Allow multi-tenant clients
  allowed_tenants?: string[]; // If empty or null: matches client's tenant_id only
  require_membership_validation?: boolean; // NEW: require consent binding
}

Add function:

/**
 * Resolve user's real tenant from Descope principal and validate against
 * the OAuth client's allowed tenants.
 *
 * Returns the resolved tenant_id if valid, otherwise throws.
 */
  client: OAuthClient,
  userTenantId: string,
  userOrganizationId?: string
): string {
  const allowed =
    client.allowed_tenants && client.allowed_tenants.length > 0
      ? client.allowed_tenants
      : [client.tenant_id];

  if (!allowed.includes(userTenantId)) {
    throw new Error(
      `User tenant ${userTenantId} not in client allowed tenants: ${allowed.join(", ")}`
    );
  }

  return userTenantId;
}

Task 3: Validate Redirect URIs and Sanitize Parameters

File: services/mcp-connector/src/lib/oauth-auth-server.ts

Add parameter sanitization in handleAuthorizeConfirm:

// NEW: Validate all form fields before trusting them
const violations: string[] = [];

if (!clientId || clientId.length === 0) {
  violations.push("client_id is required");
}
if (!redirect_uri) {
  violations.push("redirect_uri is required");
}
if (redirect_uri && !isValidUrl(redirect_uri)) {
  violations.push("redirect_uri is not a valid URL");
}

if (violations.length > 0) {
  return new Response(`Invalid request: ${violations.join("; ")}`, { status: 400 });
}

// Redirect validation happens here already (line 150)
if (!isRedirectUriAllowed(client, redirect_uri)) {
  return new Response("Invalid redirect_uri for this client", { status: 400 });
}

Add helper:

function isValidUrl(url: string): boolean {
  try {
    const parsed = new URL(url);
    return parsed.protocol === "http:" || parsed.protocol === "https:";
  } catch {
    return false;
  }
}

Task 4: Enable MCP_EXTERNAL_OAUTH_ENABLED in Test Environment

File: services/mcp-connector/wrangler.toml

Change (line 115):

[env.test.vars]
ENVIRONMENT = "test"
CF_ACCESS_TEAM_DOMAIN = "integratewise.cloudflareaccess.com"
MCP_EXTERNAL_OAUTH_ENABLED = "true"  # ← Change from "false" to "true"
DESCOPE_PROJECT_ID = "P3FsuxiG33B1n0G6601XP9bn75Yp"
TENANT_ID = "iw-customer-zero"
ORG_ID = "integratewise"
CLIENT_ID = "mcp-connector-test"

Keep in prod:

[env.prod.vars]
ENVIRONMENT = "prod"
...
MCP_EXTERNAL_OAUTH_ENABLED = "false"  # ← Stays disabled until manual approval

Task 5: Create Smoke Tests for Public OAuth Flow

File: services/mcp-connector/src/oauth.test.ts (new or extend existing)

Test cases:

  1. ✓ Authorize endpoint requires valid client_id and redirect_uri
  2. ✓ Authorize endpoint validates redirect_uri against registered list
  3. ✓ Consent form displays when external OAuth is enabled
  4. ✓ Consent form binds to authenticated user (Descope token)
  5. ✓ Authorization code is generated with correct tenant_id
  6. ✓ Token exchange fails if code's tenant doesn't match user's tenant
  7. ✓ Refresh token is issued and can be used for token renewal
  8. ✓ Revoked refresh tokens cannot be used

Task 6: Deploy to Test Environment

Checklist:

  • Dry-run: pnpm release:core --env test --dry-run
  • Validate output: connector, mcp-connector, connector-sync, knowledge, continuity, intelligence, workflow, l2, gateway
  • Live deploy: pnpm release:core --env test --dry-run=false
  • Validate in test:
    • Connector catalog loads at https://test-mcp.integratewise.ai/.well-known/oauth-authorization-server
    • GET /oauth/authorize?response_type=code&client_id=test&redirect_uri=... returns 200 HTML
    • POST /oauth/authorize/confirm with valid form returns 302 redirect with code
    • POST /oauth/token with code returns access_token
    • GET /tools with Bearer token returns 200
    • Existing workspace is resolved from Descope principal
    • Provider connection is stored in Spine
    • Initial sync runs
    • Normalized records arrive in Spine
    • Reauthorization works
    • Revocation works
    • Connector failure is visible in logs

Task 7: Enable MCP_EXTERNAL_OAUTH_ENABLED in Production

Only after test validation passes:

[env.prod.vars]
...
MCP_EXTERNAL_OAUTH_ENABLED = "true"  # ← Manually enable AFTER test passes

Deployment command:

pnpm release:core \
  --env prod \
  --service core \
  --dry-run=false \
  --confirm_production=true \
  --test_release_evidence=<URL to successful test workflow run>

Task 8: External v0 Proof-of-Concept

Test requirement: Create blank v0 application and connect it to production IntegrateWise without provider-specific credentials.

Connection flow:

  1. Blank v0 app
  2. Add IntegrateWise connector → redirects to https://mcp.integratewise.ai/oauth/authorize
  3. Sign in with Descope
  4. Consent screen displays workspace selection
  5. Authorize → redirected to v0 app with OAuth code
  6. v0 app exchanges code for access token
  7. v0 app fetches workspace from IntegrateWise MCP
  8. v0 displays: connected workspace, existing connections, Spine records

Validation:

  • No Supabase, D1, Salesforce, Slack, GitHub credentials leaked to v0
  • No provider-specific implementation code in v0
  • External client can read connector catalog
  • External client can read existing connections
  • External client can read Spine records
  • Retest from second external client to confirm isolation

Configuration State Machine

StateMCP_EXTERNAL_OAUTH_ENABLEDUse CaseAvailability
Private AlphafalseInternal team + Customer Zeroprivate_alpha
TesttrueRelease validationenabled
Production (V1)falsePre-launch (disable until ready)private_alpha
Production (V2)truePublic releaseenabled

Key Decisions

  1. No new database schema: Use existing oauth_clients table in D1
  2. No DCR (Dynamic Client Registration): Public clients must pre-register via admin API
  3. No multi-tenancy for v1: Each OAuth client is bound to one workspace (can be extended later)
  4. Refresh tokens enabled: Allow long-lived sessions for external integrations
  5. Descope as authz source: Only Descope-authenticated principals can consent

Rollback Plan

If production OAuth fails:

  1. Set MCP_EXTERNAL_OAUTH_ENABLED = "false" in prod wrangler.toml
  2. Deploy: pnpm release:core --env prod --service core --dry-run=false
  3. Metadata endpoint will report availability: "private_alpha" again
  4. External clients get 401 on protected endpoints

Success Criteria

  • Test deployment runs green
  • External v0 can connect without provider credentials
  • No data leaks to external clients (audit logs clean)
  • Existing internal flows still work (Customer Zero clients)
  • Marketplace submission can proceed