---
title: "OAuth2 Provider and API Tokens"
description: "Dub provides a robust OAuth2 provider and API token management system that secures programmatic access and enables third-party application integrations across workspaces and user accounts. The plat..."
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/oauth2-provider-and-api-tokens"
---

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

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

- [apps/web/app/api/tokens/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/tokens/route.ts)
- [packages/stripe-app/src/utils/oauth.ts](https://github.com/blade47/dub/blob/HEAD/packages/stripe-app/src/utils/oauth.ts)
- [packages/cli/src/utils/oauth.ts](https://github.com/blade47/dub/blob/HEAD/packages/cli/src/utils/oauth.ts)
- [apps/web/lib/api/oauth/constants.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/oauth/constants.ts)
- [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/dashboard/slug/ee/settings/tokens/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/tokens/page.tsx)
- [apps/web/app/api/oauth/token/refresh-access-token.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/token/refresh-access-token.ts)
- [apps/web/app/api/oauth/authorize/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/authorize/route.ts)
- [apps/web/app/api/oauth/apps/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/apps/route.ts)
- [apps/web/lib/auth/workspace.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/workspace.ts)
- [apps/web/app/api/oauth/token/exchange-code-for-token.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/token/exchange-code-for-token.ts)
- [apps/web/lib/integrations/oauth-provider.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/oauth-provider.ts)
- [apps/web/app/ee/api/intercom/callback/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/intercom/callback/route.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/app/ee/api/hubspot/callback/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/hubspot/callback/route.ts)
- [apps/web/app/api/slack/callback/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/slack/callback/route.ts)
- [apps/web/app/api/callback/bitly/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/callback/bitly/route.ts)
- [packages/cli/src/api/callback.ts](https://github.com/blade47/dub/blob/HEAD/packages/cli/src/api/callback.ts)
- [apps/web/app/api/oauth/apps/appId/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/apps/%5BappId%5D/route.ts)
- [apps/web/app/api/tokens/id/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/tokens/%5Bid%5D/route.ts)
- [apps/web/app/api/oauth/token/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/token/route.ts)
- [apps/web/app/app.dub.co/dashboard/account/settings/tokens/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/account/settings/tokens/page.tsx)
- [apps/web/lib/auth/token-cache.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/token-cache.ts)
- [apps/web/app/api/user/referrals-token/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/user/referrals-token/route.ts)
- [apps/web/lib/integrations/bitly/oauth.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/bitly/oauth.ts)
- [apps/web/lib/dub.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/dub.ts)
- [apps/web/prisma/schema/oauth.prisma](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/oauth.prisma)
- [apps/web/lib/actions/generate-client-secret.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/generate-client-secret.ts)
- [apps/web/lib/integrations/google-ads/oauth.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/google-ads/oauth.ts)
- [packages/cli/src/commands/login.ts](https://github.com/blade47/dub/blob/HEAD/packages/cli/src/commands/login.ts)
</details>

## Overview

Dub provides a robust OAuth2 provider and API token management system that secures programmatic access and enables third-party application integrations across workspaces and user accounts. The platform supports standard authorization code flows with PKCE and client secrets, granular permission scopes, token caching via Upstash Redis, and dedicated endpoints for identity resolution and token lifecycles.

Sources: [apps/web/app/api/oauth/authorize/route.ts:1-134](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/authorize/route.ts#L1-L134), [apps/web/app/api/oauth/token/route.ts:1-30](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/token/route.ts#L1-L30), [apps/web/lib/auth/token-cache.ts:1-76](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/token-cache.ts#L1-L76), [apps/web/app/api/oauth/userinfo/route.ts:1-84](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/userinfo/route.ts#L1-L84)

## OAuth Application Registration and Secrets

### Overview

OAuth applications are modeled through a relational Prisma schema combining general integration metadata with OAuth-specific credentials. Each registered application is backed by an `Integration` record linked to a project workspace and user, containing descriptive properties such as names, slugs, developers, websites, install URLs, descriptions, readmes, logos, and screenshots. The associated `OAuthApp` record stores unique client identifiers, hashed client secrets, partial secret previews, redirect URIs in JSON format, and PKCE requirement flags.

Sources: [apps/web/app/api/oauth/apps/route.ts:44-118](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/apps/route.ts#L44-L118), [apps/web/prisma/schema/oauth.prisma:1-12](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/oauth.prisma#L1-L12)

### Application Registration and Lifecycle Routes

The OAuth application lifecycle is managed via REST endpoints under `/api/oauth/apps` and `/api/oauth/apps/[appId]`, which enforce workspace permissions (`oauth_apps.read` and `oauth_apps.write`). When creating an application via `POST /api/oauth/apps`, the request body is parsed and validated against `createOAuthAppSchema`. The execution sequence proceeds as follows: `parseRequestBody()` → `createOAuthAppSchema.parseAsync()` → `prisma.integration.findUnique()` checking for slug conflicts → `createToken()` generating `clientId` and optionally `clientSecret` → `prisma.integration.create()` creating the integration and nested `oAuthApp` records → conditional storage upload for application logos via `storage.upload()`.

Sources: [apps/web/app/api/oauth/apps/route.ts:44-134](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/apps/route.ts#L44-L134)

> [!WARNING]
> If a slug conflict occurs during application creation or updating, Prisma throws error code `P2002`, which is caught and re-thrown as a `DubApiError` with a `conflict` status code and a message indicating the slug is already in use.

Sources: [apps/web/app/api/oauth/apps/route.ts:67-72](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/apps/route.ts#L67-L72), [apps/web/app/api/oauth/apps/route.ts:143-155](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/apps/route.ts#L143-L155), [apps/web/app/api/oauth/apps/appId/route.ts:153-165](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/apps/%5BappId%5D/route.ts#L153-L165)

Updates and deletions handled by `PATCH /api/oauth/apps/[appId]` and `DELETE /api/oauth/apps/[appId]` utilize Vercel's `waitUntil` function to asynchronously clean up stale logo assets and removed screenshot URLs from object storage without blocking the HTTP response.

Sources: [apps/web/app/api/oauth/apps/appId/route.ts:52-215](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/apps/%5BappId%5D/route.ts#L52-L215)

### Client Secret Generation and Token Prefixes

Client identifiers and secrets adhere to strict length and prefix conventions defined in the OAuth configuration constants. Client IDs use the prefix `dub_app_` with a length of 24 characters, while client secrets use the prefix `dub_app_secret_` with a length of 30 characters. When applications do not enforce PKCE (`pkce: false`), a client secret is generated upon creation.

Sources: [apps/web/lib/api/oauth/constants.ts:7-14](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/oauth/constants.ts#L7-L14), [apps/web/app/api/oauth/apps/route.ts:74-84](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/apps/route.ts#L74-L84)

Developers can also regenerate client secrets out-of-band using the server action `generateClientSecret`. This action validates workspace permissions (`oauth_apps.write`), confirms ownership of the integration via `prisma.integration.findFirstOrThrow()`, generates a new token using `createToken()`, updates the database record with a newly hashed secret via `hashToken()`, and stores a masked partial secret preview displaying the last 8 characters (`dub_app_secret_****${clientSecret.slice(-8)}`).

Sources: [apps/web/lib/actions/generate-client-secret.ts:17-51](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/generate-client-secret.ts#L17-L51)

| Configuration Property | Value / Length | Prefix | Purpose |
| :--- | :--- | :--- | :--- |
| `CLIENT_ID` | 24 chars | `dub_app_` | Unique public identifier for the OAuth app |
| `CLIENT_SECRET` | 30 chars | `dub_app_secret_` | Confidential credential for confidential clients |
| `ACCESS_TOKEN` | 40 chars | `dub_access_token_` | Bearer token for API authentication |
| `REFRESH_TOKEN` | 40 chars | None (hashed) | Long-lived token for rotating access tokens |
| `CODE` | 40 chars | None | Short-lived authorization code |

Sources: [apps/web/lib/api/oauth/constants.ts:7-16](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/oauth/constants.ts#L7-L16)

## Authorization Code Flow and Consent

### Overview

The authorization code flow governs how third-party applications request delegated access to user workspaces. The consent UI page (`apps/web/app/app.dub.co/(auth)/oauth/authorize/page.tsx`) validates incoming query parameters against the `authorizeRequestSchema` using `validateAuthorizeRequest()`. If valid, it presents a consent screen displaying the requesting application's name, logo, developer, and requested permission scopes, along with an optional verification warning banner for unverified apps.

Sources: [apps/web/app/app.dub.co/auth/oauth/authorize/page.tsx:1-115](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(auth)/oauth/authorize/page.tsx#L1-L115)

### Authorization Request Validation and Code Issuance

Approval of an authorization request is handled by the `POST /api/oauth/authorize` endpoint. The execution sequence proceeds as follows: `authorizeRequestSchema.parse()` → `getGrantedScopesForRole()` filtering requested scopes against the workspace role → `prisma.oAuthApp.findUniqueOrThrow()` fetching application metadata and integration status → plan check for restricted integrations → redirect URI validation against app whitelist → conditional PKCE presence check → `canInstallOAuthApp()` verification → `prisma.oAuthCode.create()` issuing an authorization code.

Sources: [apps/web/app/api/oauth/authorize/route.ts:17-113](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/authorize/route.ts#L17-L113)

> [!WARNING]
> If a request specifies Stripe or Shopify integration IDs (`STRIPE_INTEGRATION_ID`, `SHOPIFY_INTEGRATION_ID`) while the workspace is on a `free` or `pro` plan, the authorization request fails immediately with a `bad_request` error requiring a Business plan upgrade.

Sources: [apps/web/app/api/oauth/authorize/route.ts:59-70](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/authorize/route.ts#L59-L70)

### OAuth Scopes and Descriptions

The OAuth provider defines twelve distinct permission scopes that applications can request. Each scope governs specific read or write capabilities across workspace resources.

| Scope | Description |
| :--- | :--- |
| `links.read` | Read access to links |
| `links.write` | Read and Write access to links |
| `tags.read` | Read access to tags |
| `tags.write` | Read and Write access to tags |
| `analytics.read` | Read access to analytics and events |
| `domains.read` | Read access to domains |
| `domains.write` | Read and Write access to domains |
| `user.read` | Read your name, email and profile image |
| `webhooks.read` | Read access to webhooks |
| `webhooks.write` | Read and Write access to webhooks |
| `folders.read` | Read access to folders |
| `folders.write` | Read and Write access to folders |

Sources: [apps/web/lib/api/oauth/constants.ts:21-50](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/oauth/constants.ts#L21-L50)

> [!NOTE]
> The `user.read` scope is granted by default to all authorization requests without requiring applications to explicitly request it in the authorization URL.

Sources: [apps/web/lib/api/oauth/constants.ts:33-34](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/oauth/constants.ts#L33-L34)

## Token Exchange and Refresh Lifecycle

### Overview

The token exchange and refresh lifecycle is handled through `POST /api/oauth/token`, which parses incoming form data against `tokenGrantSchema` and branches based on the requested `grant_type`. The route supports two primary grant types: `authorization_code` and `refresh_token`, each enforcing strict client authentication, expiration checks, and token rotation rules.

Sources: [apps/web/app/api/oauth/token/route.ts:10-30](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/token/route.ts#L10-L30)

### Authorization Code Exchange

When a client presents an authorization code, `exchangeAuthCodeForToken` validates the request parameters, authenticates the client via HTTP Basic Auth or direct body parameters when PKCE is disabled, and verifies code integrity.

Sources: [apps/web/app/api/oauth/token/exchange-code-for-token.ts:15-112](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/token/exchange-code-for-token.ts#L15-L112)

The execution sequence proceeds as follows: `tokenGrantSchema.parse()` → `exchangeAuthCodeForToken()` → `Promise.all([prisma.oAuthApp.findUnique(), prisma.oAuthCode.findUnique()])` fetching application configuration and code record → PKCE or client secret verification → code expiration and redirect URI comparison → `installIntegration()` provisioning workspace access → `prisma.restrictedToken.create()` persisting the access token and nested refresh token → `waitUntil()` executing deferred cleanup (`prisma.oAuthCode.delete()` and `prisma.restrictedToken.deleteMany()`).

Sources: [apps/web/app/api/oauth/token/exchange-code-for-token.ts:15-220](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/token/exchange-code-for-token.ts#L15-L220)

> [!CAUTION]
> If PKCE is enabled on the `OAuthApp`, a `code_verifier` parameter is mandatory. When the code challenge method is `S256`, the verifier is hashed via `generateCodeChallengeHash()` and compared against `accessCode.codeChallenge`; a mismatch throws an `unauthorized` error with the `invalid_grant` code.

Sources: [apps/web/app/api/oauth/token/exchange-code-for-token.ts:85-132](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/token/exchange-code-for-token.ts#L85-L132)

### Refresh Token Rotation

The `refreshAccessToken` function processes `refresh_token` grants by validating client credentials, looking up the hashed refresh token, checking expiration, and issuing a rotated token pair.

Sources: [apps/web/app/api/oauth/token/refresh-access-token.ts:13-111](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/token/refresh-access-token.ts#L13-L111)

The execution sequence proceeds as follows: `tokenGrantSchema.parse()` → `refreshAccessToken()` → Basic Auth header parsing if client credentials are omitted from the body → `prisma.oAuthApp.findUnique()` checking app and PKCE settings → `prisma.oAuthRefreshToken.findUnique()` querying the hashed refresh token → `prisma.installedIntegration.findUnique()` loading installation and project plan metadata → `prisma.$transaction()` deleting the old access token (`prisma.restrictedToken.delete()`) and creating the new token pair (`prisma.restrictedToken.create()`) with a fresh refresh token record.

Sources: [apps/web/app/api/oauth/token/refresh-access-token.ts:13-198](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/token/refresh-access-token.ts#L13-L198)

> [!IMPORTANT]
> Token lifetimes and key generation parameters are centrally defined in `OAUTH_CONFIG`. Access tokens have a lifetime of 2 hours (`7200` seconds) and a length of 40 characters with a `dub_access_token_` prefix, while refresh tokens have a lifetime of 120 days and a 40-character length without a prefix. Authorization codes expire after 2 minutes.

Sources: [apps/web/lib/api/oauth/constants.ts:2-16](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/oauth/constants.ts#L2-L16)

### Token Lifecycle Configuration and Trade-offs

The OAuth token storage schema coordinates relational ownership between applications, authorization codes, restricted tokens, and refresh tokens across database tables.

| Model | Primary Key | Unique Constraints | Cascade Relations |
| :--- | :--- | :--- | :--- |
| `OAuthApp` | `id` | `integrationId`, `clientId` | `oAuthCodes`, `integration` |
| `OAuthCode` | `id` (cuid) | `code` | `oAuthApp`, `user`, `project` |
| `OAuthRefreshToken` | `id` (cuid) | `hashedRefreshToken` | `accessToken`, `installedIntegration` |

Sources: [apps/web/prisma/schema/oauth.prisma:1-49](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/oauth.prisma#L1-L49)

| Design Choice | Benefit | Cost |
| :--- | :--- | :--- |
| Hashed token storage (`hashedKey`, `hashedRefreshToken`) | Prevents plain-text token exposure if the database is compromised | Requires hashing overhead on every token exchange and verification lookup |
| Single token per client per user per workspace (`deleteMany` in `waitUntil`) | Prevents accumulation of orphaned active tokens per installation | Automatically revokes concurrent active sessions for the same integration |
| Deferred cleanup via Vercel `waitUntil` | Speeds up the token response by non-blocking code deletion | Relies on serverless runtime background execution support |

Sources: [apps/web/app/api/oauth/token/exchange-code-for-token.ts:201-220](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/token/exchange-code-for-token.ts#L201-L220)

## UserInfo Endpoint and Identity Resolution

### Overview

The UserInfo endpoint (`GET /api/oauth/userinfo`) resolves the authenticated user and associated workspace identity from a bearer token. It implements CORS support via predefined headers (`Access-Control-Allow-Origin: *`, `Access-Control-Allow-Methods: GET, OPTIONS`, and `Access-Control-Allow-Headers: Content-Type, Authorization`) and handles preflight `OPTIONS` requests by returning status `204`.

Sources: [apps/web/app/api/oauth/userinfo/route.ts:7-14](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/userinfo/route.ts#L7-L14), [apps/web/app/api/oauth/userinfo/route.ts:78-83](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/userinfo/route.ts#L78-L83)

### Identity Resolution Call Chain

The execution sequence proceeds as follows: `GET()` → `getAuthTokenOrThrow(req)` extracts the bearer token from the request → `hashToken(accessToken)` hashes the token string → `prisma.restrictedToken.findFirst()` queries the database for an active, non-expired token matching the hash where `installationId` is not null → selection extracts the related `user` (`id`, `name`, `image`) and `project` (`id`, `name`, `slug`, `logo`) records → `prefixWorkspaceId(tokenRecord.project.id)` formats the workspace identifier → `NextResponse.json(userInfo)` serializes the payload.

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

> [!WARNING]
> If `tokenRecord` is not found or has expired (`expires < new Date()`), or lacks an `installationId`, the lookup fails and throws a `DubApiError` with code `unauthorized` and message `"Access token not found or expired."`.

Sources: [apps/web/app/api/oauth/userinfo/route.ts:20-54](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/userinfo/route.ts#L20-L54)

## Personal and Workspace API Tokens

### Overview

Dub supports scoped personal and workspace API tokens used by external applications and automation clients to interact with workspace resources. Tokens are managed through standard REST API endpoints supporting retrieval, creation, updating, and revocation, alongside dashboard client components. Workspace tokens enforce limits, role validation, and optional machine-user provisioning.

Sources: [apps/web/app/api/tokens/route.ts:25-184](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/tokens/route.ts#L25-L184), [apps/web/app/api/tokens/id/route.ts:11-150](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/tokens/%5Bid%5D/route.ts#L11-L150)

### Token Management Endpoints

The API provides structured operations for inspecting, modifying, and deleting tokens within a workspace context. Each route enforces required permissions via workspace authorization middleware.

| HTTP Method | Path | Required Permission | Description |
| :--- | :--- | :--- | :--- |
| `GET` | `/api/tokens` | `tokens.read` | Lists all non-installation workspace tokens, ordered by last used and creation date. |
| `POST` | `/api/tokens` | `tokens.write` | Generates a new API token, enforcing workspace limits and scope validations. |
| `GET` | `/api/tokens/:id` | `tokens.read` | Retrieves details for a specific token ID within the workspace. |
| `PATCH` | `/api/tokens/:id` | `tokens.write` | Updates a token's name or scopes, refreshing the token cache. |
| `DELETE` | `/api/tokens/:id` | `tokens.write` | Deletes a token, purging associated machine users and cache entries. |

Sources: [apps/web/app/api/tokens/route.ts:26-65](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/tokens/route.ts#L26-L65), [apps/web/app/api/tokens/route.ts:68-184](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/tokens/route.ts#L68-L184), [apps/web/app/api/tokens/id/route.ts:12-150](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/tokens/%5Bid%5D/route.ts#L12-L150)

> [!WARNING]
> Workspace token creation is capped at a maximum of `100` active tokens (`MAX_WORKSPACE_TOKENS`). Exceeding this limit throws a forbidden `DubApiError` prompting users to contact support.

Sources: [apps/web/app/api/tokens/route.ts:19-20](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/tokens/route.ts#L19-L20), [apps/web/app/api/tokens/route.ts:110-115](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/tokens/route.ts#L110-L115)

### Token Creation and Machine User Call Chain

The token creation workflow (`POST /api/tokens`) executes the following sequential stages: `POST()` → `assertRateLimit()` checks creation frequency against `RATELIMIT_POLICIES.createToken` → `createTokenSchema.parse()` validates request body parameters (`name`, `isMachine`, `scopes`) → workspace user role validation confirms authorization → `hashToken()` generates a secure hash of the raw `dub_{nanoid(24)}` token → prisma transaction counts existing tokens against `MAX_WORKSPACE_TOKENS` (using isolation level `ReadUncommitted`) → if `isMachine` is true, a dedicated machine user is created with a `user_` prefix and associated as a workspace member → `prisma.restrictedToken.create()` inserts the token record with hashed key, partial display key, and space-separated scopes → `waitUntil()` dispatches an email notification via `sendEmail()` with the `APIKeyCreated` template → `NextResponse.json()` returns the plain text token once.

Sources: [apps/web/app/api/tokens/route.ts:68-180](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/tokens/route.ts#L68-L180)

> [!NOTE]
> Only workspace owners can create machine users (`isMachine: true`). Attempting to create a machine user with a non-owner role throws a forbidden `DubApiError`.

Sources: [apps/web/app/api/tokens/route.ts:81-87](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/tokens/route.ts#L81-L87)

## Scope Enforcement and Token Caching

### Overview

Workspace authentication middleware authenticates incoming requests by inspecting `Authorization` Bearer tokens, checking Upstash Redis cache via hashed keys, querying Prisma for restricted or legacy tokens, validating expiration, enforcing plan-based rate limits, and updating token last-used timestamps asynchronously.

Sources: [apps/web/lib/auth/workspace.ts:98-302](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/workspace.ts#L98-L302), [apps/web/lib/auth/token-cache.ts:32-74](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/token-cache.ts#L32-L74)

### Authentication and Caching Mechanism

The authentication flow executes the following call chain: `withWorkspace()` wrapper executes → clones incoming request via `req.clone()` → extracts `Authorization` header → checks `Bearer ` prefix via `authorizationHeader.startsWith("Bearer ")` → extracts raw API key → computes `hashedKey = await hashToken(apiKey)` → queries Redis cache via `tokenCache.get({ hashedKey })` → if cache misses, queries database via `prisma.restrictedToken.findUnique()` for restricted tokens (`dub_` prefix) or `prisma.token.findUnique()` for legacy tokens → validates token existence and `token.user` presence → checks `token.expires < new Date()` for expiration → if cache missed, persists item via background `waitUntil(tokenCache.set({ hashedKey, token }))` → evaluates plan rate limits via `rateLimitRequest()` using identifier `workspace:ratelimit:${hashedKey}` → updates token `lastUsed` timestamp asynchronously via `ratelimit(1, "1 m").limit()` and database update → populates `session` with user metadata.

Sources: [apps/web/lib/auth/workspace.ts:81-302](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/workspace.ts#L81-L302), [apps/web/lib/auth/token-cache.ts:33-51](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/token-cache.ts#L33-L51)

> [!WARNING]
> Requests lacking the `Bearer ` prefix on their `Authorization` header immediately throw a `bad_request` `DubApiError` requiring the correct schema prefix.

Sources: [apps/web/lib/auth/workspace.ts:98-106](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/workspace.ts#L98-L106)

### Token Cache and Upstash Redis Integration

The `TokenCache` class interfaces with Upstash Redis (`redis`) using the prefix `dubTokenCache` and a default 24-hour expiration (`CACHE_EXPIRATION`). Cached items adhere to a Zod schema validating user details, scopes, and project plans.

Sources: [apps/web/lib/auth/token-cache.ts:1-74](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/token-cache.ts#L1-L74)

| Method | Parameters | Redis Operation | Description |
| :--- | :--- | :--- | :--- |
| `set` | `{ hashedKey, token }` | `redis.set` with `ex: 86400` | Serializes and caches token metadata for 24 hours. |
| `get` | `{ hashedKey }` | `redis.get` | Retrieves cached `TokenCacheItem` using the prefixed key. |
| `delete` | `{ hashedKey }` | `redis.del` | Removes a token cache entry upon revocation or deletion. |
| `expireMany` | `{ hashedKeys }` | `redis.pipeline()` / `expire(..., 1)` | Instantly expires multiple cache keys using a Redis pipeline. |

Sources: [apps/web/lib/auth/token-cache.ts:33-69](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/token-cache.ts#L33-L69)

> [!NOTE]
> When a token is updated via `PATCH /api/tokens/:id` or deleted via `DELETE /api/tokens/:id`, the token cache is explicitly synchronized using `waitUntil(tokenCache.set(...))` or `waitUntil(tokenCache.delete(...))`.

Sources: [apps/web/app/api/tokens/id/route.ts:93-100](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/tokens/%5Bid%5D/route.ts#L93-L100), [apps/web/app/api/tokens/id/route.ts:136-141](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/tokens/%5Bid%5D/route.ts#L136-L141)

### Scope Enforcement and Role Validation

Scope validation ensures that users and tokens possess appropriate permissions before executing workspace operations. Role-based scope checks are enforced during token updates and workspace route handlers.

Sources: [apps/web/app/api/tokens/id/route.ts:48-77](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/tokens/%5Bid%5D/route.ts#L48-L77)

> [!CAUTION]
> The `PATCH /api/tokens/:id` route validates requested scopes against the user's project role via `validateScopesForRole(scopes, role)`. If any requested scope is unavailable for that role, an `unprocessable_entity` `DubApiError` is thrown.

Sources: [apps/web/app/api/tokens/id/route.ts:54-77](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/tokens/%5Bid%5D/route.ts#L54-L77)

## First-Party Client Integrations

### Overview

First-party client integrations and third-party service connections within Dub rely on specialized OAuth provider abstractions and client helper utilities. The architecture standardizes OAuth interactions across CLI commands, Stripe App extensions, and webhook integrations like Bitly, Slack, Intercom, HubSpot, and Google Ads.

Sources: [packages/stripe-app/src/utils/oauth.ts:1-150](https://github.com/blade47/dub/blob/HEAD/packages/stripe-app/src/utils/oauth.ts#L1-L150), [packages/cli/src/utils/oauth.ts:1-9](https://github.com/blade47/dub/blob/HEAD/packages/cli/src/utils/oauth.ts#L1-L9), [apps/web/lib/integrations/oauth-provider.ts:1-174](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/oauth-provider.ts#L1-L174)

### Third-Party Provider Abstractions

The `OAuthProvider` class handles authorization URL generation, state storage in Upstash Redis with a 30-minute expiration, and code exchange using configurable body formats and authorization methods.

Sources: [apps/web/lib/integrations/oauth-provider.ts:25-141](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/oauth-provider.ts#L25-L141)

| Configuration Property | Type | Description |
| :--- | :--- | :--- |
| `name` | `string` | Human-readable name of the OAuth provider. |
| `clientId` | `string` | Client identifier assigned by the provider. |
| `clientSecret` | `string` | Client secret used for token exchanges. |
| `authUrl` | `string` | Provider authorization endpoint URL. |
| `tokenUrl` | `string` | Provider token exchange endpoint URL. |
| `redirectUri` | `string` | Callback URI where the provider redirects after authorization. |
| `scopes` | `string` (optional) | Space-separated list of requested OAuth scopes. |
| `redisStatePrefix` | `string` | Prefix key for storing transient state tokens in Upstash Redis. |
| `tokenSchema` | `z.ZodSchema` | Zod schema for validating token responses. |
| `bodyFormat` | `"form" \| "json"` | Request body serialization format for token requests. |
| `responseFormat` | `"json" \| "text"` (optional) | Expected response body format from the token endpoint. |
| `authorizationMethod` | `"header" \| "body"` | Method for passing client credentials during token exchange. |

Sources: [apps/web/lib/integrations/oauth-provider.ts:5-18](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/oauth-provider.ts#L5-L18)

> [!NOTE]
> Concrete implementations extend `OAuthProvider` to customize parameters, such as `googleAdsOAuthProvider`, which configures offline access and custom scopes.

Sources: [apps/web/lib/integrations/google-ads/oauth.ts:11-38](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/google-ads/oauth.ts#L11-L38), [apps/web/lib/integrations/google-ads/oauth.ts:222-233](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/integrations/google-ads/oauth.ts#L222-L233)

### CLI Login and Local Callback Server

The CLI authentication lifecycle initiates via the `login` command, which generates a PKCE code verifier (`getNanoid(64)`), constructs the authorization URI using `@badgateway/oauth2-client`, opens the browser via `open(authUrl)`, and spawns a local HTTP server on port 4587 to capture the callback.

Sources: [packages/cli/src/commands/login.ts:9-38](https://github.com/blade47/dub/blob/HEAD/packages/cli/src/commands/login.ts#L9-L38)

The call chain for the CLI callback handling executes: `oauthCallbackServer()` creates an HTTP server listening on port 4587 (`server.listen(4587)`) with a 5-minute timeout (`setTimeout(..., 300000)`) → incoming requests are parsed via `url.parse(req.url || "", true)` → verifies `reqUrl.pathname === "/callback"` and `req.method === "GET"` → extracts the authorization `code` query parameter → invokes `oauthClient.authorizationCode.getToken({ code, redirectUri, codeVerifier })` to exchange credentials → invokes `setConfig(configInfo)` to persist tokens and domain (`dub.sh`) → terminates the server via `server.close()` and exits the process via `process.exit(0)`.

Sources: [packages/cli/src/api/callback.ts:17-86](https://github.com/blade47/dub/blob/HEAD/packages/cli/src/api/callback.ts#L17-L86)

> [!WARNING]
> If the callback server does not receive an authorization code within 300,000 milliseconds (5 minutes), the timeout handler automatically closes the server and terminates the process.

Sources: [packages/cli/src/api/callback.ts:80-84](https://github.com/blade47/dub/blob/HEAD/packages/cli/src/api/callback.ts#L80-L84)

### Stripe App OAuth Utilities

The Stripe App integration manages Dub authentication inside Stripe dashboards using mode-specific redirect URLs and secure secret storage.

Sources: [packages/stripe-app/src/utils/oauth.ts:1-150](https://github.com/blade47/dub/blob/HEAD/packages/stripe-app/src/utils/oauth.ts#L1-L150)

```typescript
export async function getValidToken({ stripe }: { stripe: Stripe }) {
  const token = await getSecret<Token>({
    stripe,
    name: "dub_token",
  });

  if (!token) {
    throw new Error("Access token not found for the account.");
  }

  try {
    await getUserInfo({ token });
  } catch (e) {
    const refreshedToken = await refreshToken({ token });

    if (!refreshedToken) {
      console.error("Failed to refresh access token.");
      return null;
    }

    await setSecret({
      stripe,
      name: "dub_token",
      payload: JSON.stringify(refreshedToken),
    });

    return refreshedToken;
  }

  return token;
}
```

Sources: [packages/stripe-app/src/utils/oauth.ts:91-121](https://github.com/blade47/dub/blob/HEAD/packages/stripe-app/src/utils/oauth.ts#L91-L121)

## Related

- [Authentication and Sessions](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/authentication-and-security/authentication-and-sessions)
- [OpenAPI and Public REST API](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/developer-tools/openapi-and-public-rest-api)


## Sitemap

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