Skip to content

Satellite domains cannot be tested when using ports on domains #8263

@jeffreytgilbert

Description

@jeffreytgilbert

Preliminary Checks

Reproduction

https://github.com/jeffreytgilbert/javascript/tree/fix/allowed-redirect-origins-non-default-ports

Publishable key

pk_test_aW50aW1hdGUtdXJjaGluLTc1LmNsZXJrLmFjY291bnRzLmRldiQ

Description

Summary

When using Clerk with custom local dev domains (e.g. Vite dev servers on :5173, :5174, :5176), the Account Portal ignores the redirect_url parameter and falls back to the configured Home URL. The root cause is in isAllowedRedirect in @clerk/shared: glob patterns like https://*.example.com never match origins that include a non-default port (https://sub.example.com:5176), so the check always fails and the warning fires.


Affected packages

  • @clerk/sharedisAllowedRedirect in src/internal/clerk-js/url.ts

Steps to reproduce

  1. Set up any Clerk satellite app running on a custom local domain with a non-default port — e.g. https://app.admin.example.net:5174 (a typical Vite dev server).
  2. Use an auth passthrough pattern where the Account Portal's redirect_url points to another custom-domain dev server with a port — e.g. https://app.auth.example.net:5176/?target=....
  3. Sign in on the Account Portal (intimate-urchin-75.accounts.dev or equivalent dev instance).
  4. Observe that the browser does not redirect to https://app.auth.example.net:5176/?target=... — it goes to the Home URL configured in the Clerk Dashboard instead.

The browser console on the Account Portal shows:

Clerk: Redirect URL https://app.auth.example.net:5176/?target=... is not on one of the
allowedRedirectOrigins, falling back to the default redirect URL.

This occurs even though https://*.example.net is one of the origins in the generated allowedRedirectOrigins list (via createAllowedRedirectOrigins), and even if the user explicitly adds https://app.auth.example.net:5176 to the allowedRedirectOrigins prop on their ClerkProvider.


Expected behavior

https://*.example.net should match https://app.auth.example.net:5176. A pattern that covers a domain should cover that domain on any port, since non-default ports only appear in local dev environments — in production, HTTPS traffic is always on port 443 and url.port is the empty string.


Actual behavior

The match always fails for any URL with a non-default port when the pattern is a string. The warning fires and Clerk redirects to the Home URL, breaking the redirect_url flow entirely.


Root cause

isAllowedRedirect converts string entries in allowedRedirectOrigins to regexes using glob-to-regexp, then tests them against url.origin:

// packages/shared/src/internal/clerk-js/url.ts
const isAllowed =
  !isProblematicUrl(url) &&
  (isSameOrigin ||
    allowedRedirectOrigins
      .map(origin => (typeof origin === 'string' ? globs.toRegexp(trimTrailingSlash(origin)) : origin))
      .some(origin => origin.test(trimTrailingSlash(url.origin))));

glob-to-regexp converts https://*.example.net to a regex roughly equivalent to /^https:\/\/[^/]*\.example\.net$/. When url.origin is https://app.auth.example.net:5176, the trailing :5176 causes the match to fail — the regex is anchored to .net$ and the port breaks it.

This also means that explicitly adding 'https://app.auth.example.net:5176' to allowedRedirectOrigins in the satellite's ClerkProvider does not help, because that prop controls validation within the satellite app's own SDK instance — it has no effect on the Account Portal's isAllowedRedirect check, which uses the origins generated by createAllowedRedirectOrigins (derived from the instance's FAPI configuration).

The default origins generated by createAllowedRedirectOrigins for a dev instance are:

https://jurying.net          ← no port
https://*.jurying.net        ← no port, glob fails on :5176
https://foo-bar-42.clerk.accounts.dev

None of these match a URL with a non-default port.


Proposed fix

After the full-origin test fails, also test each pattern against the port-stripped origin (protocol + hostname only) when the redirect URL has an explicit non-default port. url.port is an empty string for default ports (443 for https, 80 for http), so this fallback is only reached in non-standard-port contexts. The domain must still satisfy the pattern — only the port check is relaxed.

const patterns = allowedRedirectOrigins.map(origin =>
  typeof origin === 'string' ? globs.toRegexp(trimTrailingSlash(origin)) : origin,
);

const portlessOrigin = url.port ? `${url.protocol}//${url.hostname}` : null;

const isAllowed =
  !isProblematicUrl(url) &&
  (isSameOrigin ||
    patterns.some(
      pattern =>
        pattern.test(trimTrailingSlash(url.origin)) ||
        (portlessOrigin !== null && pattern.test(portlessOrigin)),
    ));

A PR with this fix, tests, and a changeset is available at: https://github.com/jeffreytgilbert/javascript/tree/fix/allowed-redirect-origins-non-default-ports


Why the ClerkProvider.allowedRedirectOrigins prop doesn't help

Worth calling out explicitly because it's the obvious first thing people try. The allowedRedirectOrigins prop on a satellite's ClerkProvider only gates redirects within that satellite app's own SDK context. The Account Portal is Clerk's hosted application — you cannot pass props to its ClerkProvider. Its allowedRedirectOrigins list comes from createAllowedRedirectOrigins, which uses the Clerk instance's FAPI-configured origins. No amount of configuring the satellite's ClerkProvider affects what the Account Portal validates.


Environment

  • @clerk/shared: 4.4.0 (confirmed in source — behaviour is present in current main)
  • Trigger context: Account Portal → satellite redirect in local dev with Vite (non-standard port)
  • Reproduces with both the default origins from createAllowedRedirectOrigins and user-supplied string entries in allowedRedirectOrigins

Environment

System:
    OS: macOS 26.4
    CPU: (10) arm64 Apple M1 Max
    Memory: 2.87 GB / 64.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 24.14.1 - /Users/jeffreygilbert/.nvm/versions/node/v24.14.1/bin/node
    npm: 11.11.0 - /Users/jeffreygilbert/.nvm/versions/node/v24.14.1/bin/npm
    pnpm: 10.33.0 - /opt/homebrew/bin/pnpm
    bun: 1.3.11 - /opt/homebrew/bin/bun
  Browsers:
    Brave Browser: 146.1.88.136
    Chrome: 146.0.7680.178
    Firefox: 148.0.2
    Safari: 26.4
    Safari Technology Preview: 26.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-triageA ticket that needs to be triaged by a team member

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions