---
title: "Authentication and Sessions"
description: "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, use..."
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/authentication-and-sessions"
---

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

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

- [apps/web/lib/auth/options.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/options.ts)
- [apps/web/app/api/auth/...nextauth/route.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/auth/%5B...nextauth%5D/route.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/ee/admin.dub.co/layout.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/layout.tsx)
- [apps/web/app/ee/api/admin/impersonate/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/impersonate/route.ts)
- [apps/web/lib/auth/utils.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/utils.ts)
- [apps/web/lib/middleware/link.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts)
- [apps/web/app/api/me/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/me/route.ts)
- [apps/web/app/ee/api/email-domains/domain/verify/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/email-domains/%5Bdomain%5D/verify/route.ts)
- [apps/web/app/app.dub.co/layout.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/layout.tsx)
- [apps/web/app/ee/api/cron/domains/update/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/domains/update/route.ts)
- [apps/web/middleware.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/middleware.ts)
- [apps/web/app/ee/api/email-domains/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/email-domains/route.ts)
- [apps/web/app/api/user/set-password/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/user/set-password/route.ts)
- [apps/web/lib/middleware/app.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/app.ts)
- [apps/web/ui/auth/login/login-form.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/auth/login/login-form.tsx)
- [apps/web/app/ee/partners.dub.co/layout.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/layout.tsx)
- [apps/web/lib/middleware/utils/get-user-via-token.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/get-user-via-token.ts)
- [apps/web/lib/auth/session.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/session.ts)
- [apps/web/lib/actions/auth/throw-if-authenticated.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/auth/throw-if-authenticated.ts)
- [apps/web/ui/account/user-id.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/account/user-id.tsx)
- [apps/web/ui/auth/login/email-sign-in.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/auth/login/email-sign-in.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/lib/middleware/workspaces.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/workspaces.ts)
- [apps/web/lib/auth/admin-impersonation.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/admin-impersonation.ts)
- [apps/web/lib/middleware/admin.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/admin.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)
- [packages/stripe-app/src/utils/oauth.ts](https://github.com/blade47/dub/blob/HEAD/packages/stripe-app/src/utils/oauth.ts)
- [apps/web/lib/next-auth.d.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/next-auth.d.ts)
- [apps/web/app/api/user/tokens/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/user/tokens/route.ts)
</details>

## Overview

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). 

