Skip to content

wip: allow configurable missing key behavior #6496

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion packages/backend/src/createRedirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,21 @@ type CreateRedirect = <ReturnType>(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<ReturnType>;
redirectToSignUp: RedirectFun<ReturnType>;
};

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';
Expand All @@ -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();
}

Expand All @@ -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();
}

Expand Down
6 changes: 4 additions & 2 deletions packages/nextjs/src/app-router/keyless-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,17 @@ export async function syncKeylessConfigAction(args: AccountlessApplication & { r
return;
}

export async function createOrReadKeylessAction(): Promise<null | Omit<AccountlessApplication, 'secretKey'>> {
export async function createOrReadKeylessAction(
missingKeyBehavior?: import('@clerk/shared/error').MissingKeyBehavior,
): Promise<null | Omit<AccountlessApplication, 'secretKey'>> {
if (!canUseKeyless) {
return null;
}

const result = await import('../server/keyless-node.js').then(m => m.createOrReadKeyless()).catch(() => null);

if (!result) {
errorThrower.throwMissingPublishableKeyError();
errorThrower.throwMissingPublishableKeyError(missingKeyBehavior);
return null;
}

Expand Down
23 changes: 21 additions & 2 deletions packages/nextjs/src/server/clerkMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ClerkMiddlewareOptions>;
Expand Down Expand Up @@ -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, () =>
Expand All @@ -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,
Expand Down
17 changes: 16 additions & 1 deletion packages/nextjs/src/server/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
10 changes: 8 additions & 2 deletions packages/react/src/contexts/ClerkProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
Expand Down
8 changes: 8 additions & 0 deletions packages/react/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
32 changes: 30 additions & 2 deletions packages/shared/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -345,7 +359,7 @@ export interface ErrorThrower {

throwInvalidProxyUrl(params: { url?: string }): never;

throwMissingPublishableKeyError(): never;
throwMissingPublishableKeyError(behavior?: MissingKeyBehavior): never;

throwMissingSecretKeyError(): never;

Expand Down Expand Up @@ -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));
},

Expand Down
20 changes: 16 additions & 4 deletions packages/shared/src/loadClerkJsScript.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -27,7 +27,7 @@ export function setClerkJsLoadingErrorPackageName(packageName: string) {
}

type LoadClerkJsScriptOptions = Without<ClerkOptions, 'isSatellite'> & {
publishableKey: string;
publishableKey?: string;
clerkJSUrl?: string;
clerkJSVariant?: 'headless' | '';
clerkJSVersion?: string;
Expand All @@ -41,6 +41,15 @@ type LoadClerkJsScriptOptions = Without<ClerkOptions, 'isSatellite'> & {
* @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;
};

/**
Expand Down Expand Up @@ -147,8 +156,11 @@ const loadClerkJsScript = async (opts?: LoadClerkJsScriptOptions): Promise<HTMLS
}

if (!opts?.publishableKey) {
errorThrower.throwMissingPublishableKeyError();
return null;
errorThrower.throwMissingPublishableKeyError(opts?.missingKeyBehavior);
// In fail-open or warn mode, we can't load Clerk.js without a key, so return null
if (opts?.missingKeyBehavior !== MissingKeyBehavior.THROW) {
return null;
}
}

const loadPromise = waitForClerkWithTimeout(timeout);
Expand Down
Loading