Skip to content

Improve key missing error handling with custom hooks #6521

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
2 changes: 1 addition & 1 deletion packages/nextjs/src/app-router/keyless-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export async function createOrReadKeylessAction(): Promise<null | Omit<Accountle
const result = await import('../server/keyless-node.js').then(m => m.createOrReadKeyless()).catch(() => null);

if (!result) {
errorThrower.throwMissingPublishableKeyError();
// In non-keyless scenarios, allow continuing without throwing so the app can render.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ Why is Keyless being affected by these changes ?

return null;
}

Expand Down
82 changes: 66 additions & 16 deletions packages/nextjs/src/server/clerkMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ import type { AuthProtect } from './protect';
import { createProtect } from './protect';
import type { NextMiddlewareEvtParam, NextMiddlewareRequestParam, NextMiddlewareReturn } from './types';
import {
assertKey,
// assertKey, // removed direct usage for keys to allow non-throwing behavior
decorateRequest,
handleMultiDomainAndProxy,
redirectAdapter,
Expand Down Expand Up @@ -93,6 +93,22 @@ export interface ClerkMiddlewareOptions extends AuthenticateAnyRequestOptions {
* When set, automatically injects a Content-Security-Policy header(s) compatible with Clerk.
*/
contentSecurityPolicy?: ContentSecurityPolicyOptions;

/**
* Optional hook invoked when the publishable key is missing. If it returns a response, it will be used as the middleware result.
* If it returns void, the request will continue unauthenticated.
*/
onMissingPublishableKey?: (
request: NextRequest,
) => NextMiddlewareReturn | Promise<NextMiddlewareReturn> | void | Promise<void>;

/**
* Optional hook invoked when the secret key is missing. If it returns a response, it will be used as the middleware result.
* If it returns void, the request will continue unauthenticated.
*/
onMissingSecretKey?: (
request: NextRequest,
) => NextMiddlewareReturn | Promise<NextMiddlewareReturn> | void | Promise<void>;
Comment on lines +96 to +111
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand the usage correctly those should only be executed in DEV. Also if we are thinking these hooks as something that will enable extra Clerk functionality we should prefix them with __internal_.

}

type ClerkMiddlewareOptionsCallback = (req: NextRequest) => ClerkMiddlewareOptions | Promise<ClerkMiddlewareOptions>;
Expand Down Expand Up @@ -141,14 +157,48 @@ export const clerkMiddleware = ((...args: unknown[]): NextMiddleware | NextMiddl

const keyless = await getKeylessCookieValue(name => request.cookies.get(name)?.value);

const publishableKey = assertKey(
resolvedParams.publishableKey || PUBLISHABLE_KEY || keyless?.publishableKey,
() => errorThrower.throwMissingPublishableKeyError(),
);
// Resolve keys without throwing
const resolvedPublishableKey = resolvedParams.publishableKey || PUBLISHABLE_KEY || keyless?.publishableKey;
const resolvedSecretKey = resolvedParams.secretKey || SECRET_KEY || keyless?.secretKey;

const secretKey = assertKey(resolvedParams.secretKey || SECRET_KEY || keyless?.secretKey, () =>
errorThrower.throwMissingSecretKeyError(),
);
// Handle missing publishable key
if (!resolvedPublishableKey) {
if (resolvedParams.onMissingPublishableKey) {
const maybeResponse = await resolvedParams.onMissingPublishableKey(request);
if (maybeResponse) {
return maybeResponse;
}
// Fall through to allow request to continue unauthenticated
const res = NextResponse.next();
setRequestHeadersOnNextResponse(res, request, {
[constants.Headers.AuthStatus]: 'signed-out',
});
return res;
}
Comment on lines +171 to +177
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you clarify how this operates on the client ?

@clerk/clerk-react will throw if the publishable key is missing, since to the url encoded withing the PK is required to hotload clerk-js.

// Default behavior remains throwing if no hook is provided
return errorThrower.throwMissingPublishableKeyError();
}

// Handle missing secret key
if (!resolvedSecretKey) {
if (resolvedParams.onMissingSecretKey) {
const maybeResponse = await resolvedParams.onMissingSecretKey(request);
if (maybeResponse) {
return maybeResponse;
}
// Fall through to allow request to continue unauthenticated
const res = NextResponse.next();
setRequestHeadersOnNextResponse(res, request, {
[constants.Headers.AuthStatus]: 'signed-out',
});
return res;
}
// Default behavior remains throwing if no hook is provided
return errorThrower.throwMissingSecretKeyError();
}

const publishableKey = resolvedPublishableKey;
const secretKey = resolvedSecretKey;
const signInUrl = resolvedParams.signInUrl || SIGN_IN_URL;
const signUpUrl = resolvedParams.signUpUrl || SIGN_UP_URL;

Expand All @@ -158,7 +208,7 @@ export const clerkMiddleware = ((...args: unknown[]): NextMiddleware | NextMiddl
signInUrl,
signUpUrl,
...resolvedParams,
};
} as ClerkMiddlewareOptions;

// Propagates the request data to be accessed on the server application runtime from helpers such as `clerkClient`
clerkMiddlewareRequestDataStore.set('requestData', options);
Expand Down Expand Up @@ -198,7 +248,7 @@ export const clerkMiddleware = ((...args: unknown[]): NextMiddleware | NextMiddl
const locationHeader = requestState.headers.get(constants.Headers.Location);
if (locationHeader) {
const res = NextResponse.redirect(locationHeader);
requestState.headers.forEach((value, key) => {
requestState.headers.forEach((value: string, key: string) => {
if (key === constants.Headers.Location) {
return;
}
Expand Down Expand Up @@ -247,7 +297,7 @@ export const clerkMiddleware = ((...args: unknown[]): NextMiddleware | NextMiddl
// TODO @nikos: we need to make this more generic
// and move the logic in clerk/backend
if (requestState.headers) {
requestState.headers.forEach((value, key) => {
requestState.headers.forEach((value: string, key: string) => {
if (key === constants.Headers.ContentSecurityPolicy) {
logger.debug('Content-Security-Policy detected', () => ({
value,
Expand All @@ -259,7 +309,7 @@ export const clerkMiddleware = ((...args: unknown[]): NextMiddleware | NextMiddl

if (isRedirect(handlerResult)) {
logger.debug('handlerResult is redirect');
return serverRedirectWithAuth(clerkRequest, handlerResult, options);
return serverRedirectWithAuth(clerkRequest, handlerResult, { secretKey });
}

if (options.debug) {
Expand All @@ -270,10 +320,10 @@ export const clerkMiddleware = ((...args: unknown[]): NextMiddleware | NextMiddl
// Only pass keyless credentials when there are no explicit keys
secretKey === keyless?.secretKey
? {
publishableKey: keyless?.publishableKey,
secretKey: keyless?.secretKey,
publishableKey: keyless?.publishableKey || '',
secretKey: keyless?.secretKey || '',
}
: {};
: { publishableKey: '', secretKey: '' };

decorateRequest(
clerkRequest,
Expand Down Expand Up @@ -514,7 +564,7 @@ const handleControlFlowErrors = (
sessionStatus: requestState.toAuth()?.sessionStatus,
});

const { returnBackUrl } = e;
const { returnBackUrl } = e as any;
return redirect[isRedirectToSignIn ? 'redirectToSignIn' : 'redirectToSignUp']({ returnBackUrl });
}

Expand Down
17 changes: 17 additions & 0 deletions packages/types/tsup.config.bundled_g21w90c7pz.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// tsup.config.ts
import { defineConfig } from 'tsup';
var tsup_config_default = defineConfig(() => {
return {
entry: {
index: 'src/index.ts',
},
minify: false,
clean: true,
sourcemap: true,
format: ['cjs', 'esm'],
legacyOutput: true,
dts: true,
};
});
export { tsup_config_default as default };
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidHN1cC5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9faW5qZWN0ZWRfZmlsZW5hbWVfXyA9IFwiL3dvcmtzcGFjZS9wYWNrYWdlcy90eXBlcy90c3VwLmNvbmZpZy50c1wiO2NvbnN0IF9faW5qZWN0ZWRfZGlybmFtZV9fID0gXCIvd29ya3NwYWNlL3BhY2thZ2VzL3R5cGVzXCI7Y29uc3QgX19pbmplY3RlZF9pbXBvcnRfbWV0YV91cmxfXyA9IFwiZmlsZTovLy93b3Jrc3BhY2UvcGFja2FnZXMvdHlwZXMvdHN1cC5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd0c3VwJztcblxuZXhwb3J0IGRlZmF1bHQgZGVmaW5lQ29uZmlnKCgpID0+IHtcbiAgcmV0dXJuIHtcbiAgICBlbnRyeToge1xuICAgICAgaW5kZXg6ICdzcmMvaW5kZXgudHMnLFxuICAgIH0sXG4gICAgbWluaWZ5OiBmYWxzZSxcbiAgICBjbGVhbjogdHJ1ZSxcbiAgICBzb3VyY2VtYXA6IHRydWUsXG4gICAgZm9ybWF0OiBbJ2NqcycsICdlc20nXSxcbiAgICBsZWdhY3lPdXRwdXQ6IHRydWUsXG4gICAgZHRzOiB0cnVlLFxuICB9O1xufSk7XG4iXSwKICAibWFwcGluZ3MiOiAiO0FBQXlOLFNBQVMsb0JBQW9CO0FBRXRQLElBQU8sc0JBQVEsYUFBYSxNQUFNO0FBQ2hDLFNBQU87QUFBQSxJQUNMLE9BQU87QUFBQSxNQUNMLE9BQU87QUFBQSxJQUNUO0FBQUEsSUFDQSxRQUFRO0FBQUEsSUFDUixPQUFPO0FBQUEsSUFDUCxXQUFXO0FBQUEsSUFDWCxRQUFRLENBQUMsT0FBTyxLQUFLO0FBQUEsSUFDckIsY0FBYztBQUFBLElBQ2QsS0FBSztBQUFBLEVBQ1A7QUFDRixDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo=
Comment on lines +1 to +17
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was this autogenerated ?

Loading
Loading