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 implements a robust, multi-layered authentication and session management architecture built on top of NextAuth.js, Prisma, and Next.js Edge Middleware. Designed to secure public API routes, user dashboards, and enterprise workspaces, the system orchestrates diverse authentication flows ranging from passwordless magic links and OAuth providers to credentials and enforced SAML Single Sign-On (SSO).
The architecture solves complex access control requirements by enforcing granular verification checks at both the application edge and server runtime layers. Key design decisions include stateless JWT session strategies with secure HTTP-only cookies, robust administrative impersonation hooks, token-based API authentication with rate limiting, and automated lifecycle management for passwords and reset tokens.
Sources: apps/web/lib/auth/options.ts:377-390, apps/web/lib/auth/session.ts:25-136, apps/web/lib/middleware/utils/get-user-via-token.ts:5-15
Dub exposes its authentication endpoints via a public Next.js API route handler that wraps the NextAuth configuration using NextAuth(authOptions), exporting both GET and POST methods to manage the complete authentication lifecycle.
The NextAuth instance is configured with the CustomPrismaAdapter(prisma) adapter and relies on a JSON Web Token (jwt) session strategy. Session persistence is secured via HTTP-only cookies configured with lax same-site rules and conditional domain and security flags depending on the deployment environment.
Sources: apps/web/lib/auth/options.ts:375-390
Sources: apps/web/lib/auth/options.ts:375-394
Warning
When working on localhost, the cookie domain configuration property must be omitted entirely rather than set to localhost, or browser cookie validation will reject session persistence.
Sources: apps/web/lib/auth/options.ts:385-386
The authentication options define comprehensive callback hooks and event triggers that execute during sign-in, token generation, and session population.
The execution flow during sign-in proceeds through specific validation steps:
signIn callback: Receives user, account, and profile. It checks if user.email is absent or blacklisted via isBlacklistedEmail(user.email), returning false if invalid.
Sources: apps/web/lib/auth/options.ts:395-399user.lockedAt is populated, it throws an exceeded-login-attempts error.
Sources: apps/web/lib/auth/options.ts:401-403account?.provider === "email" via consumeAdminImpersonation). If not impersonating and provider is not SAML or credentials, it verifies whether SSO is enforced for the email domain via isSamlEnforcedForEmailDomain(user.email), throwing require-saml-sso if required.
Sources: apps/web/lib/auth/options.ts:405-420saml-idp providers, it extracts the target tenant workspace, validates domain matching against ssoEmailDomain, upserts the user into projectUsers, and deletes pending project invites.
Sources: apps/web/lib/auth/options.ts:422-529Tip
The jwt callback handles session update triggers by querying Prisma for fresh user metadata (name, email, image, isMachine, defaultWorkspace, defaultPartnerId) when trigger === "update".
Sources: apps/web/lib/auth/options.ts:532-567
Authentication in Dub supports multiple access vectors including email verification links, traditional password credentials, and SAML Single Sign-On (SSO). The login interface coordinates these flows dynamically through LoginForm and provider-specific subcomponents, handling account existence checks, error code mappings, and strict security enforcement before delegating to NextAuth.
Sources: apps/web/ui/auth/login/login-form.tsx:21-50, apps/web/ui/auth/login/email-sign-in.tsx:13-37
When a user initiates sign-in using the email form, the submission follows a strict asynchronous validation and dispatch sequence:
onSubmit: Intercepts form submission and prevents default browser behavior.
Sources: apps/web/ui/auth/login/email-sign-in.tsx:41-43checkAccountExistsAction: If showPasswordField is false, it executes an asynchronous server action passing { email } to query account status.
Sources: apps/web/ui/auth/login/email-sign-in.tsx:46-48result.data, extracting accountExists, hasPassword, and requireSAML. If requireSAML is true, it aborts and displays an error toast. If accountExists and hasPassword are both true, it sets setShowPasswordField(true) and returns.
Sources: apps/web/ui/auth/login/email-sign-in.tsx:53-66signIn: If the password field is visible and populated, it selects the "credentials" provider; otherwise, it falls back to the magic link "email" provider. It then invokes NextAuth's signIn(provider, { email, redirect: false, callbackUrl, ...password }).
Sources: apps/web/ui/auth/login/email-sign-in.tsx:77-102response.error. If present, it maps error codes via errorCodes and resets clickedMethod. On success, it updates setLastUsedAuthMethod("email"). If "email", it triggers a success toast; if "credentials", it routes the user via router.push.
Sources: apps/web/ui/auth/login/email-sign-in.tsx:104-134During sign-in, the system verifies whether corporate identity federation is mandatory for the user's email domain. The check executes via isSamlEnforcedForEmailDomain(email), which inspects incoming headers for the request hostname, extracts the lowercase email domain, filters out generic email providers, and queries Prisma for a workspace matching ssoEmailDomain where ssoEnforcedAt is populated.
Sources: apps/web/lib/api/workspaces/is-saml-enforced-for-email-domain.ts:7-34, apps/web/lib/auth/options.ts:408-420
Warning
If isSamlEnforcedForEmailDomain returns true for a user attempting standard login, the signIn callback throws a require-saml-sso error, halting standard authentication and requiring identity provider redirection.
Sources: apps/web/lib/auth/options.ts:415-419
IdP-initiated SAML login flows are handled separately by SAMLForm, which extracts the authorization code search parameter on mount and invokes signIn("saml-idp", { callbackUrl: "/", code }).
Server-side session resolution and route protection are handled via NextAuth integration, wrapper functions, and request-level token parsing. The core session utility exposes getSession(), which invokes getServerSession(authOptions) returning a Session object containing user attributes such as id, name, email, image, isMachine, defaultWorkspace, and defaultPartnerId. Unauthenticated route access can be explicitly prohibited using throwIfAuthenticated(), which inspects active sessions and throws an error if an active session exists.
withSession API WrapperThe withSession function wraps API route handlers to enforce authentication, handle rate-limiting, and populate request context with session data.
Sources: apps/web/lib/auth/session.ts:11-136
Warning
If an Authorization header is provided without the exact Bearer prefix, withSession immediately throws a bad_request DubApiError rather than falling back to cookie-based session cookies.
Sources: apps/web/lib/auth/session.ts:38-47
API routes and OAuth endpoints parse authorization credentials using getAuthTokenOrThrow(req, type). This helper extracts the Authorization header, validates its presence, and strips the specified auth type prefix (defaulting to "Bearer").
Sources: apps/web/lib/auth/utils.ts:23-38
OAuth access tokens are validated against RestrictedToken database records in the user info endpoint.
// GET /api/oauth/userinfo - get user info by access token
export async function GET(req: NextRequest) {
try {
const accessToken = getAuthTokenOrThrow(req);
const tokenRecord = await prisma.restrictedToken.findFirst({
where: {
hashedKey: await hashToken(accessToken),
expires: {
gte: new Date(),
},
installationId: {
not: null,
},
},
select: {
user: {
select: {
id: true,
name: true,
image: true,
},
},
project: {
select: {
id: true,
name: true,
slug: true,
logo: true,
},
},
},
});
if (!tokenRecord) {
throw new DubApiError({
code: "unauthorized",
message: "Access token not found or expired.",
});
}
const { user } = tokenRecord;
const userInfo = {
id: user.id,
name: user.name,
image: user.image,
workspace: {
id: prefixWorkspaceId(tokenRecord.project.id),
slug: tokenRecord.project.slug,
name: tokenRecord.project.name,
logo: tokenRecord.project.logo,
},
};
return NextResponse.json(userInfo, {
headers: CORS_HEADERS,
});
} catch (e) {
return handleAndReturnErrorResponse(e, CORS_HEADERS);
}
}Protected routes leverage withSession to retrieve or manage user and token resources safely.
Edge-level request processing handles subdomain routing, token extraction, and workspace access control before requests reach application page handlers. The main entry point in apps/web/middleware.ts intercepts requests matching the configured matcher pattern—excluding API routes, Next.js internal paths, and static files—and directs traffic based on the requesting hostname.
Sources: apps/web/middleware.ts:20-44
When a request arrives at the edge, middleware() parses the request context using parse(req) and routes it to specialized handlers depending on the matching hostname or path condition.
Sources: apps/web/middleware.ts:34-89
For application requests targeting app.dub.co, AppMiddleware processes authentication state by invoking getUserViaToken(req). This helper uses NextAuth's getToken with process.env.NEXTAUTH_SECRET to extract and return the authenticated user payload from the encrypted session cookie.
export async function getUserViaToken(req: NextRequest) {
const session = (await getToken({
req,
secret: process.env.NEXTAUTH_SECRET,
})) as {
email?: string;
user?: UserProps;
};
return session?.user;
}The execution flow within AppMiddleware proceeds through several distinct branches:
/embed invoke EmbedMiddleware(req), while public paths such as /marketplace, /share/, /deeplink/, /unsubscribe/, and /auth/reset-password/ bypass authentication checks./login with a ?next= query parameter preserving the attempted destination.ONBOARDING_WINDOW_SECONDS) lacking a default workspace or pending invites, the middleware evaluates cached onboarding steps via onboardingStepCache and directs the user through setup./, /links, /analytics, /settings, etc.) delegate access enforcement to WorkspacesMiddleware(req, user).When users access application roots or top-level settings, WorkspacesMiddleware resolves active workspace context and handles open-redirect protection.
export async function WorkspacesMiddleware(req: NextRequest, user: UserProps) {
const { path, searchParamsObj, searchParamsString } = parse(req);
if (
searchParamsObj.next &&
isValidInternalRedirect({
redirectPath: searchParamsObj.next,
currentUrl: req.url,
})
) {
return NextResponse.redirect(new URL(searchParamsObj.next, req.url));
}
const defaultWorkspace = await getDefaultWorkspace(user);
if (defaultWorkspace) {
let redirectPath = path;
if (["/", "/login", "/register"].includes(path)) {
redirectPath = "";
} else if (isTopLevelSettingsRedirect(path)) {
redirectPath = `/settings/${path}`;
}
if (!redirectPath) {
const product = await getWorkspaceProduct(defaultWorkspace);
redirectPath = `/${product}`;
}
return NextResponse.redirect(
new URL(
`/${defaultWorkspace}${redirectPath}${searchParamsString}`,
req.url,
),
);
}
const projectInvite = await prismaEdge.projectInvite.findFirst({
where: { email: user.email },
select: { project: { select: { slug: true } } },
});
if (projectInvite) {
return NextResponse.redirect(
new URL(`/${projectInvite.project.slug}/invite`, req.url),
);
}
return NextResponse.redirect(new URL("/onboarding/workspace", req.url));
}Note
WorkspacesMiddleware validates any ?next= query parameter using isValidInternalRedirect prior to processing workspace lookups, preventing open-redirect vulnerabilities during session handoffs.
Client-side rendering layouts for application subdomains (app.dub.co and partners.dub.co) wrap their component trees in NextAuth's SessionProvider alongside React Suspense boundaries to maintain synchronized session state across client navigations.
The password lifecycle mechanism handles password initialization for OAuth-authenticated accounts, issues secure cryptographic reset tokens, and dispatches verification emails. This capability is exposed via the POST route handler at /api/user/set-password, which verifies active user sessions and guards against duplicate password provisioning.
When an OAuth-authenticated user requests to set an account password, the endpoint executes a validation check via Prisma to ensure the user exists, is not a machine account, and currently has a null password hash.
export const POST = withSession(async ({ session }) => {
const user = await prisma.user.findFirst({
where: {
id: session.user.id,
isMachine: false,
passwordHash: null,
},
select: {
id: true,
},
});
if (!user) {
throw new DubApiError({
code: "bad_request",
message:
"You already have a password set. You can change it in your account settings.",
});
}
const { token } = await prisma.passwordResetToken.create({
data: {
identifier: session.user.email,
token: randomBytes(32).toString("hex"),
expires: new Date(Date.now() + PASSWORD_RESET_TOKEN_EXPIRY * 1000),
},
});Warning
If passwordHash is already populated, the endpoint immediately throws a bad_request DubApiError, preventing users from overwriting existing credentials through the initial setup flow.
Following token persistence, the application utilizes @dub/email and the ResetPasswordLink template to dispatch delivery instructions containing the reset URL.
// Send email with password reset link
await sendEmail({
subject: "Dub: Password reset instructions",
to: session.user.email,
react: ResetPasswordLink({
email: session.user.email,
url: `${process.env.NEXTAUTH_URL}/auth/reset-password/${token}`,
}),
});
if (process.env.NODE_ENV === "development") {
console.info(
"Password reset URL:",
`${process.env.NEXTAUTH_URL}/auth/reset-password/${token}`,
);
}
return NextResponse.json({ ok: true });
});Tip
In development environments (NODE_ENV === "development"), the generated password reset URL is automatically emitted to standard output via console.info to facilitate local testing without requiring an active email server integration.
Privileged access and administrative impersonation capabilities on admin.dub.co are governed by specialized API endpoints, persistent tracking stores for verification tokens, and Next.js Edge middleware guards. These mechanisms allow authorized system administrators to generate secure impersonation targets, trace administrative sign-in tokens during custom adapter execution, and restrict administrative subdomains strictly to members of the designated Dub workspace.
Sources: apps/web/app/ee/api/admin/impersonate/route.ts:1-221, apps/web/lib/auth/admin-impersonation.ts:1-20, apps/web/lib/middleware/admin.ts:1-38
The admin impersonation workflow begins at the POST route handler /api/admin/impersonate, which is protected by the withAdmin wrapper. The route accepts a payload containing query parameters such as an email, workspace slug, domain, or Stripe customer ID, and delegates parsing to parseImpersonateQuery().
type ImpersonateIdentifier =
| { type: "email"; email: string }
| { type: "slug"; slug: string }
| { type: "domain"; domain: string }
| { type: "stripeCustomerId"; stripeCustomerId: string };
function parseImpersonateQuery(
raw: unknown,
): ImpersonateIdentifier | { error: string } {
if (typeof raw !== "string" || !raw.trim()) {
return {
error:
"Enter a user email, workspace slug, domain, or Stripe customer ID",
};
}
let query = raw.trim();
if (query.toLowerCase().startsWith("mailto:")) {
query = query.slice(7).trim();
}
// ...Once a valid identifier type is recognized and resolved to a target user via database lookups, the route serializes the associated workspaces and programs, and calls getImpersonateUrl(response.email) to produce a secure session login URL.
const data = {
email: response.email,
workspaces: await serializeWorkspaces(
response.projects.map(({ project }) => project),
),
programs:
response.partners.length > 0
? response.partners[0].partner.programs.map(({ program, ...rest }) => ({
...program,
...rest,
}))
: [],
impersonateUrl: await getImpersonateUrl(response.email),
};
return NextResponse.json(data);
});Note
The admin layout wrapper explicitly provides a NextAuth SessionProvider context for client components operating under admin.dub.co.
To differentiate routine user sign-ins from privileged administrative impersonations, the application maintains an in-memory tracking store (pendingAdminImpersonations) within admin-impersonation.ts.
// Tracks emails signing in via admin impersonation links. Populated in
// CustomPrismaAdapter.useVerificationToken before the token is deleted.
const pendingAdminImpersonations = new Set<string>();
export const markAdminImpersonation = (email: string) => {
pendingAdminImpersonations.add(email.toLowerCase());
};
export const consumeAdminImpersonation = (email: string) => {
const isAdminImpersonation = pendingAdminImpersonations.has(
email.toLowerCase(),
);
if (isAdminImpersonation) {
pendingAdminImpersonations.delete(email.toLowerCase());
}
return isAdminImpersonation;
};This module exports two core utility functions: markAdminImpersonation(email), which registers an email address into the active tracking set when an impersonation token is generated, and consumeAdminImpersonation(email), which verifies and subsequently purges the entry when the verification token is consumed inside CustomPrismaAdapter.useVerificationToken.
Requests destined for the administrative subdomain are intercepted by AdminMiddleware, which evaluates authentication status, workspace membership, and path permissions.
export async function AdminMiddleware(req: NextRequest) {
const { path } = parse(req);
const user = await getUserViaToken(req);
if (!user && path !== "/login") {
return NextResponse.redirect(new URL("/login", req.url));
} else if (user) {
const isAdminUser = await prismaEdge.projectUsers.findUnique({
where: {
userId_projectId: {
userId: user.id,
projectId: DUB_WORKSPACE_ID,
},
},
});
if (!isAdminUser) {
return NextResponse.next(); // throw 404 page
} else if (
path === "/login" ||
!canAccessAdminPath({ userId: user.id, pathname: path })
) {
return NextResponse.redirect(new URL("/", req.url));
}
}
return NextResponse.rewrite(
new URL(`/admin.dub.co${path === "/" ? "" : path}`, req.url),
);
}Warning
If an authenticated user lacks membership in the core Dub workspace (DUB_WORKSPACE_ID), the middleware bypasses administrative routing and returns NextResponse.next(), effectively triggering a 404 page for unauthorized visitors.