Skip to main content
doc0blade47/dubdocumentation home
Sign in

OAuth Apps

Last updated Oct 12, 202663% grounded
Relevant source files

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

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 NameValue / LengthMeaning / Purpose
ACCESS_TOKEN_LIFETIME7200 (2 hours)Validity duration for issued access tokens
REFRESH_TOKEN_LIFETIME10368000 (120 days)Validity duration for issued refresh tokens
CODE_LIFETIME120 (2 minutes)Expiry window for authorization codes
CLIENT_ID_LENGTH24 charactersGenerated length for client identifiers
CLIENT_SECRET_LENGTH30 charactersGenerated length for raw client secrets
ACCESS_TOKEN_LENGTH40 charactersGenerated length for access tokens
REFRESH_TOKEN_LENGTH40 charactersGenerated length for refresh tokens
CODE_LENGTH40 charactersGenerated length for authorization codes
CLIENT_ID_PREFIXdub_app_Prefix string prepended to client IDs
CLIENT_SECRET_PREFIXdub_app_secret_Prefix string prepended to client secrets
ACCESS_TOKEN_PREFIXdub_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 IdentifierDescription
links.readRead access to links
links.writeRead and Write access to links
tags.readRead access to tags
tags.writeRead and Write access to tags
analytics.readRead access to analytics and events
domains.readRead access to domains
domains.writeRead and Write access to domains
user.readRead your name, email and profile image
webhooks.readRead access to webhooks
webhooks.writeRead and Write access to webhooks
folders.readRead access to folders
folders.writeRead 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

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 CheckError CodeTrigger Condition
Application existencenot_foundIntegration record not found for clientId
Plan requirementbad_requestStripe or Shopify integration requested on a free or pro workspace plan
Redirect URIbad_requestProvided redirect_uri is absent from the application's allowed list
PKCE parametersbad_requestapp.pkce is enabled but code_challenge or code_challenge_method is missing
App installationforbiddenUser 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 TypeHandler FunctionPurpose
authorization_codeexchangeAuthCodeForTokenExchanges a valid authorization code for an access token and refresh token
refresh_tokenrefreshAccessTokenRotates 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 ParameterValueDescription
ACCESS_TOKEN_LIFETIME7200 (2 hours)Lifetime of issued access tokens in seconds
REFRESH_TOKEN_LIFETIME10368000 (120 days)Lifetime of refresh tokens in seconds
CODE_LIFETIME120 (2 minutes)Expiration window for authorization codes
ACCESS_TOKEN_LENGTH40Character length for access tokens
REFRESH_TOKEN_LENGTH40Character length for refresh tokens
ACCESS_TOKEN_PREFIXdub_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 NameValue / TypePurpose
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

Was this page helpful?