> ## Documentation Index
> Fetch the complete documentation index at: https://docs-dev-actions-triggers-prototype.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

> Migrate your existing SAML Federation Connection from a legacy Auth0 custom domain to a new one, allowing your tenants to leverage Multiple Custom Domain (MCD) capabilities.

# SAML Migration for Custom Domains

export const AuthCodeGroup = ({children, dropdown}) => {
  const [processedChildren, setProcessedChildren] = useState(children);
  useEffect(() => {
    let unsubscribe = null;
    function init() {
      unsubscribe = window.autorun(() => {
        const processChildren = node => {
          if (typeof node === "string") {
            let processedNode = node;
            for (const [key, value] of window.rootStore.variableStore.values.entries()) {
              const escapedKey = key.replaceAll(/[.*+?^${}()|[\]\\]/g, (String.raw)`\$&`);
              processedNode = processedNode.replaceAll(new RegExp(escapedKey, "g"), value);
            }
            return processedNode;
          } else if (Array.isArray(node)) {
            return node.map(processChildren);
          } else if (node && node.props && node.props.children) {
            return {
              ...node,
              props: {
                ...node.props,
                children: processChildren(node.props.children)
              }
            };
          }
          return node;
        };
        setProcessedChildren(processChildren(children));
      });
    }
    if (window.rootStore) {
      init();
    } else {
      window.addEventListener("adu:storeReady", init);
    }
    return () => {
      window.removeEventListener("adu:storeReady", init);
      unsubscribe?.();
    };
  }, [children]);
  return <CodeGroup dropdown={dropdown}>{processedChildren}</CodeGroup>;
};

export const AuthCodeBlock = ({filename, icon, language, highlight, children}) => {
  const [displayText, setDisplayText] = useState(children);
  const [copyText, setCopyText] = useState(children);
  const wrapperRef = React.useRef(null);
  useEffect(() => {
    let unsubscribe = null;
    function init() {
      if (!window.autorun || !window.rootStore) {
        return;
      }
      unsubscribe = window.autorun(() => {
        let processedChildrenForDisplay = children;
        let processedChildrenForCopy = children;
        for (const [key, value] of window.rootStore.variableStore.values.entries()) {
          const escapedKey = key.replaceAll(/[.*+?^${}()|[\]\\]/g, (String.raw)`\$&`);
          let displayValue = value;
          if (key === "{yourClientSecret}" && value !== "{yourClientSecret}") {
            displayValue = value.substring(0, 3) + "*****MASKED*****";
          }
          processedChildrenForDisplay = processedChildrenForDisplay.replaceAll(new RegExp(escapedKey, "g"), displayValue);
          processedChildrenForCopy = processedChildrenForCopy.replaceAll(new RegExp(escapedKey, "g"), value);
        }
        setDisplayText(processedChildrenForDisplay);
        setCopyText(processedChildrenForCopy);
      });
    }
    if (window.rootStore) {
      init();
    } else {
      window.addEventListener("adu:storeReady", init);
    }
    return () => {
      window.removeEventListener("adu:storeReady", init);
      unsubscribe?.();
    };
  }, [children]);
  useEffect(() => {
    if (!wrapperRef.current) return;
    const originalWriteText = navigator.clipboard.writeText.bind(navigator.clipboard);
    let isOverriding = false;
    const handleClick = e => {
      const button = e.target.closest('[data-testid="copy-code-button"]');
      if (!button || !wrapperRef.current.contains(button)) return;
      isOverriding = true;
      navigator.clipboard.writeText = text => {
        if (isOverriding) {
          isOverriding = false;
          navigator.clipboard.writeText = originalWriteText;
          return originalWriteText(copyText);
        }
        return originalWriteText(text);
      };
      setTimeout(() => {
        if (isOverriding) {
          isOverriding = false;
          navigator.clipboard.writeText = originalWriteText;
        }
      }, 100);
    };
    const wrapper = wrapperRef.current;
    wrapper.addEventListener('click', handleClick, true);
    return () => {
      wrapper.removeEventListener('click', handleClick, true);
      if (navigator.clipboard.writeText !== originalWriteText) {
        navigator.clipboard.writeText = originalWriteText;
      }
    };
  }, [copyText]);
  return <div ref={wrapperRef}>
      <CodeBlock filename={filename} icon={icon} language={language} lines highlight={highlight}>
        {displayText}
      </CodeBlock>
    </div>;
};

