From fd99844a43859f2c2ea8d9b330f1c122cddb1a98 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 8 Aug 2025 16:31:21 -0400 Subject: [PATCH] wip: allow configurable missing key behavior --- packages/backend/src/createRedirect.ts | 12 ++++++- .../nextjs/src/app-router/keyless-actions.ts | 6 ++-- packages/nextjs/src/server/clerkMiddleware.ts | 23 +++++++++++-- packages/nextjs/src/server/utils.ts | 17 +++++++++- packages/react/src/contexts/ClerkProvider.tsx | 10 ++++-- packages/react/src/types.ts | 8 +++++ packages/shared/src/error.ts | 32 +++++++++++++++++-- packages/shared/src/loadClerkJsScript.ts | 20 +++++++++--- 8 files changed, 114 insertions(+), 14 deletions(-) diff --git a/packages/backend/src/createRedirect.ts b/packages/backend/src/createRedirect.ts index 07f2f6b8123..6e338cb4e21 100644 --- a/packages/backend/src/createRedirect.ts +++ b/packages/backend/src/createRedirect.ts @@ -77,13 +77,21 @@ type CreateRedirect = (params: { signInUrl?: URL | string; signUpUrl?: URL | string; sessionStatus?: SessionStatusClaim | null; + /** + * Configures how to handle missing publishable key errors. + * - `'throw'`: Throw an error (default behavior) + * - `'fail_open'`: Continue without authentication, log a warning + * - `'warn'`: Log a warning but continue + * @default 'throw' + */ + missingKeyBehavior?: 'throw' | 'fail_open' | 'warn'; }) => { redirectToSignIn: RedirectFun; redirectToSignUp: RedirectFun; }; export const createRedirect: CreateRedirect = params => { - const { publishableKey, redirectAdapter, signInUrl, signUpUrl, baseUrl, sessionStatus } = params; + const { publishableKey, redirectAdapter, signInUrl, signUpUrl, baseUrl, sessionStatus, missingKeyBehavior } = params; const parsedPublishableKey = parsePublishableKey(publishableKey); const frontendApi = parsedPublishableKey?.frontendApi; const isDevelopment = parsedPublishableKey?.instanceType === 'development'; @@ -98,6 +106,7 @@ export const createRedirect: CreateRedirect = params => { const redirectToSignUp = ({ returnBackUrl }: RedirectToParams = {}) => { if (!signUpUrl && !accountsBaseUrl) { + // TODO: Update to use behavior parameter when errorThrower interface is updated errorThrower.throwMissingPublishableKeyError(); } @@ -124,6 +133,7 @@ export const createRedirect: CreateRedirect = params => { const redirectToSignIn = ({ returnBackUrl }: RedirectToParams = {}) => { if (!signInUrl && !accountsBaseUrl) { + // TODO: Update to use behavior parameter when errorThrower interface is updated errorThrower.throwMissingPublishableKeyError(); } diff --git a/packages/nextjs/src/app-router/keyless-actions.ts b/packages/nextjs/src/app-router/keyless-actions.ts index a9f2094544c..a217f7f6069 100644 --- a/packages/nextjs/src/app-router/keyless-actions.ts +++ b/packages/nextjs/src/app-router/keyless-actions.ts @@ -49,7 +49,9 @@ export async function syncKeylessConfigAction(args: AccountlessApplication & { r return; } -export async function createOrReadKeylessAction(): Promise> { +export async function createOrReadKeylessAction( + missingKeyBehavior?: import('@clerk/shared/error').MissingKeyBehavior, +): Promise> { if (!canUseKeyless) { return null; } @@ -57,7 +59,7 @@ export async function createOrReadKeylessAction(): Promise m.createOrReadKeyless()).catch(() => null); if (!result) { - errorThrower.throwMissingPublishableKeyError(); + errorThrower.throwMissingPublishableKeyError(missingKeyBehavior); return null; } diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index 5aee42276c8..acc1ccbf091 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -93,6 +93,15 @@ export interface ClerkMiddlewareOptions extends AuthenticateAnyRequestOptions { * When set, automatically injects a Content-Security-Policy header(s) compatible with Clerk. */ contentSecurityPolicy?: ContentSecurityPolicyOptions; + + /** + * Configures how to handle missing publishable key errors. + * - `'throw'`: Throw an error (default behavior) + * - `'fail_open'`: Continue without authentication, log a warning + * - `'warn'`: Log a warning but continue + * @default 'throw' + */ + missingKeyBehavior?: import('@clerk/shared/error').MissingKeyBehavior; } type ClerkMiddlewareOptionsCallback = (req: NextRequest) => ClerkMiddlewareOptions | Promise; @@ -143,7 +152,8 @@ export const clerkMiddleware = ((...args: unknown[]): NextMiddleware | NextMiddl const publishableKey = assertKey( resolvedParams.publishableKey || PUBLISHABLE_KEY || keyless?.publishableKey, - () => errorThrower.throwMissingPublishableKeyError(), + () => errorThrower.throwMissingPublishableKeyError(resolvedParams.missingKeyBehavior), + resolvedParams.missingKeyBehavior, ); const secretKey = assertKey(resolvedParams.secretKey || SECRET_KEY || keyless?.secretKey, () => @@ -152,8 +162,17 @@ export const clerkMiddleware = ((...args: unknown[]): NextMiddleware | NextMiddl const signInUrl = resolvedParams.signInUrl || SIGN_IN_URL; const signUpUrl = resolvedParams.signUpUrl || SIGN_UP_URL; + // In fail-open mode, if we don't have keys, we should skip auth entirely + if ( + !publishableKey && + resolvedParams.missingKeyBehavior !== import('@clerk/shared/error').MissingKeyBehavior.THROW + ) { + // Return early without authentication + return handler ? handler(() => ({}), request, event) : NextResponse.next(); + } + const options = { - publishableKey, + publishableKey: publishableKey, secretKey, signInUrl, signUpUrl, diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index 155788b0164..b2f4a413844 100644 --- a/packages/nextjs/src/server/utils.ts +++ b/packages/nextjs/src/server/utils.ts @@ -148,8 +148,23 @@ export function assertAuthStatus(req: RequestLike, error: string) { } } -export function assertKey(key: string | undefined, onError: () => never): string { +export function assertKey(key: string | undefined, onError: () => never): string; +export function assertKey( + key: string | undefined, + onError: () => never, + behavior: import('@clerk/shared/error').MissingKeyBehavior, +): string | undefined; +export function assertKey( + key: string | undefined, + onError: () => never, + behavior?: import('@clerk/shared/error').MissingKeyBehavior, +): string | undefined { if (!key) { + if (behavior && behavior !== import('@clerk/shared/error').MissingKeyBehavior.THROW) { + // For FAIL_OPEN and WARN modes, just call onError which will handle the behavior + onError(); + return undefined; + } onError(); } diff --git a/packages/react/src/contexts/ClerkProvider.tsx b/packages/react/src/contexts/ClerkProvider.tsx index 66b21ed8a35..f12c731732a 100644 --- a/packages/react/src/contexts/ClerkProvider.tsx +++ b/packages/react/src/contexts/ClerkProvider.tsx @@ -8,12 +8,18 @@ import { withMaxAllowedInstancesGuard } from '../utils'; import { ClerkContextProvider } from './ClerkContextProvider'; function ClerkProviderBase(props: ClerkProviderProps) { - const { initialState, children, __internal_bypassMissingPublishableKey, ...restIsomorphicClerkOptions } = props; + const { + initialState, + children, + __internal_bypassMissingPublishableKey, + missingKeyBehavior, + ...restIsomorphicClerkOptions + } = props; const { publishableKey = '', Clerk: userInitialisedClerk } = restIsomorphicClerkOptions; if (!userInitialisedClerk && !__internal_bypassMissingPublishableKey) { if (!publishableKey) { - errorThrower.throwMissingPublishableKeyError(); + errorThrower.throwMissingPublishableKeyError(missingKeyBehavior); } else if (publishableKey && !isPublishableKey(publishableKey)) { errorThrower.throwInvalidPublishableKeyError({ key: publishableKey }); } diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index b24a28a4df0..11830457343 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -60,6 +60,14 @@ export type ClerkProviderProps = IsomorphicClerkOptions & { * @internal */ __internal_bypassMissingPublishableKey?: boolean; + /** + * Configures how to handle missing publishable key errors. + * - `'throw'`: Throw an error (default behavior) + * - `'fail_open'`: Continue without authentication, log a warning + * - `'warn'`: Log a warning but continue + * @default 'throw' + */ + missingKeyBehavior?: import('@clerk/shared/error').MissingKeyBehavior; }; export interface BrowserClerkConstructor { diff --git a/packages/shared/src/error.ts b/packages/shared/src/error.ts index 90f8a4c4807..79c063b7dc4 100644 --- a/packages/shared/src/error.ts +++ b/packages/shared/src/error.ts @@ -4,6 +4,20 @@ import type { ClerkAPIResponseError as ClerkAPIResponseErrorInterface, } from '@clerk/types'; +/** + * Defines how to handle missing publishable key errors. + * + * @public + */ +export enum MissingKeyBehavior { + /** Throw an error (default behavior). */ + THROW = 'throw', + /** Continue without authentication, fail open. */ + FAIL_OPEN = 'fail_open', + /** Log a warning but continue. */ + WARN = 'warn', +} + /** * Checks if the provided error object is an unauthorized error. * @@ -345,7 +359,7 @@ export interface ErrorThrower { throwInvalidProxyUrl(params: { url?: string }): never; - throwMissingPublishableKeyError(): never; + throwMissingPublishableKeyError(behavior?: MissingKeyBehavior): never; throwMissingSecretKeyError(): never; @@ -409,7 +423,21 @@ export function buildErrorThrower({ packageName, customMessages }: ErrorThrowerO throw new Error(buildMessage(messages.InvalidProxyUrlErrorMessage, params)); }, - throwMissingPublishableKeyError(): never { + throwMissingPublishableKeyError(behavior: MissingKeyBehavior = MissingKeyBehavior.THROW): never { + if (behavior === MissingKeyBehavior.FAIL_OPEN) { + if (typeof console !== 'undefined') { + console.warn('[Clerk] Missing publishable key - continuing in fail-open mode'); + } + return undefined as never; + } + + if (behavior === MissingKeyBehavior.WARN) { + if (typeof console !== 'undefined') { + console.warn('[Clerk] Missing publishable key - this may cause authentication issues'); + } + return undefined as never; + } + throw new Error(buildMessage(messages.MissingPublishableKeyErrorMessage)); }, diff --git a/packages/shared/src/loadClerkJsScript.ts b/packages/shared/src/loadClerkJsScript.ts index f9edff97eb4..35e7bb1ed7f 100644 --- a/packages/shared/src/loadClerkJsScript.ts +++ b/packages/shared/src/loadClerkJsScript.ts @@ -1,6 +1,6 @@ import type { ClerkOptions, SDKMetadata, Without } from '@clerk/types'; -import { buildErrorThrower } from './error'; +import { buildErrorThrower, MissingKeyBehavior } from './error'; import { createDevOrStagingUrlCache, parsePublishableKey } from './keys'; import { loadScript } from './loadScript'; import { isValidProxyUrl, proxyUrlToAbsoluteURL } from './proxy'; @@ -27,7 +27,7 @@ export function setClerkJsLoadingErrorPackageName(packageName: string) { } type LoadClerkJsScriptOptions = Without & { - publishableKey: string; + publishableKey?: string; clerkJSUrl?: string; clerkJSVariant?: 'headless' | ''; clerkJSVersion?: string; @@ -41,6 +41,15 @@ type LoadClerkJsScriptOptions = Without & { * @default 15000 (15 seconds) */ scriptLoadTimeout?: number; + /** + * Configures how to handle missing publishable key errors. + * - `'throw'`: Throw an error (default behavior) + * - `'fail_open'`: Continue without authentication, log a warning + * - `'warn'`: Log a warning but continue. + * + * @default 'throw' + */ + missingKeyBehavior?: MissingKeyBehavior; }; /** @@ -147,8 +156,11 @@ const loadClerkJsScript = async (opts?: LoadClerkJsScriptOptions): Promise