Relevant source files
The following files were used as context for generating this wiki page:
- apps/web/app/api/oauth/apps/route.ts
- apps/web/app/api/oauth/authorize/route.ts
- packages/cli/src/utils/oauth.ts
- apps/web/app/app.dub.co/auth/oauth/authorize/page.tsx
- apps/web/app/api/tokens/route.ts
- apps/web/lib/api/oauth/constants.ts
- apps/web/app/api/oauth/userinfo/route.ts
- apps/web/app/api/oauth/apps/appId/route.ts
- packages/stripe-app/src/utils/oauth.ts
- apps/web/app/api/oauth/token/exchange-code-for-token.ts
- packages/cli/src/api/callback.ts
- apps/web/app/ee/api/hubspot/callback/route.ts
- apps/web/app/api/slack/callback/route.ts
- apps/web/app/api/oauth/token/refresh-access-token.ts
- packages/cli/src/commands/login.ts
- apps/web/lib/integrations/oauth-provider.ts
- apps/web/app/api/callback/bitly/route.ts
- apps/web/app/app.dub.co/auth/oauth/authorize/authorize-form.tsx
- apps/web/lib/integrations/bitly/oauth.ts
- apps/web/app/api/oauth/token/route.ts
- apps/web/app/app.dub.co/auth/oauth/authorize/scopes-requested.tsx
- apps/web/app/app.dub.co/dashboard/slug/ee/settings/oauth-apps/page.tsx
- apps/web/app/app.dub.co/dashboard/slug/ee/settings/oauth-apps/appId/page-client.tsx
- apps/web/app/app.dub.co/dashboard/slug/ee/settings/oauth-apps/new/page-client.tsx
- apps/web/app/app.dub.co/dashboard/slug/ee/settings/oauth-apps/layout.tsx
- apps/web/app/app.dub.co/dashboard/slug/ee/settings/oauth-apps/page-client.tsx
- apps/web/app/app.dub.co/dashboard/slug/ee/settings/oauth-apps/new/page.tsx
- apps/web/lib/actions/generate-client-secret.ts
- apps/web/app/app.dub.co/dashboard/slug/ee/settings/oauth-apps/appId/page.tsx
- apps/web/prisma/schema/oauth.prisma
Overview
Dub features a complete OAuth 2.0 authorization server implementation that enables third-party developers and internal clients, such as the Dub CLI, to securely integrate with Dub workspaces. Developers can register and manage OAuth applications directly within workspace developer settings, configuring redirect URIs, scopes, and PKCE requirements. The platform provides an interactive consent and authorization workflow where users select a target workspace, review requested permission scopes against their role privileges, and approve or decline access. Authorized requests issue short-lived authorization codes that are exchanged via secure token endpoints for access and refresh tokens, supporting both confidential client secrets and public PKCE verification.
Sources: apps/web/prisma/schema/oauth.prisma:1-49, apps/web/app/api/oauth/apps/route.ts:15-160, apps/web/app/api/oauth/authorize/route.ts:17-132, apps/web/app/app.dub.co/auth/oauth/authorize/page.tsx:23-114, apps/web/app/api/oauth/token/exchange-code-for-token.ts:15-230, packages/cli/src/commands/login.ts:9-37
OAuth Application Storage and Schema
Database Persistence Architecture
The OAuth persistence layer is managed via Prisma queries targeting prisma.oAuthApp (representing registered application credentials linked to an integration), prisma.oAuthCode (tracking short-lived authorization codes issued during consent prompts), and prisma.oAuthRefreshToken (managing token longevity by linking installations to restricted access tokens). The oAuthApp database table stores credentials including clientId, hashedClientSecret, partialClientSecret, redirectUris stored as JSON, and a pkce boolean flag for PKCE enforcement.
Sources: apps/web/prisma/schema/oauth.prisma:1-12, apps/web/app/api/oauth/apps/route.ts:17-30
Authorization codes issued during user consent prompts are inserted into prisma.oAuthCode, recording clientId, authorizing userId, workspace projectId, the unique code string, granted scopes, redirectUri, expiresAt timestamps, and optional PKCE parameters codeChallenge and codeChallengeMethod.
Sources: apps/web/prisma/schema/oauth.prisma:14-34, apps/web/app/api/oauth/authorize/route.ts:102-114
Long-lived sessions use prisma.oAuthRefreshToken to persist credentials, linking an installation record (installationId) to an issued access token (accessTokenId) via hashedRefreshToken and tracking an expiresAt date. Access tokens themselves are stored as prisma.restrictedToken records, establishing the foundation for API authentication across Dub's endpoints (documented in REST API, which handles general token querying and API key authentication).
Sources: apps/web/prisma/schema/oauth.prisma:36-49, apps/web/app/api/oauth/token/exchange-code-for-token.ts:180-199, apps/web/app/api/tokens/route.ts:26-65
Configuration Constants and Lifetimes
OAuth parameters and cryptographic lengths are governed by strict configuration constants defined in OAUTH_CONFIG. Token lifetimes are measured in seconds, defining short-lived authorization codes and access tokens alongside long-lived refresh tokens. Token and identifier lengths specify exact character counts, while prefix constants standardize naming conventions for generated identifiers and secrets across the system.
| Constant Name | Value / Length | Meaning / Purpose |
|---|---|---|
ACCESS_TOKEN_LIFETIME | 7200 (2 hours) | Validity duration for issued access tokens |
REFRESH_TOKEN_LIFETIME | 10368000 (120 days) | Validity duration for issued refresh tokens |
CODE_LIFETIME | 120 (2 minutes) | Expiry window for authorization codes |
CLIENT_ID_LENGTH | 24 characters | Generated length for client identifiers |
CLIENT_SECRET_LENGTH | 30 characters | Generated length for raw client secrets |
ACCESS_TOKEN_LENGTH | 40 characters | Generated length for access tokens |
REFRESH_TOKEN_LENGTH | 40 characters | Generated length for refresh tokens |
CODE_LENGTH | 40 characters | Generated length for authorization codes |
CLIENT_ID_PREFIX | dub_app_ | Prefix string prepended to client IDs |
CLIENT_SECRET_PREFIX | dub_app_secret_ | Prefix string prepended to client secrets |
ACCESS_TOKEN_PREFIX | dub_access_token_ | Prefix string prepended to access tokens |
Sources: apps/web/lib/api/oauth/constants.ts:1-16
Note
The user.read permission scope acts as the default baseline scope, meaning client applications do not need to explicitly request it during the authorization phase. All other scopes like links.read, links.write, and analytics.read must be explicitly declared.
Sources: apps/web/lib/api/oauth/constants.ts:21-34
Supported Scopes and Descriptions
OAuth applications can request targeted permission scopes on behalf of users. The available scopes govern granular access to workspace resources, webhooks, and account profiles.
| Scope Identifier | 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
OAuth App Management in Dashboard
Workspace Developer Settings and App Registration
Workspace settings provide a developer section under /[slug]/settings/oauth-apps for managing integrations. The user interface fetches registered applications via SWR from /api/oauth/apps?workspaceId={workspaceId} or presents an empty state via EmptyState with a cube icon when no apps exist.
Sources: apps/web/app/app.dub.co/dashboard/slug/ee/settings/oauth-apps/page-client.tsx:12-43, apps/web/app/app.dub.co/dashboard/slug/ee/settings/oauth-apps/layout.tsx:8-19
Applications are registered via POST /api/oauth/apps, which requires the oauth_apps.write permission. The endpoint parses request bodies using createOAuthAppSchema, checks for slug conflicts against existing Integration records, and generates a client ID using createToken with OAUTH_CONFIG.CLIENT_ID_LENGTH and OAUTH_CONFIG.CLIENT_ID_PREFIX. If PKCE is disabled (!pkce), a raw client secret is generated using OAUTH_CONFIG.CLIENT_SECRET_LENGTH and OAUTH_CONFIG.CLIENT_SECRET_PREFIX, hashed via hashToken, and partially exposed as dub_app_secret_****${clientSecret.slice(-8)}. Logo uploads are stored in object storage under integrations/{integration.id}_{nanoid(7)}.
Sources: apps/web/app/api/oauth/apps/route.ts:45-160
Caution
The raw client secret is returned in the HTTP response body exactly once upon creation. If lost, it cannot be retrieved and must be regenerated through the secret rotation mechanism.
Sources: apps/web/app/api/oauth/apps/route.ts:136-142
App Inspection and Secret Rotation
Individual apps are managed at /[slug]/settings/oauth-apps/[appId]. The management client validates write access using clientAccessCheck and loads application metadata via SWR from /api/oauth/apps/[appId]?workspaceId={workspaceId}.
Sources: apps/web/app/app.dub.co/dashboard/slug/ee/settings/oauth-apps/appId/page-client.tsx:23-57
Client secrets can be rotated using the server action exported from generate-client-secret.ts, wrapped by authActionClient. The action verifies that the user holds the oauth_apps.write permission, checks that the integration exists within the workspace, generates a new token of length OAUTH_CONFIG.CLIENT_SECRET_LENGTH with prefix OAUTH_CONFIG.CLIENT_SECRET_PREFIX, updates the prisma.oAuthApp record with a new hashedClientSecret and partialClientSecret, and returns the raw secret.
Sources: apps/web/lib/actions/generate-client-secret.ts:17-50
Updates and deletions are handled by PATCH /api/oauth/apps/[appId] and DELETE /api/oauth/apps/[appId]. Updates upload new logos or delete old screenshots asynchronously using @vercel/functions waitUntil. Deletions remove associated storage assets and delete the corresponding integration record in the database.
Sources: apps/web/app/api/oauth/apps/appId/route.ts:52-214
Authorization Flow and Consent UI
Interactive Authorization and Consent Page
The OAuth authorization flow begins when a user navigates to the consent page at /auth/oauth/authorize. The page validates incoming query parameters using validateAuthorizeRequest against the authorizeRequestSchema. If the request parameters are invalid or the target integration is missing, an EmptyState component with the CubeSettings icon displays an error. When valid, the page renders a consent card displaying the integration's logo, name, developer information, and a warning banner if the app is unverified.
Sources: apps/web/app/app.dub.co/auth/oauth/authorize/page.tsx:23-114
Warning
Unverified applications display a prominent warning banner and can only be installed by their developer or within the developer's workspace.
Sources: apps/web/app/app.dub.co/auth/oauth/authorize/page.tsx:85-101
Workspace Selector and Scope Validation
The consent form incorporates an interactive WorkspaceSelector managed through AuthorizeWorkspaceProvider. Users must choose a target workspace before granting API access. Requested scopes are processed by ScopesRequested, which automatically appends the default user.read scope if absent and filters permissions against the user's role in the selected workspace via getDisplayedScopesForRole. Missing permissions display a warning tooltip.
Sources: apps/web/app/app.dub.co/auth/oauth/authorize/authorize-form.tsx:33-121, apps/web/app/app.dub.co/auth/oauth/authorize/scopes-requested.tsx:11-57
Note
If a user's role lacks a requested scope, a warning icon appears beside the permission, and the scope is excluded from the final grant without failing the entire authorization request.
Sources: apps/web/app/app.dub.co/auth/oauth/authorize/scopes-requested.tsx:43-52, apps/web/app/app.dub.co/auth/oauth/authorize/scopes-requested.tsx:93-100
Authorization Request Approval and Code Issuance
Submitting the consent form triggers a POST request to /api/oauth/authorize with the selected workspace identifier. The endpoint enforces integrations.write permissions, calculates granted scopes using getGrantedScopesForRole, and validates client configuration, redirect URIs, and PKCE parameters. For Stripe and Shopify integrations, it checks that the workspace plan is Business tier or higher.
Sources: apps/web/app/api/oauth/authorize/route.ts:17-87
| Validation Check | Error Code | Trigger Condition |
|---|---|---|
| Application existence | not_found | Integration record not found for clientId |
| Plan requirement | bad_request | Stripe or Shopify integration requested on a free or pro workspace plan |
| Redirect URI | bad_request | Provided redirect_uri is absent from the application's allowed list |
| PKCE parameters | bad_request | app.pkce is enabled but code_challenge or code_challenge_method is missing |
| App installation | forbidden | User lacks installation rights for an unverified application |
Sources: apps/web/app/api/oauth/authorize/route.ts:52-100
Upon successful validation, an authorization code is generated using createToken with a length of OAUTH_CONFIG.CODE_LENGTH and persisted in the database via prisma.oAuthCode.create with an expiration window defined by OAUTH_CONFIG.CODE_LIFETIME (2 minutes). The endpoint constructs a callback URL appending the authorization code and optional state, returning it to the client for redirection.
Sources: apps/web/app/api/oauth/authorize/route.ts:102-129, apps/web/lib/api/oauth/constants.ts:5, apps/web/lib/api/oauth/constants.ts:11
Token Exchange and PKCE Verification
Handling Token Grant Requests
The endpoint located at /api/oauth/token processes incoming POST requests containing form-urlencoded payloads. The maximum execution duration for this route is set to 30 seconds, and all responses incorporate standard Cross-Origin Resource Sharing (CORS) headers via COMMON_CORS_HEADERS. Incoming data is parsed using tokenGrantSchema, routing payloads with a grant_type of authorization_code to exchangeAuthCodeForToken and refresh_token requests to refreshAccessToken.
export const maxDuration = 30;
// POST /api/oauth/token - Exchange authorization code for access token and refresh access token
export async function POST(req: NextRequest) {
try {
const formData = Object.fromEntries(await req.formData());
const validatedData = tokenGrantSchema.parse(formData);
if (validatedData.grant_type === "authorization_code") {
const data = await exchangeAuthCodeForToken(req, validatedData);
return NextResponse.json(data, {
headers: COMMON_CORS_HEADERS,
});
} else if (validatedData.grant_type === "refresh_token") {
const data = await refreshAccessToken(req, validatedData);
return NextResponse.json(data, {
headers: COMMON_CORS_HEADERS,
});
}
} catch (error) {
return handleAndReturnErrorResponse(error, COMMON_CORS_HEADERS);
}
}Sources: apps/web/app/api/oauth/token/route.ts:8-30
| Grant Type | Handler Function | Purpose |
|---|---|---|
authorization_code | exchangeAuthCodeForToken | Exchanges a valid authorization code for an access token and refresh token |
refresh_token | refreshAccessToken | Rotates an existing refresh token to issue a new access token |
Sources: apps/web/app/api/oauth/token/route.ts:16-26
Note
If neither client_id nor client_secret is present in the request body, the handler inspects the Authorization header for Basic authentication credentials, decoding the base64-encoded string to extract the client identifier and secret.
Sources: apps/web/app/api/oauth/token/exchange-code-for-token.ts:27-41
PKCE and Client Secret Validation
When PKCE is enabled on an OAuth application (app.pkce), the code_verifier parameter becomes mandatory. If PKCE is disabled, the client_secret is required, hashed via hashToken, and compared against app.hashedClientSecret using timingSafeCompare to prevent timing attacks.
// When PKCE is enabled, the code_verifier is required
if (app.pkce) {
if (!codeVerifier) {
throw new DubApiError({
code: "bad_request",
message: "Missing code_verifier parameter",
});
}
}
// When PKCE is not enabled, the client_secret is required
else if (!app.pkce) {
if (!clientSecret) {
throw new DubApiError({
code: "unauthorized",
message: "Missing client_secret",
});
}
const hashedClientSecret = await hashToken(clientSecret);
if (!timingSafeCompare(hashedClientSecret, app.hashedClientSecret)) {
throw new DubApiError({
code: "unauthorized",
message: "Invalid client_secret",
});
}
}Sources: apps/web/app/api/oauth/token/exchange-code-for-token.ts:85-111
For PKCE-enabled exchanges, the code challenge verification inspects accessCode.codeChallengeMethod. If set to S256, the code_verifier is hashed using generateCodeChallengeHash; otherwise, it is evaluated directly against accessCode.codeChallenge. Mismatches result in an unauthorized error with an invalid_grant message.
if (app.pkce) {
const codeChallenge =
accessCode.codeChallengeMethod === "S256"
? await generateCodeChallengeHash(codeVerifier!)
: codeVerifier;
if (accessCode.codeChallenge != codeChallenge) {
throw new DubApiError({
code: "unauthorized",
message: "invalid_grant",
});
}
}Sources: apps/web/app/api/oauth/token/exchange-code-for-token.ts:120-132
Warning
Expired authorization codes trigger an immediate deletion via prisma.oAuthCode.delete accompanied by a bad_request error response indicating that the code has expired.
Sources: apps/web/app/api/oauth/token/exchange-code-for-token.ts:134-146
Token Issuance and Persistence
Upon successful validation, the system installs the integration for the user and workspace, then provisions a restricted access token and a refresh token. Lifetimes and token string characteristics are governed by OAUTH_CONFIG.
export const OAUTH_CONFIG = {
ACCESS_TOKEN_LIFETIME: 2 * 60 * 60, // 2 hours
REFRESH_TOKEN_LIFETIME: 120 * 24 * 60 * 60, // 120 days
CODE_LIFETIME: 2 * 60, // 2 minutes
CLIENT_ID_LENGTH: 24,
CLIENT_SECRET_LENGTH: 30,
ACCESS_TOKEN_LENGTH: 40,
REFRESH_TOKEN_LENGTH: 40,
CODE_LENGTH: 40,
CLIENT_ID_PREFIX: "dub_app_",
CLIENT_SECRET_PREFIX: "dub_app_secret_",
ACCESS_TOKEN_PREFIX: "dub_access_token_",
};Sources: apps/web/lib/api/oauth/constants.ts:2-16
| Configuration Parameter | Value | Description |
|---|---|---|
ACCESS_TOKEN_LIFETIME | 7200 (2 hours) | Lifetime of issued access tokens in seconds |
REFRESH_TOKEN_LIFETIME | 10368000 (120 days) | Lifetime of refresh tokens in seconds |
CODE_LIFETIME | 120 (2 minutes) | Expiration window for authorization codes |
ACCESS_TOKEN_LENGTH | 40 | Character length for access tokens |
REFRESH_TOKEN_LENGTH | 40 | Character length for refresh tokens |
ACCESS_TOKEN_PREFIX | dub_access_token_ | String prefix prepended to access tokens |
Sources: apps/web/lib/api/oauth/constants.ts:2-16
Once tokens are created, Vercel's waitUntil function asynchronously deletes the consumed authorization code and purges any prior restricted tokens associated with the same installation identifier, ensuring only one active token set remains valid per client-user-workspace combination.
waitUntil(
Promise.all([
// Delete the code after it's been used
prisma.oAuthCode.delete({
where: {
code,
},
}),
// Remove all existing tokens for this client
prisma.restrictedToken.deleteMany({
where: {
installationId: installation.id,
id: {
not: restrictedToken.id,
},
},
}),
]),
);Sources: apps/web/app/api/oauth/token/exchange-code-for-token.ts:201-220
Token Refresh and User Info
Refreshing Expired Access Tokens
Clients request new access tokens by submitting a POST request to /api/oauth/token with grant_type set to refresh_token. The refreshAccessToken handler accepts the payload, resolving credentials from the request body or via HTTP Basic Authentication credentials parsed from the Authorization header.
export const refreshAccessToken = async (
req: NextRequest,
params: z.infer<typeof refreshTokenSchema>,
) => {
let {
refresh_token,
client_id: clientId,
client_secret: clientSecret,
} = params;
// If no client_id or client_secret is provided in the request body
// then it should be provided in the Authorization header as Basic Auth for non-PKCE
if (!clientId && !clientSecret) {
const authorizationHeader = req.headers.get("Authorization") || "";
const [type, token] = authorizationHeader.split(" ");
if (type === "Basic") {
const splits = Buffer.from(token, "base64").toString("utf-8").split(":");
if (splits.length > 1) {
clientId = splits[0];
clientSecret = splits[1];
}
}
}Sources: apps/web/app/api/oauth/token/refresh-access-token.ts:13-37
The application validates the client_id against prisma.oAuthApp and evaluates PKCE support. If PKCE is disabled, the supplied client_secret is hashed and checked against hashedClientSecret using timingSafeCompare.
const oAuthApp = await prisma.oAuthApp.findUnique({
where: {
clientId,
},
select: {
pkce: true,
integrationId: true,
hashedClientSecret: true,
},
});
if (!oAuthApp) {
throw new DubApiError({
code: "unauthorized",
message: "OAuth app not found for the provided client_id",
});
}
if (!oAuthApp.pkce) {
if (!clientSecret) {
throw new DubApiError({
code: "unauthorized",
message: "Missing client_secret",
});
}
const hashedClientSecret = await hashToken(clientSecret);
if (!timingSafeCompare(hashedClientSecret, oAuthApp.hashedClientSecret)) {
throw new DubApiError({
code: "unauthorized",
message: "Invalid client_secret",
});
}
}Sources: apps/web/app/api/oauth/token/refresh-access-token.ts:46-79
Caution
If a refresh token record is missing in prisma.oAuthRefreshToken, or if its expiresAt timestamp precedes the current system time, an unauthorized error is thrown, invalidating the refresh attempt.
Sources: apps/web/app/api/oauth/token/refresh-access-token.ts:99-112
Upon validating the refresh token and its installation association, the old restricted access token is deleted within a Prisma transaction, and a replacement access token and refresh token are generated.
await prisma.$transaction([
// Delete the old access token
prisma.restrictedToken.delete({
where: {
id: accessToken.id,
},
}),
// Create the access token and refresh token
prisma.restrictedToken.create({
data: {
name: generateRandomName(),
hashedKey: await hashToken(newAccessToken),
partialKey: `${newAccessToken.slice(0, 3)}...${newAccessToken.slice(-4)}`,
scopes: accessToken.scopes,
expires: accessTokenExpires,
userId: authorizedApp.userId,
projectId: authorizedApp.projectId,
installationId: authorizedApp.id,
refreshTokens: {
create: {
installationId: authorizedApp.id,
hashedRefreshToken: await hashToken(newRefreshToken),
expiresAt: new Date(
Date.now() + OAUTH_CONFIG.REFRESH_TOKEN_LIFETIME * 1000,
),
},
},
},
}),
]);Sources: apps/web/app/api/oauth/token/refresh-access-token.ts:168-198
Resolving User Identity via User Info
The GET /api/oauth/userinfo endpoint extracts the bearer token from incoming requests using getAuthTokenOrThrow, hashes it, and queries active restricted token records that possess an associated installation ID and have not expired.
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,
},
},
},
});Sources: apps/web/app/api/oauth/userinfo/route.ts:16-47
If the token record is valid, the endpoint constructs an identity response containing the user identifier, display name, user image URL, and workspace details featuring a prefixed workspace identifier.
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,
});Sources: apps/web/app/api/oauth/userinfo/route.ts:56-72
Client SDK and CLI Integration
Dub CLI Authentication Flow
The Dub CLI utilizes @badgateway/oauth2-client to manage the OAuth 2.0 authorization code flow with PKCE. The client configuration hardcodes the client identifier dub_app_39527dcc11b452f38bb54a3a1664fd044d7158dfea8abcde, the authorization endpoint https://app.dub.co/oauth/authorize, and the token endpoint https://api.dub.co/oauth/token.
import { OAuth2Client } from "@badgateway/oauth2-client";
export const oauthClient = new OAuth2Client({
// TODO: add client id here
clientId: "dub_app_39527dcc11b452f38bb54a3a1664fd044d7158dfea8abcde",
authorizationEndpoint: "https://app.dub.co/oauth/authorize",
tokenEndpoint: "https://api.dub.co/oauth/token",
});Sources: packages/cli/src/utils/oauth.ts:1-8
When running the dub login command, a 64-character code verifier is generated using nanoid, and an authorization URL is requested with scopes links.read, links.write, and domains.read. An interactive browser window opens automatically via the open package, pointing to the authorization endpoint on app.dub.co.
export const login = new Command()
.name("login")
.description("Log into the Dub platform")
.action(async () => {
try {
const codeVerifier = getNanoid(64);
const redirectUri = "http://localhost:4587/callback";
const authUrl = await oauthClient.authorizationCode.getAuthorizeUri({
redirectUri,
codeVerifier,
scope: ["links.read", "links.write", "domains.read"],
});
const spinner = ora("Opening browser for authentication").start();
await open(authUrl);
spinner.text = "Waiting for authentication";
oauthCallbackServer({
oauthClient,
redirectUri,
codeVerifier,
spinner,
});
} catch (error) {
handleError(error);
}Sources: packages/cli/src/commands/login.ts:9-37
Concurrently, a local HTTP server listens on port 4587 specifically for the /callback path redirection from the authorization server.
export function oauthCallbackServer({
oauthClient,
redirectUri,
codeVerifier,
spinner,
}: OAuthCallbackServerProps) {
const server = http.createServer(async (req, res) => {
const reqUrl = url.parse(req.url || "", true);
if (reqUrl.pathname !== "/callback" || req.method !== "GET") {
res.writeHead(404);
res.end("Not found");
return;
}Sources: packages/cli/src/api/callback.ts:17-30
Warning
The local OAuth callback server enforces a strict 5-minute timeout window (300000 milliseconds), after which the server forcefully closes and terminates the process if authentication has not finished.
Sources: packages/cli/src/api/callback.ts:80-84
Once the authorization code is received at the local loopback receiver, the CLI exchanges the code for tokens and persists the session configuration locally.
try {
spinner.text = "Verifying";
const { accessToken, refreshToken, expiresAt } =
await oauthClient.authorizationCode.getToken({
code,
redirectUri,
codeVerifier,
});
spinner.text = "Configuring";
const configInfo: DubConfig = {
access_token: accessToken.trim(),
refresh_token: refreshToken,
expires_at: expiresAt,
domain: "dub.sh",
};
await setConfig(configInfo);
spinner.succeed("Configuration completed");Sources: packages/cli/src/api/callback.ts:43-63
External Client Integration Parameters
External integrations and companion packages interact with Dub's OAuth endpoints by configuring parameters such as response types, grant types, and code challenge methods. The Stripe App integration builds authorization URLs with PKCE S256 parameters and exchanges codes directly via POST requests to the token endpoint.
| Parameter Name | Value / Type | Purpose |
|---|---|---|
response_type | "code" | Specifies the authorization code grant flow |
code_challenge_method | "S256" | Cryptographic hashing method for PKCE verification |
grant_type | "authorization_code" / "refresh_token" | Token endpoint grant type specifier |
scope | ["links.read", "links.write", "domains.read"] | Requested permission scopes for CLI operations |
Sources: packages/cli/src/commands/login.ts:20-20, packages/stripe-app/src/utils/oauth.ts:20-27, packages/stripe-app/src/utils/oauth.ts:51-54, packages/stripe-app/src/utils/oauth.ts:134-136
Related
On This Page
OverviewOAuth Application Storage and SchemaDatabase Persistence ArchitectureConfiguration Constants and LifetimesSupported Scopes and DescriptionsOAuth App Management in DashboardWorkspace Developer Settings and App RegistrationApp Inspection and Secret RotationAuthorization Flow and Consent UIInteractive Authorization and Consent PageWorkspace Selector and Scope ValidationAuthorization Request Approval and Code IssuanceToken Exchange and PKCE VerificationHandling Token Grant RequestsPKCE and Client Secret ValidationToken Issuance and PersistenceToken Refresh and User InfoRefreshing Expired Access TokensResolving User Identity via User InfoClient SDK and CLI IntegrationDub CLI Authentication FlowExternal Client Integration ParametersRelated