---
title: "Enterprise SSO and SCIM"
description: "Dub integrates enterprise identity management capabilities through BoxyHQ Jackson integration, supporting secure SAML Single Sign-On (SSO) and SCIM 2.0 directory synchronization across enterprise w..."
last_updated: "2026-10-05T05:07:35.164326+00:00"
canonical_url: "https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/authentication-and-security/enterprise-sso-and-scim"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [apps/web/app/app.dub.co/dashboard/slug/ee/settings/security/scim.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/security/scim.tsx)
- [apps/web/app/ee/api/scim/v2.0/...directory/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/scim/v2.0/%5B...directory%5D/route.ts)
- [apps/web/ui/modals/scim-modal.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/scim-modal.tsx)
- [apps/web/lib/swr/use-scim.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/swr/use-scim.ts)
- [apps/web/app/api/workspaces/idOrSlug/scim/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/workspaces/%5BidOrSlug%5D/scim/route.ts)
- [apps/web/app/api/workspaces/idOrSlug/saml/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/workspaces/%5BidOrSlug%5D/saml/route.ts)
- [apps/web/app/app.dub.co/dashboard/slug/ee/settings/security/saml.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/security/saml.tsx)
- [apps/web/lib/auth/options.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/options.ts)
- [apps/web/app/ee/api/auth/saml/verify/route.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/auth/saml/verify/route.tsx)
- [apps/web/lib/jackson.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jackson.ts)
- [apps/web/app/app.dub.co/auth/oauth/authorize/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(auth)/oauth/authorize/page.tsx)
- [apps/web/ui/auth/login/sso-sign-in.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/auth/login/sso-sign-in.tsx)
- [apps/web/app/api/oauth/userinfo/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/userinfo/route.ts)
- [apps/web/app/app.dub.co/auth/auth/saml/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(auth)/auth/saml/page.tsx)
- [packages/utils/src/constants/saml.ts](https://github.com/blade47/dub/blob/HEAD/packages/utils/src/constants/saml.ts)
- [apps/web/app/app.dub.co/dashboard/slug/ee/settings/security/page-client.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/security/page-client.tsx)
- [apps/web/app/app.dub.co/auth/auth/saml/form.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(auth)/auth/saml/form.tsx)
- [apps/web/app/ee/api/auth/saml/authorize/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/auth/saml/authorize/route.ts)
- [apps/web/scripts/dev/data.json](https://github.com/blade47/dub/blob/HEAD/apps/web/scripts/dev/data.json)
- [apps/web/lib/swr/use-saml.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/swr/use-saml.ts)
- [packages/stripe-app/src/views/AppSettings.tsx](https://github.com/blade47/dub/blob/HEAD/packages/stripe-app/src/views/AppSettings.tsx)
- [apps/web/ui/modals/saml-modal.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/saml-modal.tsx)
- [apps/web/lib/dub.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/dub.ts)
- [apps/web/ui/modals/remove-scim-modal.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/remove-scim-modal.tsx)
- [apps/web/app/api/callback/bitly/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/callback/bitly/route.ts)
- [apps/web/lib/api/workspaces/is-saml-enforced-for-email-domain.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/workspaces/is-saml-enforced-for-email-domain.ts)
- [apps/web/app/ee/partners.dub.co/auth-login-register/generic/login/sso-login-button.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(auth-login-register)/(generic)/login/sso-login-button.tsx)
- [apps/web/lib/auth/sso-login-programs.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/sso-login-programs.ts)
- [apps/web/ui/placeholders/feature-graphics/collaboration.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/placeholders/feature-graphics/collaboration.tsx)
- [apps/web/app/app.dub.co/onboarding/signed-in-hint.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(onboarding)/signed-in-hint.tsx)
</details>

## Overview

Dub integrates enterprise identity management capabilities through BoxyHQ Jackson integration, supporting secure SAML Single Sign-On (SSO) and SCIM 2.0 directory synchronization across enterprise workspaces. These security features allow organizations to enforce centralized authentication policies, manage automated user lifecycle provisioning, and secure team access against enterprise security requirements.

Sources: [apps/web/app/app.dub.co/dashboard/slug/ee/settings/security/scim.tsx:14-74](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/security/scim.tsx#L14-L74), [apps/web/lib/jackson.ts:1-59](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jackson.ts#L1-L59), [apps/web/app/app.dub.co/dashboard/slug/ee/settings/security/saml.tsx:23-130](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/security/saml.tsx#L23-L130)

## BoxyHQ Jackson Service Architecture

### BoxyHQ Jackson Service Architecture

### Overview

The enterprise authentication layer builds upon `@boxyhq/saml-jackson` to configure and instantiate controllers for SAML SSO and SCIM 2.0 provisioning. The library initializes options based on environment variables, supporting distinct development and production configurations.

Sources: [apps/web/lib/jackson.ts:1-60](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jackson.ts#L1-L60)

### Controller Initialization and Global Caching

The `jackson()` asynchronous function acts as the central initialization and caching entry point. It verifies whether the global controller instances (`apiController`, `oauthController`, and `directorySyncController`) are attached to `globalThis`. If any controller is missing, it invokes `samlJackson(opts)` to provision them and assigns the resulting controllers to the global namespace before returning them.

```typescript
export async function jackson() {
  if (
    !globalThis.apiController ||
    !globalThis.oauthController ||
    !globalThis.directorySyncController
  ) {
    const ret = await samlJackson(opts);
    globalThis.apiController = ret.connectionAPIController;
    globalThis.oauthController = ret.oauthController;
    globalThis.directorySyncController = ret.directorySyncController;
  }

  return {
    apiController: globalThis.apiController,
    oauthController: globalThis.oauthController,
    directorySyncController: globalThis.directorySyncController,
  };
}
```

Sources: [apps/web/lib/jackson.ts:36-59](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jackson.ts#L36-L59)

> [!NOTE]
> Global controller caching prevents multiple redundant initializations across serverless function warm starts and hot-reloading cycles in development.

Sources: [apps/web/lib/jackson.ts:42-53](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jackson.ts#L42-L53)

### Jackson Configuration Options

The `opts` object configures the BoxyHQ Jackson runtime environment, adjusting paths, audience URIs, database connectivity, and client verification secrets according to `NODE_ENV`.

| Option Property | Production Value | Development Value | Purpose |
| :--- | :--- | :--- | :--- |
| `externalUrl` | `"https://api.dub.co"` | `APP_DOMAIN_WITH_NGROK` | Base external URL for metadata and endpoints |
| `samlPath` | `"/auth/saml/callback"` | `"/api/auth/saml/callback"` | Endpoint path for SAML assertion consumer service (ACS) |
| `scimPath` | `"/scim/v2.0"` | `"/api/scim/v2.0"` | Custom SCIM 2.0 endpoint path for directory sync |
| `samlAudience` | `"https://saml.dub.co"` | `"https://saml.dub.co"` | Expected SAML audience URI |
| `db.engine` | `"planetscale"` | `"planetscale"` | Database driver engine |
| `db.type` | `"mysql"` | `"mysql"` | Database type |
| `db.url` | `process.env.DATABASE_URL` | `process.env.DATABASE_URL` | Connection string for persistence |
| `db.ssl` | `{ rejectUnauthorized: false }` | `{ rejectUnauthorized: false }` | SSL configuration for database connection |
| `idpEnabled` | `true` | `true` | Enables IdP-initiated SSO |
| `clientSecretVerifier` | `process.env.NEXTAUTH_SECRET` | `process.env.NEXTAUTH_SECRET` | Secret used for client verification |

Sources: [apps/web/lib/jackson.ts:10-34](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jackson.ts#L10-L34)

### Supported SAML and SCIM Identity Providers

The constant `SAML_PROVIDERS` defines the supported enterprise identity providers, mapping their display names, logos, internal identifiers, copy strings, and SCIM protocol options.

| Provider Name | Identifier (`saml`) | SCIM Identifier (`scim`) | Modal Copy Keys | Work-in-Progress (`wip`) |
| :--- | :--- | :--- | :--- | :--- |
| Okta | `"okta"` | `"okta-scim-v2"` | Metadata URL, SCIM 2.0 Base URL, OAuth Bearer Token | `false` |
| Entra ID (formerly Azure AD) | `"azure"` | `"azure-scim-v2"` | App Federation Metadata URL, Tenant URL, Secret Token | `false` |
| Google | `"google"` | `"google"` | XML Metadata File, SCIM 2.0 Base URL, OAuth Bearer Token | `false` |

Sources: [packages/utils/src/constants/saml.ts:1-38](https://github.com/blade47/dub/blob/HEAD/packages/utils/src/constants/saml.ts#L1-L38)

## SAML SSO Configuration and Management

### Overview

Workspace SAML integration handles the complete lifecycle of configuring, retrieving, and removing Enterprise Single Sign-On connections via the Dub API and Jackson controller. Workspaces on the Enterprise plan with `workspaces.write` permissions can provision identity provider connections using either remote XML metadata URLs or uploaded raw XML metadata files.

Sources: [apps/web/app/api/workspaces/idOrSlug/saml/route.ts:30-97](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/workspaces/%5BidOrSlug%5D/saml/route.ts#L30-L97), [apps/web/ui/modals/saml-modal.tsx:53-80](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/saml-modal.tsx#L53-L80)

### API Lifecycle and Request Validation

The SAML API route exposes three HTTP methods governed by workspace permissions and Zod schema validations:

| HTTP Method | Route Endpoint | Required Permissions | Required Plan | Purpose |
| :--- | :--- | :--- | :--- | :--- |
| `GET` | `/api/workspaces/[idOrSlug]/saml` | `workspaces.read` | — | Retrieves configured SAML connections, issuer URI, and ACS callback URL |
| `POST` | `/api/workspaces/[idOrSlug]/saml` | `workspaces.write` | `enterprise` | Provisions a new SAML connection and associates the email domain |
| `DELETE` | `/api/workspaces/[idOrSlug]/saml` | `workspaces.write` | — | Deletes SAML connections and clears workspace SSO enforcement settings |

Sources: [apps/web/app/api/workspaces/idOrSlug/saml/route.ts:30-150](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/workspaces/%5BidOrSlug%5D/saml/route.ts#L30-L150)

> [!WARNING]
> When provisioning via `POST`, users must be authenticated with a non-generic corporate email address. Generic email domains are rejected to prevent insecure tenant-to-domain bindings.

Sources: [apps/web/app/api/workspaces/idOrSlug/saml/route.ts:61-69](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/workspaces/%5BidOrSlug%5D/saml/route.ts#L61-L69)

### SAML Connection Provisioning Workflow

The connection provisioning flow executes across API validation, BoxyHQ Jackson persistence, and relational database updates in a strict sequence:

1. `createSAMLConnectionSchema.parse()` validates that either `metadataUrl` or `encodedRawMetadata` is present in the request body.
2. `isGenericEmail()` checks the session user's email domain; if the domain is generic, a `bad_request` DubApiError is thrown.
3. `jackson()` initializes and returns the BoxyHQ Jackson API controller.
4. `apiController.createSAMLConnection()` provisions the connection with parameters:
   - `encodedRawMetadata`: Base64-encoded XML metadata (if uploaded)
   - `metadataUrl`: Remote metadata URL
   - `defaultRedirectUrl`: `${process.env.NEXTAUTH_URL}/auth/saml`
   - `redirectUrl`: `process.env.NEXTAUTH_URL`
   - `tenant`: `workspace.id`
   - `product`: `"Dub"`
5. `prisma.project.update()` updates the workspace record with the verified `ssoEmailDomain`.

Sources: [apps/web/app/api/workspaces/idOrSlug/saml/route.ts:10-90](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/workspaces/%5BidOrSlug%5D/saml/route.ts#L10-L90)

### Client-Side State Management and Modals

The client interface relies on SWR data fetching and React component hooks to manage configuration state and modal lifecycles.

```typescript
export default function useSAML() {
  const { id: workspaceId } = useWorkspace();

  const { data, isLoading, mutate } = useSWR<{ connections: SAMLSSORecord[] }>(
    workspaceId && `/api/workspaces/${workspaceId}/saml`,
    fetcher,
    {
      keepPreviousData: true,
    },
  );

  const configured = useMemo(() => {
    return data?.connections && data.connections.length > 0;
  }, [data]);

  return {
    saml: data as { connections: SAMLSSORecord[] },
    provider: configured
      ? data!.connections[0].idpMetadata.friendlyProviderName
      : null,
    configured,
    loading: isLoading,
    mutate,
  };
}
```

Sources: [apps/web/lib/swr/use-saml.ts:7-31](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/swr/use-saml.ts#L7-L31)

The UI switches between the configuration modal (`SAMLModal`) and the removal modal (`RemoveSAMLModal`) depending on whether `configured` evaluates to true. For Google identity provider integrations, the modal renders a file upload dropzone for `.xml` metadata files which are read via `FileReader` and converted to base64 strings before submission; all other providers render a standard URL input field.

Sources: [apps/web/app/app.dub.co/dashboard/slug/ee/settings/security/saml.tsx:117-120](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/security/saml.tsx#L117-L120), [apps/web/ui/modals/saml-modal.tsx:132-205](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/saml-modal.tsx#L132-L205)

## SAML Authorization and Assertion Verification

### Overview

The SAML authorization and verification subsystem handles runtime SAML login initiation, identity provider (IdP) callback processing, assertion verification, and security enforcement through dedicated API routes and client components. It orchestrates interactions between NextAuth, Prisma, Upstash rate limiting, and BoxyHQ Jackson controllers to securely authenticate workspace users.

Sources: [apps/web/app/ee/api/auth/saml/authorize/route.ts:5-25](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/auth/saml/authorize/route.ts#L5-L25), [apps/web/app/ee/api/auth/saml/verify/route.tsx:9-68](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/auth/saml/verify/route.tsx#L9-L68), [apps/web/app/app.dub.co/auth/auth/saml/form.tsx:8-21](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(auth)/auth/saml/form.tsx#L8-L21)

### Authorization and Endpoint Request Handling

The SAML authorization endpoint supports both `GET` and `POST` HTTP requests via a shared `handler` function. It retrieves query parameters or JSON payloads depending on the request method, invokes the BoxyHQ Jackson `oauthController.authorize()`, and returns either a `302` redirect or an HTML form response.

```typescript
const handler = async (req: Request) => {
  const { oauthController } = await jackson();

  const requestParams =
    req.method === "GET" ? getSearchParams(req.url) : await req.json();

  const { redirect_url, authorize_form } =
    await oauthController.authorize(requestParams);

  if (redirect_url) {
    return NextResponse.redirect(redirect_url, {
      status: 302,
    });
  } else {
    return new Response(authorize_form, {
      headers: {
        "Content-Type": "text/html; charset=utf-8",
        },
    });
  }
};

export { handler as GET, handler as POST };
```

Sources: [apps/web/app/ee/api/auth/saml/authorize/route.ts:5-27](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/auth/saml/authorize/route.ts#L5-L27)

### Verification and Rate Limiting

The verification endpoint (`POST /api/auth/saml/verify`) validates incoming workspace slugs, enforces rate limiting via Upstash, and verifies active SAML connections before completing identity checks.

```typescript
export async function POST(req: Request) {
  const { apiController } = await jackson();

  const { slug } = await req.json();

  if (!slug) {
    return NextResponse.json(
      { error: "No workspace slug provided." },
      { status: 400 },
    );
  }

  try {
    await assertRateLimit({
      policy: RATELIMIT_POLICIES.samlVerify,
      identifier: await getIP(),
    });
  } catch (error) {
    if (error instanceof DubApiError && error.code === "rate_limit_exceeded") {
      return NextResponse.json(
        {
          error: error.message,
        },
        { status: 429 },
      );
    }

    throw error;
  }

  const workspace = await prisma.project.findUnique({
    where: { slug },
    select: { id: true },
  });

  if (!workspace) {
    return NextResponse.json(
      { error: "No SSO connection found for this workspace." },
      { status: 404 },
    );
  }

  const connections = await apiController.getConnections({
    tenant: workspace.id,
    product: "Dub",
  });

  if (!connections || connections.length === 0) {
    return NextResponse.json(
      { error: "No SSO connection found for this workspace." },
      { status: 404 },
    );
  }

  const data = {
    workspaceId: workspace.id,
  };

  return NextResponse.json({ data });
}
```

Sources: [apps/web/app/ee/api/auth/saml/verify/route.tsx:9-68](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/auth/saml/verify/route.tsx#L9-L68)

> [!WARNING]
> The verification endpoint strictly evaluates rate limit policies using `RATELIMIT_POLICIES.samlVerify` based on the requester's IP address. Exceeding this threshold immediately returns a `429` HTTP response with the corresponding error message.

Sources: [apps/web/app/ee/api/auth/saml/verify/route.tsx:22-37](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/auth/saml/verify/route.tsx#L22-L37)

### Client-Side SAML Callback Processing

The client authentication page renders an empty state with a loading spinner while `SAMLIDPForm` executes its effect hook. The form reads the authorization code from search parameters and triggers the NextAuth `saml-idp` sign-in provider.

```typescript
export default function SAMLForm() {
  const searchParams = useSearchParams();

  useEffect(() => {
    const code = searchParams?.get("code");

    signIn("saml-idp", {
      callbackUrl: "/",
      code,
    });
  }, []);

  return null;
}
```

Sources: [apps/web/app/app.dub.co/auth/auth/saml/form.tsx:8-21](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(auth)/auth/saml/form.tsx#L8-L21)

## Email Domain SAML Enforcement

### Overview

Email domain SAML enforcement integrates single sign-on policy checks directly into the NextAuth credentials provider and client-side authentication flow. When users attempt to sign in using standard email and password credentials, the authentication handler checks whether SAML is enforced for their email domain. If enforced, password-based authentication is blocked, requiring the user to authenticate through their enterprise identity provider.

Sources: [apps/web/lib/auth/options.ts:256-286](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/options.ts#L256-L286)

### Domain-Level SAML Enforcement Check

The policy check is performed by `isSamlEnforcedForEmailDomain`, which inspects the request headers and email address. It rejects requests if the hostname is not an application hostname or if the email address belongs to a generic provider. Otherwise, it queries the database to determine whether a workspace has configured and enforced an `ssoEmailDomain`.

```typescript
export const isSamlEnforcedForEmailDomain = async (email: string) => {
  const hostname = (await headers()).get("host");
  const emailDomain = email.split("@")[1].toLocaleLowerCase();

  if (
    !hostname ||
    !emailDomain ||
    !isAppHostname(hostname) ||
    isGenericEmail(email)
  ) {
    return false;
  }

  const workspace = await prisma.project.findUnique({
    where: {
      ssoEmailDomain: emailDomain,
    },
    select: {
      ssoEnforcedAt: true,
    },
  });

  if (workspace?.ssoEnforcedAt) {
    return true;
  }

  return false;
};
```

Sources: [apps/web/lib/api/workspaces/is-saml-enforced-for-email-domain.ts:7-34](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/workspaces/is-saml-enforced-for-email-domain.ts#L7-L34)

> [!NOTE]
> Generic email providers (such as public email services handled by `isGenericEmail`) bypass domain SAML enforcement to prevent locking out individual accounts that do not belong to an enterprise domain.

Sources: [apps/web/lib/api/workspaces/is-saml-enforced-for-email-domain.ts:11-18](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/workspaces/is-saml-enforced-for-email-domain.ts#L11-L18)

### Credentials Provider Guard and Error Handling

Inside the NextAuth credentials provider (`id: "credentials"`), authentication requests undergo rate limiting followed immediately by the SSO enforcement check. If `isSamlEnforcedForEmailDomain` evaluates to true, the provider throws a `require-saml-sso` error, bypassing password verification entirely.

```typescript
        await assertRateLimit({
          policy: RATELIMIT_POLICIES.login,
          identifier: email.trim().toLowerCase(),
        });

        // SSO enforcement check
        const ssoEnforced = await isSamlEnforcedForEmailDomain(email);

        if (ssoEnforced) {
          throw new Error("require-saml-sso");
        }
```

Sources: [apps/web/lib/auth/options.ts:275-286](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/options.ts#L275-L286)

### Client-Side SSO Routing and Workspace Verification

The client-side `SSOSignIn` component manages workspace slug inputs and verification requests prior to initiating the NextAuth SAML sign-in flow. When submitted, it posts the workspace slug to `/api/auth/saml/verify`, retrieves the workspace identifier, and invokes `signIn("saml")` with the corresponding tenant parameters.

```typescript
export const SSOSignIn = () => {
  const { isMobile } = useMediaQuery();

  const {
    setClickedMethod,
    clickedMethod,
    authMethod,
    setLastUsedAuthMethod,
    setShowSSOOption,
    showSSOOption,
  } = useContext(LoginFormContext);

  return (
    <form
      onSubmit={async (e) => {
        e.preventDefault();
        setClickedMethod("saml");
        fetch("/api/auth/saml/verify", {
          method: "POST",
          body: JSON.stringify({ slug: e.currentTarget.slug.value }),
        }).then(async (res) => {
          const { data, error } = await res.json();
          if (error) {
            toast.error(error);
            setClickedMethod(undefined);
            return;
          }
          setLastUsedAuthMethod("saml");
          await signIn("saml", undefined, {
            tenant: data.workspaceId,
            product: "Dub",
          });
        });
      }}
      className="flex flex-col space-y-3"
    >
      {showSSOOption && (
        <div>
          {authMethod !== "saml" && (
            <div className="mb-4 mt-1 border-t border-neutral-300" />
          )}
          <div className="flex items-center space-x-2">
            <h2 className="text-sm font-medium text-neutral-900">
              Workspace Slug
            </h2>
            <InfoTooltip content="This is your workspace's unique identifier on Dub. E.g. app.dub.co/acme is 'acme'." />
          </div>
          <input
            id="slug"
            name="slug"
            autoFocus={!isMobile}
            type="text"
            placeholder="my-team"
            autoComplete="off"
            required
            className="mt-1 block w-full appearance-none rounded-md border border-neutral-300 px-3 py-2 placeholder-neutral-400 shadow-sm focus:border-black focus:outline-none focus:ring-black sm:text-sm"
          />
        </div>
      )}

      <Button
        text="Continue with SAML SSO"
        variant="secondary"
        icon={<Lock className="size-4" />}
        {...(!showSSOOption && {
          type: "button",
          onClick: (e) => {
            e.preventDefault();
            setShowSSOOption(true);
          },
        })}
        loading={clickedMethod === "saml"}
        disabled={clickedMethod && clickedMethod !== "saml"}
      />
    </form>
  );
};
```

Sources: [apps/web/ui/auth/login/sso-sign-in.tsx:10-86](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/auth/login/sso-sign-in.tsx#L10-L86)

## SCIM Directory Sync Endpoint Processing

### Overview

SCIM 2.0 provisioning requests from identity providers target the dynamic API route handler at `apps/web/app/(ee)/api/scim/v2.0/[...directory]/route.ts`. The handler extracts bearer tokens from the authorization header, parses URL query filters, and passes the operation payload to the BoxyHQ Jackson `directorySyncController`.

Sources: [apps/web/app/ee/api/scim/v2.0/...directory/route.ts:13-56](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/scim/v2.0/%5B...directory%5D/route.ts#L13-L56)

### Request Execution Flow

When an inbound SCIM request arrives, the route executes a structured call sequence to authenticate, normalize, and dispatch the payload:

1. `headers()` and `getSearchParams(req.url)` extract the authorization secret and query parameters (`count`, `startIndex`, `filter`).
Sources: [apps/web/app/ee/api/scim/v2.0/...directory/route.ts:18-22](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/scim/v2.0/%5B...directory%5D/route.ts#L18-L22)
2. `req.json()` parses the request body, defaulting to an empty object if parsing fails.
Sources: [apps/web/app/ee/api/scim/v2.0/...directory/route.ts:25-29](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/scim/v2.0/%5B...directory%5D/route.ts#L25-L29)
3. `jackson()` initializes the core Jackson controller instance.
Sources: [apps/web/app/ee/api/scim/v2.0/...directory/route.ts:31](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/scim/v2.0/%5B...directory%5D/route.ts#L31)
4. `directorySyncController.requests.handle(request, handleEvents)` processes the resource query or mutation and triggers lifecycle event callbacks.
Sources: [apps/web/app/ee/api/scim/v2.0/...directory/route.ts:50-53](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/scim/v2.0/%5B...directory%5D/route.ts#L50-L53)

> [!NOTE]
> Resource paths containing `"Users"` map to `resourceType: "users"`, while all other path segments resolve to `"groups"`.
> Sources: [apps/web/app/ee/api/scim/v2.0/...directory/route.ts:39](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/scim/v2.0/%5B...directory%5D/route.ts#L39)

### Directory Sync Event Handling and User Provisioning

The `handleEvents` asynchronous function receives directory synchronization events and updates workspace memberships using Prisma. Enterprise plan validation ensures events only process for active enterprise workspaces containing email attributes.

| SCIM Action / Condition | Target State | Prisma Operation | Sources |
| :--- | :--- | :--- | :--- |
| `user.created` (New user) | User invited to workspace | `inviteUser()` | [apps/web/app/ee/api/scim/v2.0/...directory/route.ts:96-101](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/scim/v2.0/%5B...directory%5D/route.ts#L96-L101) |
| `user.updated` (`active === true` / `"True"`) | User activated | `inviteUser()` (if unassigned) | [apps/web/app/ee/api/scim/v2.0/...directory/route.ts:104-115](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/scim/v2.0/%5B...directory%5D/route.ts#L104-L115) |
| `user.updated` (`active === false` / `"False"`) | User deactivated | `prisma.projectUsers.delete()` & `prisma.projectInvite.delete()` | [apps/web/app/ee/api/scim/v2.0/...directory/route.ts:118-144](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/scim/v2.0/%5B...directory%5D/route.ts#L118-L144) |
| `user.deleted` | User deleted | `prisma.projectUsers.delete()` & `prisma.projectInvite.delete()` | [apps/web/app/ee/api/scim/v2.0/...directory/route.ts:118-144](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/scim/v2.0/%5B...directory%5D/route.ts#L118-L144) |

Sources: [apps/web/app/ee/api/scim/v2.0/...directory/route.ts:61-146](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/scim/v2.0/%5B...directory%5D/route.ts#L61-L146)

> [!WARNING]
> Azure AD sends boolean activation states as string values (`"True"` or `"False"`). Event guards explicitly evaluate both boolean primitives and string equivalents to prevent silent de-provisioning failures.
> Sources: [apps/web/app/ee/api/scim/v2.0/...directory/route.ts:106-108](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/scim/v2.0/%5B...directory%5D/route.ts#L106-L108), [apps/web/app/ee/api/scim/v2.0/...directory/route.ts:120-122](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/scim/v2.0/%5B...directory%5D/route.ts#L120-L122)

## Enterprise SCIM UI and Lifecycle

### Overview

The client dashboard provides interface elements for configuring, inspecting, and dismantling SCIM directory synchronization. The React component tree orchestrates state across SWR hooks, workspace permission checkers, and configuration modals. Users with `workspaces.write` permissions on enterprise plans can initiate setup, retrieve directory base URLs, and securely remove synchronization configurations.

Sources: [apps/web/app/app.dub.co/dashboard/slug/ee/settings/security/scim.tsx:14-25](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/security/scim.tsx#L14-L25), [apps/web/ui/modals/scim-modal.tsx:26-86](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/scim-modal.tsx#L26-L86)

### SWR State Hook and Component Composition

The `useSCIM` hook manages data fetching against `/api/workspaces/{id}/scim` with SWR, exposing directory payloads, the configured identity provider, loading indicators, and mutation triggers. The parent `SCIM` component evaluates workspace plan limits and role-based access before rendering interactive UI controls.

| Hook / Component | File Source | Primary Responsibility |
| :--- | :--- | :--- |
| `useSCIM` | [apps/web/lib/swr/use-scim.ts:7-29](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/swr/use-scim.ts#L7-L29) | Fetches SCIM directory records and computes `configured` state boolean. |
| `SCIM` | [apps/web/app/app.dub.co/dashboard/slug/ee/settings/security/scim.tsx:14-181](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/security/scim.tsx#L14-L181) | Main dashboard settings card displaying configuration status, dropdown menus, and triggers. |
| `SCIMModal` | [apps/web/ui/modals/scim-modal.tsx:26-32](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/scim-modal.tsx#L26-L32) | Modal dialog for selecting a provider and generating directory endpoints. |
| `RemoveSCIMModal` | [apps/web/ui/modals/remove-scim-modal.tsx:15-21](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/remove-scim-modal.tsx#L15-L21) | Modal dialog requiring explicit text confirmation to revoke directory synchronization. |

Sources: [apps/web/app/app.dub.co/dashboard/slug/ee/settings/security/scim.tsx:14-19](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/security/scim.tsx#L14-L19), [apps/web/lib/swr/use-scim.ts:7-29](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/swr/use-scim.ts#L7-L29)

> [!NOTE]
> If a workspace lacks an enterprise subscription, the configuration button displays a disabled tooltip prompting the user to upgrade or contact sales.
> Sources: [apps/web/app/app.dub.co/dashboard/slug/ee/settings/security/scim.tsx:147-162](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/security/scim.tsx#L147-L162)

### Configuration Modals and Directory Key Generation

When configuring a provider, the `SCIMModal` component issues an HTTP POST request to `/api/workspaces/{id}/scim` containing the selected provider slug and optional `currentDirectoryId`. Successful requests mutate the local SWR cache and display success notifications via `sonner`.

```typescript
fetch(`/api/workspaces/${id}/scim`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    provider: e.currentTarget.provider.value,
    ...(configured && {
      currentDirectoryId: scim.directories[0].id,
    }),
  }),
}).then(async (res) => {
  if (res.ok) {
    await mutate();
    toast.success("Successfully configured SCIM");
  } else {
    const { error } = await res.json();
    toast.error(error.message);
  }
  setSubmitting(false);
});
```
Sources: [apps/web/ui/modals/scim-modal.tsx:94-114](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/scim-modal.tsx#L94-L114)

### Connection Revocation Lifecycle

The `RemoveSCIMModal` component governs connection teardown. To execute revocation, users must type the exact confirmation string `confirm remove scim` to unlock the danger button.

```typescript
const removeSCIM = async () => {
  setRemoving(true);
  if (!scim?.directories[0]) {
    toast.error("No SCIM directories found");
    setRemoving(false);
    return;
  }
  const { id } = scim.directories[0];
  const params = new URLSearchParams({
    directoryId: id,
  });

  const res = await fetch(`/api/workspaces/${workspaceId}/scim?${params}`, {
    method: "DELETE",
  });
  setRemoving(false);
  if (res.ok) {
    await mutate();
    setShowRemoveSCIMModal(false);
    toast.success("SCIM directory removed successfully");
  } else {
    const { error } = await res.json();
    toast.error(error.message);
  }
};
```
Sources: [apps/web/ui/modals/remove-scim-modal.tsx:37-61](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/remove-scim-modal.tsx#L37-L61)

## Related

- [Authentication and Sessions](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/authentication-and-security/authentication-and-sessions)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/llms.txt) for all pages in this wiki.
