Getting Started
Core Architecture
Link Engine
Analytics & Attribution
Partners & Affiliates
Third-Party Integrations
Identity & Security
Automation & Messaging
Developer Tools
The following files were used as context for generating this wiki page:
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, apps/web/lib/jackson.ts:1-59, apps/web/app/app.dub.co/dashboard/slug/ee/settings/security/saml.tsx:23-130
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
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.
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
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
The opts object configures the BoxyHQ Jackson runtime environment, adjusting paths, audience URIs, database connectivity, and client verification secrets according to NODE_ENV.
Sources: apps/web/lib/jackson.ts:10-34
The constant SAML_PROVIDERS defines the supported enterprise identity providers, mapping their display names, logos, internal identifiers, copy strings, and SCIM protocol options.
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, apps/web/ui/modals/saml-modal.tsx:53-80
The SAML API route exposes three HTTP methods governed by workspace permissions and Zod schema validations:
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.
The connection provisioning flow executes across API validation, BoxyHQ Jackson persistence, and relational database updates in a strict sequence:
createSAMLConnectionSchema.parse() validates that either metadataUrl or encodedRawMetadata is present in the request body.isGenericEmail() checks the session user's email domain; if the domain is generic, a bad_request DubApiError is thrown.jackson() initializes and returns the BoxyHQ Jackson API controller.apiController.createSAMLConnection() provisions the connection with parameters:
encodedRawMetadata: Base64-encoded XML metadata (if uploaded)metadataUrl: Remote metadata URLdefaultRedirectUrl: ${process.env.NEXTAUTH_URL}/auth/samlredirectUrl: process.env.NEXTAUTH_URLtenant: workspace.idproduct: "Dub"prisma.project.update() updates the workspace record with the verified ssoEmailDomain.The client interface relies on SWR data fetching and React component hooks to manage configuration state and modal lifecycles.
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
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, apps/web/ui/modals/saml-modal.tsx:132-205
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, apps/web/app/ee/api/auth/saml/verify/route.tsx:9-68, apps/web/app/app.dub.co/auth/auth/saml/form.tsx:8-21
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.
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 };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.
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 });
}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.
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.
export default function SAMLForm() {
const searchParams = useSearchParams();
useEffect(() => {
const code = searchParams?.get("code");
signIn("saml-idp", {
callbackUrl: "/",
code,
});
}, []);
return null;
}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
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.
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;
};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.
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.
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
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.
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>
);
};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.
When an inbound SCIM request arrives, the route executes a structured call sequence to authenticate, normalize, and dispatch the payload:
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-22req.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-29jackson() initializes the core Jackson controller instance.
Sources: apps/web/app/ee/api/scim/v2.0/...directory/route.ts:31directorySyncController.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-53Note
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
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.
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, apps/web/app/ee/api/scim/v2.0/...directory/route.ts:120-122
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, apps/web/ui/modals/scim-modal.tsx:26-86
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.
Sources: apps/web/app/app.dub.co/dashboard/slug/ee/settings/security/scim.tsx:14-19, apps/web/lib/swr/use-scim.ts:7-29
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
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.
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);
});The RemoveSCIMModal component governs connection teardown. To execute revocation, users must type the exact confirmation string confirm remove scim to unlock the danger button.
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);
}
};