Migrate your existing SAML Federation Connection from a legacy Auth0 custom domain to a new one, allowing your tenants to leverage Multiple Custom Domain (MCD) capabilities.

External Identity Providers (IdPs) are often hardcoded to your old domain, which requires you to deploy a smart reverse proxy that intercepts the SAML response sent to your old domain and securely point it to a new custom domain.

This ensures a seamless migration for federated users, allowing you to immediately use your new domain without waiting for mandatory configuration updates from your IdP.

## Prerequisites

This process requires configuration via Terraform to set up the Auth0 components and deployment of a Cloudflare Worker for the proxy logic.

Before getting started with your migration, review the requirements below:

* A tenant on an Enterprise plan with MCD enabled.
* Two verified custom domains configured in your Auth0 tenant:
  1. **Old legacy domain** (for the proxy)
  2. **New target domain** (for the application)
* Terraform CLI and `Node.js/npm` installed.
* Cloudflare account with access to the domain hosting your custom domains.

## How it works

This migration strategy uses a smart reverse proxy to bridge the gap between your legacy custom domain and your new domain. This proxy is deployed on the old domain to intercept the SAML authentication response sent by your external Identity Provider (IdP).

This is necessary because the IdP's configuration is hardcoded to your old domain's endpoint. The proxy modifies the SAML payload's control fields (like `Destination` and `Recipient`) to accurately reflect the new custom domain.

Finally, the proxy forwards this corrected payload to the new domain's login endpoint. This enables a zero-downtime cutover to your new domain without requiring manual configuration changes from your IdP partners.

## Setup and configuration

To setup and configure your migration:

1. Clone the migration repository:

```bash theme={null}
git clone https://github.com/abbaspour/auth0-mcd-federation-migration.git
cd auth0-mcd-federation-migration
```

2. Install dependencies:

```bash theme={null}
npm install
```

3. Create a `terraform.auto.tfvars` file in your `tf` firectory with the required credentials and domain information:

```bash theme={null}
# Auth0 Service Provider (SP) variables
auth0_domain = "your-sp-tenant.auth0.com"
auth0_existing_custom_domain = "oldfed.example.com"
auth0_new_custom_domain = "id.example2.com"
auth0_tf_client_id = "your-sp-client-id"
auth0_tf_client_secret = "your-sp-client-secret"

# Auth0 Identity Provider (IDP) variables
auth0_idp_domain = "your-idp-tenant.auth0.com"
auth0_idp_tf_client_id = "your-idp-client-id"
auth0_idp_tf_client_secret = "your-idp-client-secret"

# Cloudflare variables
cloudflare_api_key = "your-cloudflare-api-key"
cloudflare_email = "your-cloudflare-email"
cloudflare_zone_id = "your-cloudflare-zone-id"
```

4. Initialize and apply Terraform:

```bash theme={null}
cd tf
terraform init
terraform apply
```

This creates the necessary SAML application and connection, configures the DNS via Cloudflare, and prepares the environment variables for the worker.

### Deploy the Cloudflare worker

This proxy handles the SAML response interception and redirection logic. To deploy:

1. Deploy the Cloudflare proxy:

```bash theme={null}
cd ..
npx wrangler deploy
```

2. The worker automatically receives the required environment variables (like `AUTH0_EDGE_LOCATION` and `NEW_SP_DOMAIN`) from the Terraform output.

### Temporarily update SAML connection parameters

Your old domain receives the SAML response before being reposted to the new domain. Therefore, the SAML Connection's expected validation parameters must temporarily point to the old domain's callback URL to avoid mismatch errors.

1. Obtain a Management API access token for your service provider tenant, with `read:connections` and `update:connections` scopes:

   ```bash theme={null}
   cd bin/
   export access_token='<sp-tenant-management-api-token>'
   ```

2. Update destination URL:

   ```bash theme={null}
   ./sp-set-destination-url.sh -i <saml-connection-id> -d https://oldfed.example.com/login/callback
   ```

3. Update recipient URL:

   ```bash theme={null}
   ./sp-set-recipient-url.sh -i <saml-connection-id> -r https://oldfed.example.com/login/callback
   ```