Sources: [apps/web/lib/auth/options.ts:375-390](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/options.ts#L375-L390), [apps/web/lib/middleware/app.ts:26-45](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/app.ts#L26-L45)

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/options.ts#L377-L390), [apps/web/lib/auth/session.ts:25-136](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/session.ts#L25-L136), [apps/web/lib/middleware/utils/get-user-via-token.ts:5-15](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/get-user-via-token.ts#L5-L15)

## NextAuth Configuration and Providers

### Overview

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.

Sources: [apps/web/app/api/auth/...nextauth/route.tsx:1-6](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/auth/%5B...nextauth%5D/route.tsx#L1-L6)

### Authentication Options and Custom Prisma Adapter

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/options.ts#L375-L390)

| Option | Value / Strategy | Purpose |
| :--- | :--- | :--- |
| `adapter` | `CustomPrismaAdapter(prisma)` | Connects NextAuth models to the underlying database via Prisma |
| `session.strategy` | `"jwt"` | Maintains stateless sessions using encrypted JSON Web Tokens |
| `cookies.sessionToken.name` | `__Secure-next-auth.session-token` (Production) / `next-auth.session-token` (Local) | Determines cookie prefix based on `VERCEL_DEPLOYMENT` |
| `cookies.sessionToken.options.httpOnly` | `true` | Prevents client-side script access to the session cookie |
| `cookies.sessionToken.options.sameSite` | `"lax"` | Mitigates CSRF vulnerabilities across cross-site requests |
| `cookies.sessionToken.options.domain` | `".dub.co"` (Production) / `undefined` (Local) | Scopes cookie domain appropriately for local development vs production |
| `pages.signIn` | `"/login"` | Custom redirect path for sign-in operations |
| `pages.error` | `"/login"` | Custom redirect path for authentication errors |

Sources: [apps/web/lib/auth/options.ts:375-394](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/options.ts#L375-L394)

> [!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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/options.ts#L385-L386)

### Authentication Callbacks and Event Handlers

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:
1. `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-399](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/options.ts#L395-L399)
2. Lockout check: If `user.lockedAt` is populated, it throws an `exceeded-login-attempts` error.
Sources: [apps/web/lib/auth/options.ts:401-403](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/options.ts#L401-L403)
3. Impersonation & SSO enforcement: It checks for admin impersonation (`account?.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-420](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/options.ts#L405-L420)
4. Provider-specific synchronization: For Google or GitHub providers, it updates missing user names and uploads missing or non-R2 avatars to storage. For SAML or `saml-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-529](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/options.ts#L422-L529)

> [!TIP]
> 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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/options.ts#L532-L567)

## Credentials, Magic Links, and SAML

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/auth/login/login-form.tsx#L21-L50), [apps/web/ui/auth/login/email-sign-in.tsx:13-37](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/auth/login/email-sign-in.tsx#L13-L37)

### Login Execution Walkthrough

When a user initiates sign-in using the email form, the submission follows a strict asynchronous validation and dispatch sequence:

1. `onSubmit`: Intercepts form submission and prevents default browser behavior.
Sources: [apps/web/ui/auth/login/email-sign-in.tsx:41-43](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/auth/login/email-sign-in.tsx#L41-L43)
2. `checkAccountExistsAction`: 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-48](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/auth/login/email-sign-in.tsx#L46-L48)
3. Domain / SAML verification: Evaluates `result.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-66](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/auth/login/email-sign-in.tsx#L53-L66)
4. Provider selection & `signIn`: 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-102](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/auth/login/email-sign-in.tsx#L77-L102)
5. Response handling: Inspects `response.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-134](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/auth/login/email-sign-in.tsx#L104-L134)

Sources: [apps/web/ui/auth/login/email-sign-in.tsx:41-135](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/auth/login/email-sign-in.tsx#L41-L135)

### SAML Enforcement Checks

During 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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/workspaces/is-saml-enforced-for-email-domain.ts#L7-L34), [apps/web/lib/auth/options.ts:408-420](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/options.ts#L408-L420)

> [!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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/options.ts#L415-L419)

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 })`.

Sources: [apps/web/app/app.dub.co/auth/auth/saml/form.tsx:7-18](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(auth)/auth/saml/form.tsx#L7-L18)

### Login Error Codes Reference

| Error Code Key | Display Message / Action |
| :--- | :--- |
| `no-credentials` | Please provide an email and password. |
| `invalid-credentials` | Email or password is incorrect. |
| `exceeded-login-attempts` | Account has been locked due to too many login attempts. Please contact support to unlock your account. |
| `too-many-login-attempts` | Too many login attempts. Please try again later. |
| `email-not-verified` | Please verify your email address. |
| `require-saml-sso` | Your organization requires authentication through your company's identity provider. |
| `EmailSignin` | Failed to send login email. Please try again in a minute or contact support. |
| `Callback` | We encountered an issue processing your request. Please try again or contact support if the problem persists. |
| `OAuthSignin` | There was an issue signing you in. Please ensure your provider settings are correct. |
| `OAuthCallback` | We faced a problem while processing the response from the OAuth provider. Please try again. |
| `OAuthAccountNotLinked` | It looks like you already have an account with this email. Please sign in with your account email instead. |

Sources: [apps/web/ui/auth/login/login-form.tsx:31-50](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/auth/login/login-form.tsx#L31-L50)

## Session Management and Route Protection

### Overview

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.

Sources: [apps/web/lib/auth/utils.ts:1-22](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/utils.ts#L1-L22), [apps/web/lib/actions/auth/throw-if-authenticated.ts:1-11](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/auth/throw-if-authenticated.ts#L1-L11)

### The `withSession` API Wrapper

The `withSession` function wraps API route handlers to enforce authentication, handle rate-limiting, and populate request context with session data. 

```mermaid
sequenceDiagram
    participant Client
    participant withSession
    participant Prisma
    participant Upstash
    participant Handler

    Client->>withSession: HTTP Request (Cookie or Authorization header)
    withSession->>withSession: Extract request headers & params
    alt Authorization Header Provided
        withSession->>withSession: Validate "Bearer " prefix
        withSession->>withSession: Hash API token (`hashToken`)
        withSession->>Prisma: Query user by token's hashedKey
        Prisma-->>withSession: User record
        withSession->>Upstash: Rate limit check (60 req / 1m)
        Upstash-->>withSession: Limit status & headers
        withSession->>Upstash: Background rate limit check for lastUsed
        Upstash-->>withSession: Success boolean
        opt Last used update allowed
            withSession->>Prisma: Update token `lastUsed` timestamp
        end
    else No Authorization Header
        withSession->>withSession: Call `getSession()` via NextAuth
        withSession->>withSession: Validate session user ID
    end
    withSession->>Handler: Execute handler({ req, params, searchParams, session })
    Handler-->>Client: JSON Response
```

Sources: [apps/web/lib/auth/session.ts:11-136](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/session.ts#L11-L136)

> [!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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/session.ts#L38-L47)

### Token-Based Authentication and User Info

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/utils.ts#L23-L38)

OAuth access tokens are validated against `RestrictedToken` database records in the user info endpoint.

```typescript
// 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);
  }
}
```

Sources: [apps/web/app/api/oauth/userinfo/route.ts:16-76](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/userinfo/route.ts#L16-L76)

### Protected API Endpoint Implementations

Protected routes leverage `withSession` to retrieve or manage user and token resources safely.

| Route File | Method | Description |
| :--- | :--- | :--- |
| `apps/web/app/api/me/route.ts` | `GET` | Fetches the complete unique user record corresponding to `session.user.id`. |
| `apps/web/app/api/user/tokens/route.ts` | `GET` | Queries all tokens belonging to `session.user.id`, sorted by `lastUsed` then `createdAt` descending. |
| `apps/web/app/api/user/tokens/route.ts` | `DELETE` | Deletes a specific token filtered by record `id` and `userId: session.user.id`. |

Sources: [apps/web/app/api/me/route.ts:1-13](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/me/route.ts#L1-L13), [apps/web/app/api/user/tokens/route.ts:1-40](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/user/tokens/route.ts#L1-L40)

## Edge Middleware Token Verification

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/middleware.ts#L20-L44)

### Subdomain Routing and Request Pipeline

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.

| Hostname / Path Condition | Target Middleware Handler / Action |
| :--- | :--- |
| `isAppHostname(domain)` (e.g., `app.dub.co`) | `AppMiddleware(req)` |
| `API_HOSTNAMES.has(domain)` | `ApiMiddleware(req)` |
| `path.startsWith("/stats/")` | Rewrites to `/${domain}/[key]/stats` |
| `path.startsWith("/.well-known/")` | Rewrites to `/wellknown/${domain}/${file}` |
| `domain === "dub.sh"` | Redirects via `DEFAULT_REDIRECTS[key]` |
| `ADMIN_HOSTNAMES.has(domain)` | `AdminMiddleware(req)` |
| `PARTNERS_HOSTNAMES.has(domain)` | `PartnersMiddleware(req)` |
| `isValidUrl(fullKey)` | `CreateLinkMiddleware(req)` |
| Default fallback | `LinkMiddleware(req, ev)` |

Sources: [apps/web/middleware.ts:34-89](https://github.com/blade47/dub/blob/HEAD/apps/web/middleware.ts#L34-L89)

### Edge Token Extraction and App Middleware Flow

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.

```typescript
export async function getUserViaToken(req: NextRequest) {
  const session = (await getToken({
    req,
    secret: process.env.NEXTAUTH_SECRET,
  })) as {
    email?: string;
    user?: UserProps;
  };

  return session?.user;
}
```

Sources: [apps/web/lib/middleware/utils/get-user-via-token.ts:1-15](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/get-user-via-token.ts#L1-L15)

The execution flow within `AppMiddleware` proceeds through several distinct branches:

1. **Embed and Public Paths**: Requests matching `/embed` invoke `EmbedMiddleware(req)`, while public paths such as `/marketplace`, `/share/`, `/deeplink/`, `/unsubscribe/`, and `/auth/reset-password/` bypass authentication checks.
2. **Unauthenticated Redirects**: If no user session is found and the path is unauthenticated, requests are redirected to `/login` with a `?next=` query parameter preserving the attempted destination.
3. **Onboarding Checks**: For newly created users within the onboarding window (`ONBOARDING_WINDOW_SECONDS`) lacking a default workspace or pending invites, the middleware evaluates cached onboarding steps via `onboardingStepCache` and directs the user through setup.
4. **Root and Settings Navigation**: Standard navigation paths (`/`, `/links`, `/analytics`, `/settings`, etc.) delegate access enforcement to `WorkspacesMiddleware(req, user)`.

Sources: [apps/web/lib/middleware/app.ts:26-126](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/app.ts#L26-L126)

### Workspace Access Control and Redirection

When users access application roots or top-level settings, `WorkspacesMiddleware` resolves active workspace context and handles open-redirect protection.

```typescript
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));
}
```

Sources: [apps/web/lib/middleware/workspaces.ts:10-70](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/workspaces.ts#L10-L70)

> [!NOTE]
> `WorkspacesMiddleware` validates any `?next=` query parameter using `isValidInternalRedirect` prior to processing workspace lookups, preventing open-redirect vulnerabilities during session handoffs.

Sources: [apps/web/lib/middleware/workspaces.ts:13-22](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/workspaces.ts#L13-L22)

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.

Sources: [apps/web/app/app.dub.co/layout.tsx:1-12](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/layout.tsx#L1-L12), [apps/web/app/ee/partners.dub.co/layout.tsx:1-12](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/layout.tsx#L1-L12)

## Password Lifecycle and Resets

### Overview

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.

Sources: [apps/web/app/api/user/set-password/route.ts:10-57](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/user/set-password/route.ts#L10-L57)

### Password Initialization and Reset Token Generation

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.

```typescript
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),
    },
  });
```

Sources: [apps/web/app/api/user/set-password/route.ts:11-37](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/user/set-password/route.ts#L11-L37)

> [!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.

Sources: [apps/web/app/api/user/set-password/route.ts:23-29](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/user/set-password/route.ts#L23-L29)

### Email Dispatch and Development Logging

Following token persistence, the application utilizes `@dub/email` and the `ResetPasswordLink` template to dispatch delivery instructions containing the reset URL.

```typescript
  // 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 });
});
```

Sources: [apps/web/app/api/user/set-password/route.ts:39-57](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/user/set-password/route.ts#L39-L57)

> [!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.

Sources: [apps/web/app/api/user/set-password/route.ts:49-54](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/user/set-password/route.ts#L49-L54)

## Admin Impersonation and Privileged Access

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/impersonate/route.ts#L1-L221), [apps/web/lib/auth/admin-impersonation.ts:1-20](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/admin-impersonation.ts#L1-L20), [apps/web/lib/middleware/admin.ts:1-38](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/admin.ts#L1-L38)

### Impersonation URL Generation and Identifier Parsing

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()`.

```typescript
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();
    }
// ...
```

Sources: [apps/web/app/ee/api/admin/impersonate/route.ts:105-126](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/impersonate/route.ts#L105-L126)

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.

```typescript
  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);
});
```

Sources: [apps/web/app/ee/api/admin/impersonate/route.ts:205-221](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/impersonate/route.ts#L205-L221)

> [!NOTE]
> The admin layout wrapper explicitly provides a NextAuth `SessionProvider` context for client components operating under `admin.dub.co`.

Sources: [apps/web/app/ee/admin.dub.co/layout.tsx:1-8](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/layout.tsx#L1-L8)

### Tracking Impersonation Tokens in Adapter Hooks

To differentiate routine user sign-ins from privileged administrative impersonations, the application maintains an in-memory tracking store (`pendingAdminImpersonations`) within `admin-impersonation.ts`. 

```typescript
// 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;
};
```

Sources: [apps/web/lib/auth/admin-impersonation.ts:1-19](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/admin-impersonation.ts#L1-L19)

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`.

Sources: [apps/web/lib/auth/admin-impersonation.ts:1-19](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/admin-impersonation.ts#L1-L19)

### Admin Middleware Guards

Requests destined for the administrative subdomain are intercepted by `AdminMiddleware`, which evaluates authentication status, workspace membership, and path permissions.

```typescript
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),
  );
}
```

Sources: [apps/web/lib/middleware/admin.ts:8-38](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/admin.ts#L8-L38)

> [!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.

Sources: [apps/web/lib/middleware/admin.ts:15-26](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/admin.ts#L15-L26)

## Related

- [Routing and Multitenancy](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/core-architecture/routing-and-multitenancy)
- [Enterprise SSO and SCIM](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/authentication-and-security/enterprise-sso-and-scim)


## Sitemap

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