From 877b55b2ed15f75ef2cc3b2b1591a1ebe5cefe1c Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 5 Jun 2024 17:48:44 -0300 Subject: [PATCH 01/39] Require signing key to be defined when using runtime options --- packages/nextjs/src/server/clerkMiddleware.ts | 10 +++++++++- packages/nextjs/src/server/constants.ts | 1 + packages/nextjs/src/server/errors.ts | 2 ++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index 7c11f520387..16631c1be65 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -13,7 +13,8 @@ import { NextResponse } from 'next/server'; import { isRedirect, serverRedirectWithAuth, setHeader } from '../utils'; import { withLogger } from '../utils/debugLogger'; import { clerkClient } from './clerkClient'; -import { PUBLISHABLE_KEY, SECRET_KEY, SIGN_IN_URL, SIGN_UP_URL } from './constants'; +import { PUBLISHABLE_KEY, SECRET_KEY, SIGN_IN_URL, SIGN_UP_URL, SIGNING_KEY } from './constants'; +import { missingSigningKey } from './errors'; import { errorThrower } from './errorThrower'; import type { AuthProtect } from './protect'; import { createProtect } from './protect'; @@ -72,10 +73,17 @@ interface ClerkMiddleware { export const clerkMiddleware: ClerkMiddleware = withLogger('clerkMiddleware', logger => (...args: unknown[]): any => { const [request, event] = parseRequestAndEvent(args); const [handler, params] = parseHandlerAndOptions(args); + if (params.debug) { logger.enable(); } + const hasProvidedRuntimeOptions = params.secretKey || params.signInUrl || params.signInUrl; + + if (hasProvidedRuntimeOptions && !SIGNING_KEY) { + throw new Error(missingSigningKey); + } + const publishableKey = assertKey(params.publishableKey || PUBLISHABLE_KEY, () => errorThrower.throwMissingPublishableKeyError(), ); diff --git a/packages/nextjs/src/server/constants.ts b/packages/nextjs/src/server/constants.ts index 38749e24510..0ebbb4774e4 100644 --- a/packages/nextjs/src/server/constants.ts +++ b/packages/nextjs/src/server/constants.ts @@ -6,6 +6,7 @@ export const CLERK_JS_URL = process.env.NEXT_PUBLIC_CLERK_JS_URL || ''; export const API_VERSION = process.env.CLERK_API_VERSION || 'v1'; export const SECRET_KEY = process.env.CLERK_SECRET_KEY || ''; export const PUBLISHABLE_KEY = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY || ''; +export const SIGNING_KEY = process.env.CLERK_SIGNING_KEY || ''; export const API_URL = process.env.CLERK_API_URL || apiUrlFromPublishableKey(PUBLISHABLE_KEY); export const DOMAIN = process.env.NEXT_PUBLIC_CLERK_DOMAIN || ''; export const PROXY_URL = process.env.NEXT_PUBLIC_CLERK_PROXY_URL || ''; diff --git a/packages/nextjs/src/server/errors.ts b/packages/nextjs/src/server/errors.ts index 8e23e9d5dd4..149393ba27d 100644 --- a/packages/nextjs/src/server/errors.ts +++ b/packages/nextjs/src/server/errors.ts @@ -97,3 +97,5 @@ For additional information about middleware, please visit https://clerk.com/docs }; export const authSignatureInvalid = `Clerk: Unable to verify request, this usually means the Clerk middleware did not run. Ensure Clerk's middleware is properly integrated and matches the current route. For more information, see: https://clerk.com/docs/nextjs/middleware. (code=auth_signature_invalid)`; + +export const missingSigningKey = `Clerk: Missing CLERK_SIGNING_KEY. When passing runtime options to clerkMiddleware, make sure to define CLERK_SIGNING_KEY.`; From 04a4cebc8a1621a72905679dc9db7e7234c070cc Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 5 Jun 2024 18:11:29 -0300 Subject: [PATCH 02/39] Add constant definition for `x-clerk-data` header --- packages/backend/src/constants.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/backend/src/constants.ts b/packages/backend/src/constants.ts index fa7ac5c5680..59a28f409cf 100644 --- a/packages/backend/src/constants.ts +++ b/packages/backend/src/constants.ts @@ -39,6 +39,7 @@ const Headers = { AuthMessage: 'x-clerk-auth-message', ClerkUrl: 'x-clerk-clerk-url', EnableDebug: 'x-clerk-debug', + ClerkData: 'x-clerk-data', ClerkRedirectTo: 'x-clerk-redirect-to', CloudFrontForwardedProto: 'cloudfront-forwarded-proto', Authorization: 'authorization', From 604d08d7a3a653b64ecd08f70c5fd3dc22800dec Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 5 Jun 2024 18:30:51 -0300 Subject: [PATCH 03/39] Add utilities to encrypt/decrypt request data --- packages/backend/src/constants.ts | 2 +- packages/nextjs/src/server/clerkMiddleware.ts | 2 -- packages/nextjs/src/server/utils.ts | 21 ++++++++++++++++++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/constants.ts b/packages/backend/src/constants.ts index 59a28f409cf..7fff948c6b9 100644 --- a/packages/backend/src/constants.ts +++ b/packages/backend/src/constants.ts @@ -39,7 +39,7 @@ const Headers = { AuthMessage: 'x-clerk-auth-message', ClerkUrl: 'x-clerk-clerk-url', EnableDebug: 'x-clerk-debug', - ClerkData: 'x-clerk-data', + ClerkRequestData: 'x-clerk-request-data', ClerkRedirectTo: 'x-clerk-redirect-to', CloudFrontForwardedProto: 'cloudfront-forwarded-proto', Authorization: 'authorization', diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index 16631c1be65..77b111ffbc3 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -73,13 +73,11 @@ interface ClerkMiddleware { export const clerkMiddleware: ClerkMiddleware = withLogger('clerkMiddleware', logger => (...args: unknown[]): any => { const [request, event] = parseRequestAndEvent(args); const [handler, params] = parseHandlerAndOptions(args); - if (params.debug) { logger.enable(); } const hasProvidedRuntimeOptions = params.secretKey || params.signInUrl || params.signInUrl; - if (hasProvidedRuntimeOptions && !SIGNING_KEY) { throw new Error(missingSigningKey); } diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index 7d6eb6b7795..4e306c12ce1 100644 --- a/packages/nextjs/src/server/utils.ts +++ b/packages/nextjs/src/server/utils.ts @@ -3,12 +3,13 @@ import { constants } from '@clerk/backend/internal'; import { handleValueOrFn } from '@clerk/shared/handleValueOrFn'; import { isDevelopmentFromSecretKey } from '@clerk/shared/keys'; import { isHttpOrHttps } from '@clerk/shared/proxy'; +import AES from 'crypto-js/aes'; import hmacSHA1 from 'crypto-js/hmac-sha1'; import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; import { constants as nextConstants } from '../constants'; -import { DOMAIN, IS_SATELLITE, PROXY_URL, SECRET_KEY, SIGN_IN_URL } from './constants'; +import { DOMAIN, IS_SATELLITE, PROXY_URL, SECRET_KEY, SIGN_IN_URL, SIGNING_KEY } from './constants'; import { authSignatureInvalid, missingDomainAndProxy, missingSignInUrlInDev } from './errors'; import type { RequestLike } from './types'; @@ -155,6 +156,7 @@ export function decorateRequest( [constants.Headers.AuthMessage]: message || '', [constants.Headers.AuthReason]: reason || '', [constants.Headers.ClerkUrl]: req.clerkUrl.toString(), + [constants.Headers.ClerkRequestData]: encryptClerkRequestData({ secretKey }), }); res.headers.set(nextConstants.Headers.NextRewrite, rewriteURL.href); } @@ -234,3 +236,20 @@ export function assertTokenSignature(token: string, key: string, signature?: str throw new Error(authSignatureInvalid); } } + +// TODO - Improve JSDocs +/** + * Encrypt request header value based on signing key + */ +function encryptClerkRequestData(options: Partial): string { + return AES.encrypt(JSON.stringify(options), SIGNING_KEY).toString(); +} + +// TODO - Improve JSDocs +/** + * Decrypt request header value based on signing key + */ +export function decryptClerkRequestData(encryptedRequestData: string): Partial { + const decryptedBytes = AES.decrypt(encryptedRequestData, SIGNING_KEY); + return JSON.parse(decryptedBytes.toString(CryptoJS.enc.Utf8)); +} From b83f91947d3b95251582e8ffcf05a45294059e2c Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 5 Jun 2024 18:52:13 -0300 Subject: [PATCH 04/39] Use propogated secret key --- packages/nextjs/src/server/createGetAuth.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/nextjs/src/server/createGetAuth.ts b/packages/nextjs/src/server/createGetAuth.ts index 207c2e9cb85..d9e8191baad 100644 --- a/packages/nextjs/src/server/createGetAuth.ts +++ b/packages/nextjs/src/server/createGetAuth.ts @@ -6,7 +6,7 @@ import { withLogger } from '../utils/debugLogger'; import { API_URL, API_VERSION, SECRET_KEY } from './constants'; import { getAuthAuthHeaderMissing } from './errors'; import type { RequestLike } from './types'; -import { assertTokenSignature, getAuthKeyFromRequest, getCookie, getHeader } from './utils'; +import { assertTokenSignature, decryptClerkRequestData, getAuthKeyFromRequest, getCookie, getHeader } from './utils'; export const createGetAuth = ({ noAuthStatusMessage, @@ -35,12 +35,15 @@ export const createGetAuth = ({ throw new Error(noAuthStatusMessage); } + const requestData = getHeader(req, constants.Headers.ClerkRequestData); + const decryptedRequestData = requestData ? decryptClerkRequestData(requestData) : {}; + const options = { authStatus, apiUrl: API_URL, apiVersion: API_VERSION, authMessage, - secretKey: opts?.secretKey || SECRET_KEY, + secretKey: opts?.secretKey || decryptedRequestData.secretKey || SECRET_KEY, authReason, }; From 1dc02f762fa8db95445258806c2349e9eefe0dcf Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 6 Jun 2024 13:27:11 -0300 Subject: [PATCH 05/39] Assert against signing key value --- .../nextjs/src/server/__tests__/clerkMiddleware.test.ts | 2 ++ packages/nextjs/src/server/clerkMiddleware.ts | 5 ++--- packages/nextjs/src/server/errors.ts | 2 -- packages/nextjs/src/server/utils.ts | 6 ++---- packages/shared/src/error.ts | 7 +++++++ 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts index 8996175599b..b2ee35c6149 100644 --- a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts +++ b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts @@ -621,4 +621,6 @@ describe('Dev Browser JWT when redirecting to cross origin for page requests', f ); expect(clerkClient.authenticateRequest).toBeCalled(); }); + + // TODO - Add tests }); diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index 77b111ffbc3..27245d81f54 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -14,7 +14,6 @@ import { isRedirect, serverRedirectWithAuth, setHeader } from '../utils'; import { withLogger } from '../utils/debugLogger'; import { clerkClient } from './clerkClient'; import { PUBLISHABLE_KEY, SECRET_KEY, SIGN_IN_URL, SIGN_UP_URL, SIGNING_KEY } from './constants'; -import { missingSigningKey } from './errors'; import { errorThrower } from './errorThrower'; import type { AuthProtect } from './protect'; import { createProtect } from './protect'; @@ -78,8 +77,8 @@ export const clerkMiddleware: ClerkMiddleware = withLogger('clerkMiddleware', lo } const hasProvidedRuntimeOptions = params.secretKey || params.signInUrl || params.signInUrl; - if (hasProvidedRuntimeOptions && !SIGNING_KEY) { - throw new Error(missingSigningKey); + if (hasProvidedRuntimeOptions) { + assertKey(SIGNING_KEY, () => errorThrower.throwMissingSigningKeyError()); } const publishableKey = assertKey(params.publishableKey || PUBLISHABLE_KEY, () => diff --git a/packages/nextjs/src/server/errors.ts b/packages/nextjs/src/server/errors.ts index 149393ba27d..8e23e9d5dd4 100644 --- a/packages/nextjs/src/server/errors.ts +++ b/packages/nextjs/src/server/errors.ts @@ -97,5 +97,3 @@ For additional information about middleware, please visit https://clerk.com/docs }; export const authSignatureInvalid = `Clerk: Unable to verify request, this usually means the Clerk middleware did not run. Ensure Clerk's middleware is properly integrated and matches the current route. For more information, see: https://clerk.com/docs/nextjs/middleware. (code=auth_signature_invalid)`; - -export const missingSigningKey = `Clerk: Missing CLERK_SIGNING_KEY. When passing runtime options to clerkMiddleware, make sure to define CLERK_SIGNING_KEY.`; diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index 4e306c12ce1..f2cc30baa24 100644 --- a/packages/nextjs/src/server/utils.ts +++ b/packages/nextjs/src/server/utils.ts @@ -237,17 +237,15 @@ export function assertTokenSignature(token: string, key: string, signature?: str } } -// TODO - Improve JSDocs /** - * Encrypt request header value based on signing key + * Encrypt request data using a signing key. */ function encryptClerkRequestData(options: Partial): string { return AES.encrypt(JSON.stringify(options), SIGNING_KEY).toString(); } -// TODO - Improve JSDocs /** - * Decrypt request header value based on signing key + * Decrypt request data using a signing key. */ export function decryptClerkRequestData(encryptedRequestData: string): Partial { const decryptedBytes = AES.decrypt(encryptedRequestData, SIGNING_KEY); diff --git a/packages/shared/src/error.ts b/packages/shared/src/error.ts index dd3fac4e4b3..9acc8df7690 100644 --- a/packages/shared/src/error.ts +++ b/packages/shared/src/error.ts @@ -197,6 +197,7 @@ const DefaultMessages = Object.freeze({ MissingPublishableKeyErrorMessage: `Missing publishableKey. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.`, MissingSecretKeyErrorMessage: `Missing secretKey. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.`, MissingClerkProvider: `{{source}} can only be used within the component. Learn more: https://clerk.com/docs/components/clerk-provider`, + MissingSigningKeyErrorMessage: `Missing signingKey. When passing runtime options to clerkMiddleware, make sure to define CLERK_SIGNING_KEY.`, }); type MessageKeys = keyof typeof DefaultMessages; @@ -223,6 +224,8 @@ export interface ErrorThrower { throwMissingSecretKeyError(): never; + throwMissingSigningKeyError(): never; + throwMissingClerkProviderError(params: { source?: string }): never; throw(message: string): never; @@ -281,6 +284,10 @@ export function buildErrorThrower({ packageName, customMessages }: ErrorThrowerO throw new Error(buildMessage(messages.MissingSecretKeyErrorMessage)); }, + throwMissingSigningKeyError(): never { + throw new Error(buildMessage(messages.MissingSigningKeyErrorMessage)); + }, + throwMissingClerkProviderError(params: { source?: string }): never { throw new Error(buildMessage(messages.MissingClerkProvider, params)); }, From a7cd6363f9f99b9847fedf6413262cf47119b1b6 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 6 Jun 2024 18:04:07 -0300 Subject: [PATCH 06/39] Fix CryptoJS import statement --- packages/nextjs/src/server/utils.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index f2cc30baa24..54f1fe65498 100644 --- a/packages/nextjs/src/server/utils.ts +++ b/packages/nextjs/src/server/utils.ts @@ -4,6 +4,7 @@ import { handleValueOrFn } from '@clerk/shared/handleValueOrFn'; import { isDevelopmentFromSecretKey } from '@clerk/shared/keys'; import { isHttpOrHttps } from '@clerk/shared/proxy'; import AES from 'crypto-js/aes'; +import encUtf8 from 'crypto-js/enc-utf8'; import hmacSHA1 from 'crypto-js/hmac-sha1'; import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; @@ -238,16 +239,17 @@ export function assertTokenSignature(token: string, key: string, signature?: str } /** - * Encrypt request data using a signing key. + * Encrypt request data using signing key. */ function encryptClerkRequestData(options: Partial): string { return AES.encrypt(JSON.stringify(options), SIGNING_KEY).toString(); } /** - * Decrypt request data using a signing key. + * Decrypt request data using signing key. */ +// TODO - Throw descriptive error when decryption fail export function decryptClerkRequestData(encryptedRequestData: string): Partial { const decryptedBytes = AES.decrypt(encryptedRequestData, SIGNING_KEY); - return JSON.parse(decryptedBytes.toString(CryptoJS.enc.Utf8)); + return JSON.parse(decryptedBytes.toString(encUtf8)); } From b0954f9d314a7f49b263e78397685a8e193e0335 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 7 Jun 2024 12:43:37 -0300 Subject: [PATCH 07/39] Handle error when signing key is invalid --- packages/nextjs/src/server/errors.ts | 2 ++ packages/nextjs/src/server/utils.ts | 14 +++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/nextjs/src/server/errors.ts b/packages/nextjs/src/server/errors.ts index 8e23e9d5dd4..8e73f23f5b2 100644 --- a/packages/nextjs/src/server/errors.ts +++ b/packages/nextjs/src/server/errors.ts @@ -97,3 +97,5 @@ For additional information about middleware, please visit https://clerk.com/docs }; export const authSignatureInvalid = `Clerk: Unable to verify request, this usually means the Clerk middleware did not run. Ensure Clerk's middleware is properly integrated and matches the current route. For more information, see: https://clerk.com/docs/nextjs/middleware. (code=auth_signature_invalid)`; + +export const signingKeyInvalid = `Clerk: Unable to decrypt request data, this usually means the signing key is invalid. Ensure the signing key is properly set. (code=signing_key_invalid)`; diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index 54f1fe65498..d288f635fc1 100644 --- a/packages/nextjs/src/server/utils.ts +++ b/packages/nextjs/src/server/utils.ts @@ -11,7 +11,7 @@ import { NextResponse } from 'next/server'; import { constants as nextConstants } from '../constants'; import { DOMAIN, IS_SATELLITE, PROXY_URL, SECRET_KEY, SIGN_IN_URL, SIGNING_KEY } from './constants'; -import { authSignatureInvalid, missingDomainAndProxy, missingSignInUrlInDev } from './errors'; +import { authSignatureInvalid, missingDomainAndProxy, missingSignInUrlInDev, signingKeyInvalid } from './errors'; import type { RequestLike } from './types'; export function setCustomAttributeOnRequest(req: RequestLike, key: string, value: string): void { @@ -241,15 +241,19 @@ export function assertTokenSignature(token: string, key: string, signature?: str /** * Encrypt request data using signing key. */ -function encryptClerkRequestData(options: Partial): string { +export function encryptClerkRequestData(options: Partial): string { return AES.encrypt(JSON.stringify(options), SIGNING_KEY).toString(); } /** * Decrypt request data using signing key. */ -// TODO - Throw descriptive error when decryption fail export function decryptClerkRequestData(encryptedRequestData: string): Partial { - const decryptedBytes = AES.decrypt(encryptedRequestData, SIGNING_KEY); - return JSON.parse(decryptedBytes.toString(encUtf8)); + try { + const decryptedBytes = AES.decrypt(encryptedRequestData, SIGNING_KEY); + const encoded = decryptedBytes.toString(encUtf8); + return JSON.parse(encoded); + } catch (err) { + throw new Error(signingKeyInvalid); + } } From 614fac55788b8534b4dce07b9bae5d89265ec79e Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 7 Jun 2024 12:48:34 -0300 Subject: [PATCH 08/39] Rename signing key to encryption key --- packages/nextjs/src/server/clerkMiddleware.ts | 4 ++-- packages/nextjs/src/server/constants.ts | 2 +- packages/nextjs/src/server/utils.ts | 6 +++--- packages/shared/src/error.ts | 9 +++++---- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index 27245d81f54..6789a88cacb 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -13,7 +13,7 @@ import { NextResponse } from 'next/server'; import { isRedirect, serverRedirectWithAuth, setHeader } from '../utils'; import { withLogger } from '../utils/debugLogger'; import { clerkClient } from './clerkClient'; -import { PUBLISHABLE_KEY, SECRET_KEY, SIGN_IN_URL, SIGN_UP_URL, SIGNING_KEY } from './constants'; +import { ENCRYPTION_KEY, PUBLISHABLE_KEY, SECRET_KEY, SIGN_IN_URL, SIGN_UP_URL } from './constants'; import { errorThrower } from './errorThrower'; import type { AuthProtect } from './protect'; import { createProtect } from './protect'; @@ -78,7 +78,7 @@ export const clerkMiddleware: ClerkMiddleware = withLogger('clerkMiddleware', lo const hasProvidedRuntimeOptions = params.secretKey || params.signInUrl || params.signInUrl; if (hasProvidedRuntimeOptions) { - assertKey(SIGNING_KEY, () => errorThrower.throwMissingSigningKeyError()); + assertKey(ENCRYPTION_KEY, () => errorThrower.throwMissingEncryptionKeyError()); } const publishableKey = assertKey(params.publishableKey || PUBLISHABLE_KEY, () => diff --git a/packages/nextjs/src/server/constants.ts b/packages/nextjs/src/server/constants.ts index 0ebbb4774e4..36ec73b95ed 100644 --- a/packages/nextjs/src/server/constants.ts +++ b/packages/nextjs/src/server/constants.ts @@ -6,7 +6,7 @@ export const CLERK_JS_URL = process.env.NEXT_PUBLIC_CLERK_JS_URL || ''; export const API_VERSION = process.env.CLERK_API_VERSION || 'v1'; export const SECRET_KEY = process.env.CLERK_SECRET_KEY || ''; export const PUBLISHABLE_KEY = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY || ''; -export const SIGNING_KEY = process.env.CLERK_SIGNING_KEY || ''; +export const ENCRYPTION_KEY = process.env.CLERK_ENCRYPTION_KEY || ''; export const API_URL = process.env.CLERK_API_URL || apiUrlFromPublishableKey(PUBLISHABLE_KEY); export const DOMAIN = process.env.NEXT_PUBLIC_CLERK_DOMAIN || ''; export const PROXY_URL = process.env.NEXT_PUBLIC_CLERK_PROXY_URL || ''; diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index d288f635fc1..ebfec8ed8d9 100644 --- a/packages/nextjs/src/server/utils.ts +++ b/packages/nextjs/src/server/utils.ts @@ -10,7 +10,7 @@ import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; import { constants as nextConstants } from '../constants'; -import { DOMAIN, IS_SATELLITE, PROXY_URL, SECRET_KEY, SIGN_IN_URL, SIGNING_KEY } from './constants'; +import { DOMAIN, ENCRYPTION_KEY, IS_SATELLITE, PROXY_URL, SECRET_KEY, SIGN_IN_URL } from './constants'; import { authSignatureInvalid, missingDomainAndProxy, missingSignInUrlInDev, signingKeyInvalid } from './errors'; import type { RequestLike } from './types'; @@ -242,7 +242,7 @@ export function assertTokenSignature(token: string, key: string, signature?: str * Encrypt request data using signing key. */ export function encryptClerkRequestData(options: Partial): string { - return AES.encrypt(JSON.stringify(options), SIGNING_KEY).toString(); + return AES.encrypt(JSON.stringify(options), ENCRYPTION_KEY).toString(); } /** @@ -250,7 +250,7 @@ export function encryptClerkRequestData(options: Partial { try { - const decryptedBytes = AES.decrypt(encryptedRequestData, SIGNING_KEY); + const decryptedBytes = AES.decrypt(encryptedRequestData, ENCRYPTION_KEY); const encoded = decryptedBytes.toString(encUtf8); return JSON.parse(encoded); } catch (err) { diff --git a/packages/shared/src/error.ts b/packages/shared/src/error.ts index 9acc8df7690..b4d6903d4d7 100644 --- a/packages/shared/src/error.ts +++ b/packages/shared/src/error.ts @@ -197,7 +197,8 @@ const DefaultMessages = Object.freeze({ MissingPublishableKeyErrorMessage: `Missing publishableKey. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.`, MissingSecretKeyErrorMessage: `Missing secretKey. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.`, MissingClerkProvider: `{{source}} can only be used within the component. Learn more: https://clerk.com/docs/components/clerk-provider`, - MissingSigningKeyErrorMessage: `Missing signingKey. When passing runtime options to clerkMiddleware, make sure to define CLERK_SIGNING_KEY.`, + // TODO - Update docs link + MissingEncryptionKeyErrorMessage: `Missing encryption key. Set CLERK_ENCRYPTION_KEY when passing options to clerkMiddleware for secure data transmission. Learn more: https://clerk.com/docs/clerkMiddleware.`, }); type MessageKeys = keyof typeof DefaultMessages; @@ -224,7 +225,7 @@ export interface ErrorThrower { throwMissingSecretKeyError(): never; - throwMissingSigningKeyError(): never; + throwMissingEncryptionKeyError(): never; throwMissingClerkProviderError(params: { source?: string }): never; @@ -284,8 +285,8 @@ export function buildErrorThrower({ packageName, customMessages }: ErrorThrowerO throw new Error(buildMessage(messages.MissingSecretKeyErrorMessage)); }, - throwMissingSigningKeyError(): never { - throw new Error(buildMessage(messages.MissingSigningKeyErrorMessage)); + throwMissingEncryptionKeyError(): never { + throw new Error(buildMessage(messages.MissingEncryptionKeyErrorMessage)); }, throwMissingClerkProviderError(params: { source?: string }): never { From 5e0eff1efde2494e6355c212fe651af270e0832b Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 7 Jun 2024 13:25:14 -0300 Subject: [PATCH 09/39] Add test to assert against propagation --- .../server/__tests__/clerkMiddleware.test.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts index b2ee35c6149..44522fa2a88 100644 --- a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts +++ b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts @@ -22,9 +22,12 @@ jest.mock('../clerkClient', () => { }); // used to assert the mock +import assert from 'assert'; + import { clerkClient } from '../clerkClient'; import { clerkMiddleware } from '../clerkMiddleware'; import { createRouteMatcher } from '../routeMatcher'; +import { decryptClerkRequestData } from '../utils'; /** * Disable console warnings about config matchers @@ -44,6 +47,7 @@ jest.mock('../constants', () => { return { PUBLISHABLE_KEY: 'pk_test_Y2xlcmsuaW5jbHVkZWQua2F0eWRpZC05Mi5sY2wuZGV2JA', SECRET_KEY: 'sk_test_xxxxxxxxxxxxxxxxxx', + ENCRYPTION_KEY: 'test', }; }); @@ -216,6 +220,20 @@ describe('clerkMiddleware(params)', () => { expect(signInResp?.headers.get('a-custom-header')).toEqual('1'); }); + it('propagates runtime options', async () => { + const secretKey = 'sk_test_xxxxxxxxxxxxxxxxxx'; + const resp = await clerkMiddleware({ secretKey })(mockRequest({ url: '/sign-in' }), {} as NextFetchEvent); + expect(resp?.status).toEqual(200); + + const requestData = resp?.headers.get('x-middleware-request-x-clerk-request-data'); + assert.ok(requestData); + + const decryptedData = decryptClerkRequestData(requestData); + + expect(resp?.headers.get('x-middleware-request-x-clerk-request-data')).toBeDefined(); + expect(decryptedData.secretKey).toBe(secretKey); + }); + describe('auth().redirectToSignIn()', () => { it('redirects to sign-in url when redirectToSignIn is called and the request is a page request', async () => { const req = mockRequest({ @@ -621,6 +639,4 @@ describe('Dev Browser JWT when redirecting to cross origin for page requests', f ); expect(clerkClient.authenticateRequest).toBeCalled(); }); - - // TODO - Add tests }); From 977bc38557ea65aeefcf1fc1e5caba20dca1f558 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 7 Jun 2024 14:22:23 -0300 Subject: [PATCH 10/39] Propagate `signInUrl` and `signUpUrl` --- packages/nextjs/src/app-router/server/auth.ts | 9 ++++++--- packages/nextjs/src/server/utils.ts | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/nextjs/src/app-router/server/auth.ts b/packages/nextjs/src/app-router/server/auth.ts index f0f66533ece..8ee57e84467 100644 --- a/packages/nextjs/src/app-router/server/auth.ts +++ b/packages/nextjs/src/app-router/server/auth.ts @@ -8,7 +8,7 @@ import { createGetAuth } from '../../server/createGetAuth'; import { authAuthHeaderMissing } from '../../server/errors'; import type { AuthProtect } from '../../server/protect'; import { createProtect } from '../../server/protect'; -import { getAuthKeyFromRequest } from '../../server/utils'; +import { decryptClerkRequestData, getAuthKeyFromRequest, getHeader } from '../../server/utils'; import { buildRequestLike } from './utils'; type Auth = AuthObject & { protect: AuthProtect; redirectToSignIn: RedirectFun> }; @@ -28,6 +28,9 @@ export const auth = (): Auth => { clerkRequest.clerkUrl.searchParams.get(constants.QueryParameters.DevBrowser) || clerkRequest.cookies.get(constants.Cookies.DevBrowser); + const requestData = getHeader(request, constants.Headers.ClerkRequestData); + const decryptedRequestData = requestData ? decryptClerkRequestData(requestData) : {}; + return createRedirect({ redirectAdapter: redirect, devBrowserToken: devBrowserToken, @@ -35,8 +38,8 @@ export const auth = (): Auth => { // TODO: Support runtime-value configuration of these options // via setting and reading headers from clerkMiddleware publishableKey: PUBLISHABLE_KEY, - signInUrl: SIGN_IN_URL, - signUpUrl: SIGN_UP_URL, + signInUrl: decryptedRequestData.signInUrl || SIGN_IN_URL, + signUpUrl: decryptedRequestData.signUpUrl || SIGN_UP_URL, }).redirectToSignIn({ returnBackUrl: opts.returnBackUrl === null ? '' : opts.returnBackUrl || clerkUrl?.toString(), }); diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index ebfec8ed8d9..1f2eabf3962 100644 --- a/packages/nextjs/src/server/utils.ts +++ b/packages/nextjs/src/server/utils.ts @@ -117,7 +117,7 @@ export function decorateRequest( requestState: RequestState, secretKey: string, ): Response { - const { reason, message, status, token } = requestState; + const { reason, message, status, token, signInUrl, signUpUrl } = requestState; // pass-through case, convert to next() if (!res) { res = NextResponse.next(); @@ -157,7 +157,7 @@ export function decorateRequest( [constants.Headers.AuthMessage]: message || '', [constants.Headers.AuthReason]: reason || '', [constants.Headers.ClerkUrl]: req.clerkUrl.toString(), - [constants.Headers.ClerkRequestData]: encryptClerkRequestData({ secretKey }), + [constants.Headers.ClerkRequestData]: encryptClerkRequestData({ secretKey, signInUrl, signUpUrl }), }); res.headers.set(nextConstants.Headers.NextRewrite, rewriteURL.href); } From 6111133876bd14e8730ce8e046c608f8d8245963 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 7 Jun 2024 15:20:51 -0300 Subject: [PATCH 11/39] Do not allow to pass `signInUrl` and `signUpUrl` from ClerkProvider --- packages/nextjs/src/app-router/server/ClerkProvider.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/src/app-router/server/ClerkProvider.tsx b/packages/nextjs/src/app-router/server/ClerkProvider.tsx index a3e10f0bbc0..26ccd07b8eb 100644 --- a/packages/nextjs/src/app-router/server/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/server/ClerkProvider.tsx @@ -6,7 +6,9 @@ import { mergeNextClerkPropsWithEnv } from '../../utils/mergeNextClerkPropsWithE import { ClientClerkProvider } from '../client/ClerkProvider'; import { initialState } from './auth'; -export function ClerkProvider(props: Without) { +export function ClerkProvider( + props: Without, +) { const { children, ...rest } = props; const state = initialState()?.__clerk_ssr_state as InitialState; From c0f37a81b6843379bd491720d6a908168d4d6234 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 12 Jun 2024 13:45:24 -0300 Subject: [PATCH 12/39] Restructure test block and add todo --- .../nextjs/src/server/__tests__/clerkMiddleware.test.ts | 5 +++-- packages/nextjs/src/server/createGetAuth.ts | 2 +- packages/nextjs/src/server/utils.ts | 8 +++++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts index 44522fa2a88..09b4cf7cd00 100644 --- a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts +++ b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts @@ -47,7 +47,6 @@ jest.mock('../constants', () => { return { PUBLISHABLE_KEY: 'pk_test_Y2xlcmsuaW5jbHVkZWQua2F0eWRpZC05Mi5sY2wuZGV2JA', SECRET_KEY: 'sk_test_xxxxxxxxxxxxxxxxxx', - ENCRYPTION_KEY: 'test', }; }); @@ -220,7 +219,7 @@ describe('clerkMiddleware(params)', () => { expect(signInResp?.headers.get('a-custom-header')).toEqual('1'); }); - it('propagates runtime options', async () => { + it('propagates middleware options to the next request', async () => { const secretKey = 'sk_test_xxxxxxxxxxxxxxxxxx'; const resp = await clerkMiddleware({ secretKey })(mockRequest({ url: '/sign-in' }), {} as NextFetchEvent); expect(resp?.status).toEqual(200); @@ -303,6 +302,8 @@ describe('clerkMiddleware(params)', () => { expect(new URL(resp!.headers.get('location')!).searchParams.get('redirect_url')).toBeNull(); expect(clerkClient.authenticateRequest).toBeCalled(); }); + + it.todo('redirects to sign-in url when propagated as an option'); }); describe('auth().protect()', () => { diff --git a/packages/nextjs/src/server/createGetAuth.ts b/packages/nextjs/src/server/createGetAuth.ts index d9e8191baad..8bf7d7cc4fc 100644 --- a/packages/nextjs/src/server/createGetAuth.ts +++ b/packages/nextjs/src/server/createGetAuth.ts @@ -36,7 +36,7 @@ export const createGetAuth = ({ } const requestData = getHeader(req, constants.Headers.ClerkRequestData); - const decryptedRequestData = requestData ? decryptClerkRequestData(requestData) : {}; + const decryptedRequestData = decryptClerkRequestData(requestData); const options = { authStatus, diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index 1f2eabf3962..dfd8ee0c172 100644 --- a/packages/nextjs/src/server/utils.ts +++ b/packages/nextjs/src/server/utils.ts @@ -248,7 +248,13 @@ export function encryptClerkRequestData(options: Partial { +export function decryptClerkRequestData( + encryptedRequestData?: string | undefined | null, +): Partial { + if (!encryptedRequestData) { + return {}; + } + try { const decryptedBytes = AES.decrypt(encryptedRequestData, ENCRYPTION_KEY); const encoded = decryptedBytes.toString(encUtf8); From bc510b84ee2b1e13d030fa801887c1cd8f0db3d0 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 12 Jun 2024 14:44:11 -0300 Subject: [PATCH 13/39] Rollback props changes on `ClerkProvider` --- packages/nextjs/src/app-router/server/ClerkProvider.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/nextjs/src/app-router/server/ClerkProvider.tsx b/packages/nextjs/src/app-router/server/ClerkProvider.tsx index 26ccd07b8eb..a3e10f0bbc0 100644 --- a/packages/nextjs/src/app-router/server/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/server/ClerkProvider.tsx @@ -6,9 +6,7 @@ import { mergeNextClerkPropsWithEnv } from '../../utils/mergeNextClerkPropsWithE import { ClientClerkProvider } from '../client/ClerkProvider'; import { initialState } from './auth'; -export function ClerkProvider( - props: Without, -) { +export function ClerkProvider(props: Without) { const { children, ...rest } = props; const state = initialState()?.__clerk_ssr_state as InitialState; From d02913edf9584ef4e7e0d28680d2e7c02a8f8067 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 12 Jun 2024 15:29:09 -0300 Subject: [PATCH 14/39] Introduce fallback for encryption key --- packages/nextjs/src/app-router/server/auth.ts | 4 +-- packages/nextjs/src/server/clerkMiddleware.ts | 7 +---- packages/nextjs/src/server/errors.ts | 3 ++- packages/nextjs/src/server/utils.ts | 26 ++++++++++++++----- packages/shared/src/error.ts | 3 +-- 5 files changed, 25 insertions(+), 18 deletions(-) diff --git a/packages/nextjs/src/app-router/server/auth.ts b/packages/nextjs/src/app-router/server/auth.ts index 8ee57e84467..254540d08f2 100644 --- a/packages/nextjs/src/app-router/server/auth.ts +++ b/packages/nextjs/src/app-router/server/auth.ts @@ -28,8 +28,8 @@ export const auth = (): Auth => { clerkRequest.clerkUrl.searchParams.get(constants.QueryParameters.DevBrowser) || clerkRequest.cookies.get(constants.Cookies.DevBrowser); - const requestData = getHeader(request, constants.Headers.ClerkRequestData); - const decryptedRequestData = requestData ? decryptClerkRequestData(requestData) : {}; + const encryptedRequestData = getHeader(request, constants.Headers.ClerkRequestData); + const decryptedRequestData = decryptClerkRequestData(encryptedRequestData); return createRedirect({ redirectAdapter: redirect, diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index 6789a88cacb..7c11f520387 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -13,7 +13,7 @@ import { NextResponse } from 'next/server'; import { isRedirect, serverRedirectWithAuth, setHeader } from '../utils'; import { withLogger } from '../utils/debugLogger'; import { clerkClient } from './clerkClient'; -import { ENCRYPTION_KEY, PUBLISHABLE_KEY, SECRET_KEY, SIGN_IN_URL, SIGN_UP_URL } from './constants'; +import { PUBLISHABLE_KEY, SECRET_KEY, SIGN_IN_URL, SIGN_UP_URL } from './constants'; import { errorThrower } from './errorThrower'; import type { AuthProtect } from './protect'; import { createProtect } from './protect'; @@ -76,11 +76,6 @@ export const clerkMiddleware: ClerkMiddleware = withLogger('clerkMiddleware', lo logger.enable(); } - const hasProvidedRuntimeOptions = params.secretKey || params.signInUrl || params.signInUrl; - if (hasProvidedRuntimeOptions) { - assertKey(ENCRYPTION_KEY, () => errorThrower.throwMissingEncryptionKeyError()); - } - const publishableKey = assertKey(params.publishableKey || PUBLISHABLE_KEY, () => errorThrower.throwMissingPublishableKeyError(), ); diff --git a/packages/nextjs/src/server/errors.ts b/packages/nextjs/src/server/errors.ts index 8e73f23f5b2..d788c2c1b03 100644 --- a/packages/nextjs/src/server/errors.ts +++ b/packages/nextjs/src/server/errors.ts @@ -98,4 +98,5 @@ For additional information about middleware, please visit https://clerk.com/docs export const authSignatureInvalid = `Clerk: Unable to verify request, this usually means the Clerk middleware did not run. Ensure Clerk's middleware is properly integrated and matches the current route. For more information, see: https://clerk.com/docs/nextjs/middleware. (code=auth_signature_invalid)`; -export const signingKeyInvalid = `Clerk: Unable to decrypt request data, this usually means the signing key is invalid. Ensure the signing key is properly set. (code=signing_key_invalid)`; +export const encryptionKeyInvalid = `Clerk: Unable to decrypt request data, this usually means the encryption key is invalid. Ensure the encryption key is properly set. (code=signing_key_invalid) +For additional information, please visit https://clerk.com/docs/references/nextjs/clerk-middleware#clerk-middleware-options.`; diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index dfd8ee0c172..76504a40a98 100644 --- a/packages/nextjs/src/server/utils.ts +++ b/packages/nextjs/src/server/utils.ts @@ -11,7 +11,8 @@ import { NextResponse } from 'next/server'; import { constants as nextConstants } from '../constants'; import { DOMAIN, ENCRYPTION_KEY, IS_SATELLITE, PROXY_URL, SECRET_KEY, SIGN_IN_URL } from './constants'; -import { authSignatureInvalid, missingDomainAndProxy, missingSignInUrlInDev, signingKeyInvalid } from './errors'; +import { authSignatureInvalid, encryptionKeyInvalid, missingDomainAndProxy, missingSignInUrlInDev } from './errors'; +import { errorThrower } from './errorThrower'; import type { RequestLike } from './types'; export function setCustomAttributeOnRequest(req: RequestLike, key: string, value: string): void { @@ -239,14 +240,25 @@ export function assertTokenSignature(token: string, key: string, signature?: str } /** - * Encrypt request data using signing key. - */ + * Encrypt request data propagated between server requests. + * @internal + **/ export function encryptClerkRequestData(options: Partial): string { - return AES.encrypt(JSON.stringify(options), ENCRYPTION_KEY).toString(); + /** + * If a secretKey is provided in the options, ENCRYPTION_KEY is required. + * If no secretKey is provided, ENCRYPTION_KEY falls back to SECRET_KEY. + * This setup ensures backward compatibility and simplifies use cases where sensitive options like `secretKey` aren't provided. + */ + const key = options.secretKey + ? assertKey(ENCRYPTION_KEY, () => errorThrower.throwMissingEncryptionKeyError()) + : ENCRYPTION_KEY ?? assertKey(SECRET_KEY, () => errorThrower.throwMissingSecretKeyError()); + + return AES.encrypt(JSON.stringify(options), key).toString(); } /** - * Decrypt request data using signing key. + * Decrypt request data propagated between server requests. + * @internal */ export function decryptClerkRequestData( encryptedRequestData?: string | undefined | null, @@ -256,10 +268,10 @@ export function decryptClerkRequestData( } try { - const decryptedBytes = AES.decrypt(encryptedRequestData, ENCRYPTION_KEY); + const decryptedBytes = AES.decrypt(encryptedRequestData, ENCRYPTION_KEY ?? SECRET_KEY); const encoded = decryptedBytes.toString(encUtf8); return JSON.parse(encoded); } catch (err) { - throw new Error(signingKeyInvalid); + throw new Error(encryptionKeyInvalid); } } diff --git a/packages/shared/src/error.ts b/packages/shared/src/error.ts index b4d6903d4d7..09c57c9d644 100644 --- a/packages/shared/src/error.ts +++ b/packages/shared/src/error.ts @@ -197,8 +197,7 @@ const DefaultMessages = Object.freeze({ MissingPublishableKeyErrorMessage: `Missing publishableKey. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.`, MissingSecretKeyErrorMessage: `Missing secretKey. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.`, MissingClerkProvider: `{{source}} can only be used within the component. Learn more: https://clerk.com/docs/components/clerk-provider`, - // TODO - Update docs link - MissingEncryptionKeyErrorMessage: `Missing encryption key. Set CLERK_ENCRYPTION_KEY when passing options to clerkMiddleware for secure data transmission. Learn more: https://clerk.com/docs/clerkMiddleware.`, + MissingEncryptionKeyErrorMessage: `Missing encryption key. Set CLERK_ENCRYPTION_KEY when passing sensitive options to clerkMiddleware for secure data transmission. Learn more: https://clerk.com/docs/references/nextjs/clerk-middleware#clerk-middleware-options.`, }); type MessageKeys = keyof typeof DefaultMessages; From 3e5758b060842ce0c06cbb934e64a3809de71b04 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 12 Jun 2024 17:33:22 -0300 Subject: [PATCH 15/39] Implement test case for `signInUrl` option --- .../server/__tests__/clerkMiddleware.test.ts | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts index 09b4cf7cd00..0a23fe4240e 100644 --- a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts +++ b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts @@ -47,6 +47,7 @@ jest.mock('../constants', () => { return { PUBLISHABLE_KEY: 'pk_test_Y2xlcmsuaW5jbHVkZWQua2F0eWRpZC05Mi5sY2wuZGV2JA', SECRET_KEY: 'sk_test_xxxxxxxxxxxxxxxxxx', + ENCRYPTION_KEY: 'encryption-key', }; }); @@ -303,7 +304,24 @@ describe('clerkMiddleware(params)', () => { expect(clerkClient.authenticateRequest).toBeCalled(); }); - it.todo('redirects to sign-in url when propagated as an option'); + it('redirects to sign-in url when propagated as an option', async () => { + const req = mockRequest({ + url: '/protected', + }); + + const resp = await clerkMiddleware( + auth => { + auth().redirectToSignIn(); + }, + { + signInUrl: '/foo', + }, + )(req, {} as NextFetchEvent); + + expect(resp?.status).toEqual(307); + expect(resp?.headers.get('location')).toContain('foo'); + expect(clerkClient.authenticateRequest).toBeCalled(); + }); }); describe('auth().protect()', () => { From 919ec6e3304eb16fbc8f75c52683edb14881bb8a Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 13 Jun 2024 15:30:24 -0300 Subject: [PATCH 16/39] Add `CLERK_ENCRYPTION_KEY` to integration tests --- integration/constants.ts | 4 ++++ integration/presets/envs.ts | 21 ++++++++++++++------- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/integration/constants.ts b/integration/constants.ts index 0154f72f82f..38113a45320 100644 --- a/integration/constants.ts +++ b/integration/constants.ts @@ -71,4 +71,8 @@ export const constants = { * PK and SK pairs from the env to use for integration tests. */ INTEGRATION_INSTANCE_KEYS: process.env.INTEGRATION_INSTANCE_KEYS, + /** + * Key used to encrypt request data for Next.js middleware options propagation. + */ + CLERK_ENCRYPTION_KEY: process.env.CLERK_ENCRYPTION_KEY, } as const; diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts index 55b086554b3..5458e2ddcdc 100644 --- a/integration/presets/envs.ts +++ b/integration/presets/envs.ts @@ -29,7 +29,8 @@ const withEmailCodes = environmentConfig() .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['with-email-codes'].pk) .setEnvVariable('public', 'CLERK_SIGN_IN_URL', '/sign-in') .setEnvVariable('public', 'CLERK_SIGN_UP_URL', '/sign-up') - .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js'); + .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') + .setEnvVariable('public', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); const withEmailLinks = environmentConfig() .setId('withEmailLinks') @@ -38,7 +39,8 @@ const withEmailLinks = environmentConfig() .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['with-email-links'].pk) .setEnvVariable('public', 'CLERK_SIGN_IN_URL', '/sign-in') .setEnvVariable('public', 'CLERK_SIGN_UP_URL', '/sign-up') - .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js'); + .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') + .setEnvVariable('public', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); const withCustomRoles = environmentConfig() .setId('withCustomRoles') @@ -49,7 +51,8 @@ const withCustomRoles = environmentConfig() .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['with-custom-roles'].pk) .setEnvVariable('public', 'CLERK_SIGN_IN_URL', '/sign-in') .setEnvVariable('public', 'CLERK_SIGN_UP_URL', '/sign-up') - .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js'); + .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') + .setEnvVariable('public', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); const withEmailCodesQuickstart = withEmailCodes .removeEnvVariable('public', 'CLERK_SIGN_IN_URL') @@ -60,26 +63,30 @@ const withAPCore1ClerkLatest = environmentConfig() .setEnvVariable('public', 'CLERK_TELEMETRY_DISABLED', true) .setEnvVariable('private', 'CLERK_SECRET_KEY', envKeys['with-email-codes'].sk) .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['with-email-codes'].pk) - .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js'); + .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') + .setEnvVariable('public', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); const withAPCore1ClerkV4 = environmentConfig() .setId('withAPCore1ClerkV4') .setEnvVariable('public', 'CLERK_TELEMETRY_DISABLED', true) .setEnvVariable('private', 'CLERK_SECRET_KEY', envKeys['with-email-codes'].sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['with-email-codes'].pk); + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['with-email-codes'].pk) + .setEnvVariable('public', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); const withAPCore2ClerkLatest = environmentConfig() .setId('withAPCore2ClerkLatest') .setEnvVariable('public', 'CLERK_TELEMETRY_DISABLED', true) .setEnvVariable('private', 'CLERK_SECRET_KEY', envKeys['core-2-all-enabled'].sk) .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['core-2-all-enabled'].pk) - .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js'); + .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') + .setEnvVariable('public', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); const withAPCore2ClerkV4 = environmentConfig() .setId('withAPCore2ClerkV4') .setEnvVariable('public', 'CLERK_TELEMETRY_DISABLED', true) .setEnvVariable('private', 'CLERK_SECRET_KEY', envKeys['core-2-all-enabled'].sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['core-2-all-enabled'].pk); + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['core-2-all-enabled'].pk) + .setEnvVariable('public', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); export const envs = { withEmailCodes, From 73942bd4f89e214f4448b021c5131fdfd1c0ef3d Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 13 Jun 2024 15:53:17 -0300 Subject: [PATCH 17/39] Access redirect URLs from middleware options --- integration/presets/envs.ts | 14 ++++----- .../server/__tests__/clerkMiddleware.test.ts | 29 +++++-------------- packages/nextjs/src/server/authMiddleware.ts | 7 ++++- packages/nextjs/src/server/clerkMiddleware.ts | 2 +- packages/nextjs/src/server/createGetAuth.ts | 4 +-- packages/nextjs/src/server/errors.ts | 2 +- packages/nextjs/src/server/utils.ts | 9 ++++-- 7 files changed, 31 insertions(+), 36 deletions(-) diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts index 5458e2ddcdc..cbd98ae0eb4 100644 --- a/integration/presets/envs.ts +++ b/integration/presets/envs.ts @@ -30,7 +30,7 @@ const withEmailCodes = environmentConfig() .setEnvVariable('public', 'CLERK_SIGN_IN_URL', '/sign-in') .setEnvVariable('public', 'CLERK_SIGN_UP_URL', '/sign-up') .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') - .setEnvVariable('public', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); + .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); const withEmailLinks = environmentConfig() .setId('withEmailLinks') @@ -40,7 +40,7 @@ const withEmailLinks = environmentConfig() .setEnvVariable('public', 'CLERK_SIGN_IN_URL', '/sign-in') .setEnvVariable('public', 'CLERK_SIGN_UP_URL', '/sign-up') .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') - .setEnvVariable('public', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); + .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); const withCustomRoles = environmentConfig() .setId('withCustomRoles') @@ -52,7 +52,7 @@ const withCustomRoles = environmentConfig() .setEnvVariable('public', 'CLERK_SIGN_IN_URL', '/sign-in') .setEnvVariable('public', 'CLERK_SIGN_UP_URL', '/sign-up') .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') - .setEnvVariable('public', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); + .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); const withEmailCodesQuickstart = withEmailCodes .removeEnvVariable('public', 'CLERK_SIGN_IN_URL') @@ -64,14 +64,14 @@ const withAPCore1ClerkLatest = environmentConfig() .setEnvVariable('private', 'CLERK_SECRET_KEY', envKeys['with-email-codes'].sk) .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['with-email-codes'].pk) .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') - .setEnvVariable('public', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); + .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); const withAPCore1ClerkV4 = environmentConfig() .setId('withAPCore1ClerkV4') .setEnvVariable('public', 'CLERK_TELEMETRY_DISABLED', true) .setEnvVariable('private', 'CLERK_SECRET_KEY', envKeys['with-email-codes'].sk) .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['with-email-codes'].pk) - .setEnvVariable('public', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); + .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); const withAPCore2ClerkLatest = environmentConfig() .setId('withAPCore2ClerkLatest') @@ -79,14 +79,14 @@ const withAPCore2ClerkLatest = environmentConfig() .setEnvVariable('private', 'CLERK_SECRET_KEY', envKeys['core-2-all-enabled'].sk) .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['core-2-all-enabled'].pk) .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') - .setEnvVariable('public', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); + .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); const withAPCore2ClerkV4 = environmentConfig() .setId('withAPCore2ClerkV4') .setEnvVariable('public', 'CLERK_TELEMETRY_DISABLED', true) .setEnvVariable('private', 'CLERK_SECRET_KEY', envKeys['core-2-all-enabled'].sk) .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['core-2-all-enabled'].pk) - .setEnvVariable('public', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); + .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); export const envs = { withEmailCodes, diff --git a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts index 0a23fe4240e..f70e3ebb8ca 100644 --- a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts +++ b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts @@ -221,8 +221,12 @@ describe('clerkMiddleware(params)', () => { }); it('propagates middleware options to the next request', async () => { - const secretKey = 'sk_test_xxxxxxxxxxxxxxxxxx'; - const resp = await clerkMiddleware({ secretKey })(mockRequest({ url: '/sign-in' }), {} as NextFetchEvent); + const options = { + secretKey: 'sk_test_xxxxxxxxxxxxxxxxxx', + signInUrl: '/foo', + signUpUrl: '/bar', + }; + const resp = await clerkMiddleware(options)(mockRequest({ url: '/sign-in' }), {} as NextFetchEvent); expect(resp?.status).toEqual(200); const requestData = resp?.headers.get('x-middleware-request-x-clerk-request-data'); @@ -231,7 +235,7 @@ describe('clerkMiddleware(params)', () => { const decryptedData = decryptClerkRequestData(requestData); expect(resp?.headers.get('x-middleware-request-x-clerk-request-data')).toBeDefined(); - expect(decryptedData.secretKey).toBe(secretKey); + expect(decryptedData).toEqual(options); }); describe('auth().redirectToSignIn()', () => { @@ -303,25 +307,6 @@ describe('clerkMiddleware(params)', () => { expect(new URL(resp!.headers.get('location')!).searchParams.get('redirect_url')).toBeNull(); expect(clerkClient.authenticateRequest).toBeCalled(); }); - - it('redirects to sign-in url when propagated as an option', async () => { - const req = mockRequest({ - url: '/protected', - }); - - const resp = await clerkMiddleware( - auth => { - auth().redirectToSignIn(); - }, - { - signInUrl: '/foo', - }, - )(req, {} as NextFetchEvent); - - expect(resp?.status).toEqual(307); - expect(resp?.headers.get('location')).toContain('foo'); - expect(clerkClient.authenticateRequest).toBeCalled(); - }); }); describe('auth().protect()', () => { diff --git a/packages/nextjs/src/server/authMiddleware.ts b/packages/nextjs/src/server/authMiddleware.ts index 1b560dcc0c3..f878d6c31c4 100644 --- a/packages/nextjs/src/server/authMiddleware.ts +++ b/packages/nextjs/src/server/authMiddleware.ts @@ -219,7 +219,12 @@ const authMiddleware: AuthMiddleware = (...args: unknown[]) => { logger.debug(`Added ${constants.Headers.EnableDebug} on request`); } - const result = decorateRequest(clerkRequest, finalRes, requestState, secretKey) || NextResponse.next(); + const result = + decorateRequest(clerkRequest, finalRes, requestState, { + secretKey, + signInUrl: params.signInUrl, + signUpUrl: params.signUpUrl, + }) || NextResponse.next(); if (requestState.headers) { requestState.headers.forEach((value, key) => { diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index 7c11f520387..9509a73879e 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -153,7 +153,7 @@ export const clerkMiddleware: ClerkMiddleware = withLogger('clerkMiddleware', lo setRequestHeadersOnNextResponse(handlerResult, clerkRequest, { [constants.Headers.EnableDebug]: 'true' }); } - decorateRequest(clerkRequest, handlerResult, requestState, options.secretKey); + decorateRequest(clerkRequest, handlerResult, requestState, { secretKey, signInUrl, signUpUrl }); return handlerResult; }; diff --git a/packages/nextjs/src/server/createGetAuth.ts b/packages/nextjs/src/server/createGetAuth.ts index 8bf7d7cc4fc..03fe44252f4 100644 --- a/packages/nextjs/src/server/createGetAuth.ts +++ b/packages/nextjs/src/server/createGetAuth.ts @@ -35,8 +35,8 @@ export const createGetAuth = ({ throw new Error(noAuthStatusMessage); } - const requestData = getHeader(req, constants.Headers.ClerkRequestData); - const decryptedRequestData = decryptClerkRequestData(requestData); + const encryptedRequestData = getHeader(req, constants.Headers.ClerkRequestData); + const decryptedRequestData = decryptClerkRequestData(encryptedRequestData); const options = { authStatus, diff --git a/packages/nextjs/src/server/errors.ts b/packages/nextjs/src/server/errors.ts index d788c2c1b03..b9003c89ffe 100644 --- a/packages/nextjs/src/server/errors.ts +++ b/packages/nextjs/src/server/errors.ts @@ -98,5 +98,5 @@ For additional information about middleware, please visit https://clerk.com/docs export const authSignatureInvalid = `Clerk: Unable to verify request, this usually means the Clerk middleware did not run. Ensure Clerk's middleware is properly integrated and matches the current route. For more information, see: https://clerk.com/docs/nextjs/middleware. (code=auth_signature_invalid)`; -export const encryptionKeyInvalid = `Clerk: Unable to decrypt request data, this usually means the encryption key is invalid. Ensure the encryption key is properly set. (code=signing_key_invalid) +export const encryptionKeyInvalid = `Clerk: Unable to decrypt request data, this usually means the encryption key is invalid. Ensure the encryption key is properly set. (code=encryption_key_invalid) For additional information, please visit https://clerk.com/docs/references/nextjs/clerk-middleware#clerk-middleware-options.`; diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index 76504a40a98..06be66e3add 100644 --- a/packages/nextjs/src/server/utils.ts +++ b/packages/nextjs/src/server/utils.ts @@ -116,9 +116,14 @@ export function decorateRequest( req: ClerkRequest, res: Response, requestState: RequestState, - secretKey: string, + { + secretKey, + signInUrl, + signUpUrl, + }: Pick & + Required>, ): Response { - const { reason, message, status, token, signInUrl, signUpUrl } = requestState; + const { reason, message, status, token } = requestState; // pass-through case, convert to next() if (!res) { res = NextResponse.next(); From 853d90ed47b129804213d3598fe5b5bd86655a10 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 13 Jun 2024 17:00:05 -0300 Subject: [PATCH 18/39] Add changeset --- .changeset/young-pigs-live.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/young-pigs-live.md diff --git a/.changeset/young-pigs-live.md b/.changeset/young-pigs-live.md new file mode 100644 index 00000000000..41b50d8ec5e --- /dev/null +++ b/.changeset/young-pigs-live.md @@ -0,0 +1,12 @@ +--- +'@clerk/backend': minor +'@clerk/nextjs': minor +'@clerk/shared': minor +--- + +Propagate Next.js middleware options to the application server, solving the following issues: +- `auth` couldn't assert the session token signature due to not having access to `secretKey` passed as an option. +- `auth.redirectToSignIn` doesn't redirect to `signInUrl` provided as option. + +`CLERK_ENCRYPTION_KEY` must be defined when providing `secretKey` as an option, in order to securely propagated this value +between servers. For more information regarding options propagation, refer to (TODO - Add docs URL here) From c1856cb02266ea0ae4b9286e65f9acb5824c23e0 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 14 Jun 2024 11:30:24 -0300 Subject: [PATCH 19/39] Mock `ENCRYPTION_KEY` on `authMiddleware` unit tests --- packages/nextjs/src/server/__tests__/authMiddleware.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nextjs/src/server/__tests__/authMiddleware.test.ts b/packages/nextjs/src/server/__tests__/authMiddleware.test.ts index 59457f284fe..31deb0b2be6 100644 --- a/packages/nextjs/src/server/__tests__/authMiddleware.test.ts +++ b/packages/nextjs/src/server/__tests__/authMiddleware.test.ts @@ -16,6 +16,7 @@ jest.mock('../constants', () => { return { PUBLISHABLE_KEY: 'pk_test_Y2xlcmsuaW5jbHVkZWQua2F0eWRpZC05Mi5sY2wuZGV2JA', SECRET_KEY: 'sk_test_xxxxxxxxxxxxxxxxxx', + ENCRYPTION_KEY: 'encryption-key', }; }); From 0be0727bb0d13f3f54f736cc4cc32e877d2d290d Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 14 Jun 2024 11:43:14 -0300 Subject: [PATCH 20/39] Update snapshot for request headers --- .../fastify/src/__tests__/__snapshots__/constants.test.ts.snap | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/fastify/src/__tests__/__snapshots__/constants.test.ts.snap b/packages/fastify/src/__tests__/__snapshots__/constants.test.ts.snap index 023875d97ba..a5daa663617 100644 --- a/packages/fastify/src/__tests__/__snapshots__/constants.test.ts.snap +++ b/packages/fastify/src/__tests__/__snapshots__/constants.test.ts.snap @@ -19,6 +19,7 @@ exports[`constants from environment variables 1`] = ` "AuthToken": "x-clerk-auth-token", "Authorization": "authorization", "ClerkRedirectTo": "x-clerk-redirect-to", + "ClerkRequestData": "x-clerk-request-data", "ClerkUrl": "x-clerk-clerk-url", "CloudFrontForwardedProto": "cloudfront-forwarded-proto", "ContentType": "content-type", From f520025019b76493d5fd76d4fcd655d562b4841b Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 14 Jun 2024 15:37:16 -0300 Subject: [PATCH 21/39] Warn when encryption key is missing instead of throwing --- packages/nextjs/src/server/utils.ts | 22 ++++++++++++++-------- packages/shared/src/error.ts | 1 - 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index 06be66e3add..299a40c0597 100644 --- a/packages/nextjs/src/server/utils.ts +++ b/packages/nextjs/src/server/utils.ts @@ -1,5 +1,6 @@ import type { AuthenticateRequestOptions, ClerkRequest, RequestState } from '@clerk/backend/internal'; import { constants } from '@clerk/backend/internal'; +import { logger } from '@clerk/shared'; import { handleValueOrFn } from '@clerk/shared/handleValueOrFn'; import { isDevelopmentFromSecretKey } from '@clerk/shared/keys'; import { isHttpOrHttps } from '@clerk/shared/proxy'; @@ -248,17 +249,22 @@ export function assertTokenSignature(token: string, key: string, signature?: str * Encrypt request data propagated between server requests. * @internal **/ -export function encryptClerkRequestData(options: Partial): string { +export function encryptClerkRequestData(options: Partial) { /** - * If a secretKey is provided in the options, ENCRYPTION_KEY is required. - * If no secretKey is provided, ENCRYPTION_KEY falls back to SECRET_KEY. - * This setup ensures backward compatibility and simplifies use cases where sensitive options like `secretKey` aren't provided. + * Warns if encryption key is missing when secret key is provided to prevent breaking changes. + * Prepares users for a potential escalation to an error in a future major version. */ - const key = options.secretKey - ? assertKey(ENCRYPTION_KEY, () => errorThrower.throwMissingEncryptionKeyError()) - : ENCRYPTION_KEY ?? assertKey(SECRET_KEY, () => errorThrower.throwMissingSecretKeyError()); + if (options.secretKey && !ENCRYPTION_KEY) { + logger.warnOnce( + 'Clerk: Missing `CLERK_ENCRYPTION_KEY`. Required for propagating `secretKey` middleware option. See docs: https://clerk.com/docs/references/nextjs/clerk-middleware#server-side-options-propagation', + ); + return; + } - return AES.encrypt(JSON.stringify(options), key).toString(); + return AES.encrypt( + JSON.stringify(options), + ENCRYPTION_KEY ?? assertKey(SECRET_KEY, () => errorThrower.throwMissingSecretKeyError()), + ).toString(); } /** diff --git a/packages/shared/src/error.ts b/packages/shared/src/error.ts index 09c57c9d644..88bcd1013a3 100644 --- a/packages/shared/src/error.ts +++ b/packages/shared/src/error.ts @@ -197,7 +197,6 @@ const DefaultMessages = Object.freeze({ MissingPublishableKeyErrorMessage: `Missing publishableKey. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.`, MissingSecretKeyErrorMessage: `Missing secretKey. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.`, MissingClerkProvider: `{{source}} can only be used within the component. Learn more: https://clerk.com/docs/components/clerk-provider`, - MissingEncryptionKeyErrorMessage: `Missing encryption key. Set CLERK_ENCRYPTION_KEY when passing sensitive options to clerkMiddleware for secure data transmission. Learn more: https://clerk.com/docs/references/nextjs/clerk-middleware#clerk-middleware-options.`, }); type MessageKeys = keyof typeof DefaultMessages; From 8059461c437164de356ee47bb56e06dc95e1088c Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 14 Jun 2024 15:41:19 -0300 Subject: [PATCH 22/39] Reference to docs on error message and changeset --- .changeset/young-pigs-live.md | 2 +- packages/nextjs/src/server/errors.ts | 2 +- packages/nextjs/src/server/utils.ts | 6 ++++-- packages/shared/src/error.ts | 6 ------ 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/.changeset/young-pigs-live.md b/.changeset/young-pigs-live.md index 41b50d8ec5e..af09ee32332 100644 --- a/.changeset/young-pigs-live.md +++ b/.changeset/young-pigs-live.md @@ -9,4 +9,4 @@ Propagate Next.js middleware options to the application server, solving the foll - `auth.redirectToSignIn` doesn't redirect to `signInUrl` provided as option. `CLERK_ENCRYPTION_KEY` must be defined when providing `secretKey` as an option, in order to securely propagated this value -between servers. For more information regarding options propagation, refer to (TODO - Add docs URL here) +between servers. For more information regarding options propagation, refer to https://clerk.com/docs/references/nextjs/clerk-middleware#server-side-options-propagation diff --git a/packages/nextjs/src/server/errors.ts b/packages/nextjs/src/server/errors.ts index b9003c89ffe..f619ee01a48 100644 --- a/packages/nextjs/src/server/errors.ts +++ b/packages/nextjs/src/server/errors.ts @@ -99,4 +99,4 @@ For additional information about middleware, please visit https://clerk.com/docs export const authSignatureInvalid = `Clerk: Unable to verify request, this usually means the Clerk middleware did not run. Ensure Clerk's middleware is properly integrated and matches the current route. For more information, see: https://clerk.com/docs/nextjs/middleware. (code=auth_signature_invalid)`; export const encryptionKeyInvalid = `Clerk: Unable to decrypt request data, this usually means the encryption key is invalid. Ensure the encryption key is properly set. (code=encryption_key_invalid) -For additional information, please visit https://clerk.com/docs/references/nextjs/clerk-middleware#clerk-middleware-options.`; +For additional information, please visit https://clerk.com/docs/references/nextjs/clerk-middleware#server-side-options-propagation.`; diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index 299a40c0597..5ef6cffdaf6 100644 --- a/packages/nextjs/src/server/utils.ts +++ b/packages/nextjs/src/server/utils.ts @@ -157,6 +157,8 @@ export function decorateRequest( } if (rewriteURL) { + const clerkRequestData = encryptClerkRequestData({ secretKey, signInUrl, signUpUrl }); + setRequestHeadersOnNextResponse(res, req, { [constants.Headers.AuthStatus]: status, [constants.Headers.AuthToken]: token || '', @@ -164,7 +166,7 @@ export function decorateRequest( [constants.Headers.AuthMessage]: message || '', [constants.Headers.AuthReason]: reason || '', [constants.Headers.ClerkUrl]: req.clerkUrl.toString(), - [constants.Headers.ClerkRequestData]: encryptClerkRequestData({ secretKey, signInUrl, signUpUrl }), + ...(clerkRequestData ? { [constants.Headers.ClerkRequestData]: clerkRequestData } : {}), }); res.headers.set(nextConstants.Headers.NextRewrite, rewriteURL.href); } @@ -252,7 +254,7 @@ export function assertTokenSignature(token: string, key: string, signature?: str export function encryptClerkRequestData(options: Partial) { /** * Warns if encryption key is missing when secret key is provided to prevent breaking changes. - * Prepares users for a potential escalation to an error in a future major version. + * Prepares users for a escalation to an error in a future major version. */ if (options.secretKey && !ENCRYPTION_KEY) { logger.warnOnce( diff --git a/packages/shared/src/error.ts b/packages/shared/src/error.ts index 88bcd1013a3..dd3fac4e4b3 100644 --- a/packages/shared/src/error.ts +++ b/packages/shared/src/error.ts @@ -223,8 +223,6 @@ export interface ErrorThrower { throwMissingSecretKeyError(): never; - throwMissingEncryptionKeyError(): never; - throwMissingClerkProviderError(params: { source?: string }): never; throw(message: string): never; @@ -283,10 +281,6 @@ export function buildErrorThrower({ packageName, customMessages }: ErrorThrowerO throw new Error(buildMessage(messages.MissingSecretKeyErrorMessage)); }, - throwMissingEncryptionKeyError(): never { - throw new Error(buildMessage(messages.MissingEncryptionKeyErrorMessage)); - }, - throwMissingClerkProviderError(params: { source?: string }): never { throw new Error(buildMessage(messages.MissingClerkProvider, params)); }, From 4deca282a240c6ef569917935c34741fcc5ff117 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 14 Jun 2024 17:51:21 -0300 Subject: [PATCH 23/39] Add todo comment for next major version --- packages/nextjs/src/server/utils.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index 5ef6cffdaf6..e00d94cede6 100644 --- a/packages/nextjs/src/server/utils.ts +++ b/packages/nextjs/src/server/utils.ts @@ -252,11 +252,8 @@ export function assertTokenSignature(token: string, key: string, signature?: str * @internal **/ export function encryptClerkRequestData(options: Partial) { - /** - * Warns if encryption key is missing when secret key is provided to prevent breaking changes. - * Prepares users for a escalation to an error in a future major version. - */ if (options.secretKey && !ENCRYPTION_KEY) { + // TODO SDK-1833: change this to an error in the next major version of `@clerk/nextjs` logger.warnOnce( 'Clerk: Missing `CLERK_ENCRYPTION_KEY`. Required for propagating `secretKey` middleware option. See docs: https://clerk.com/docs/references/nextjs/clerk-middleware#server-side-options-propagation', ); From aa532db6fa51739c3eaee9ef5eac3692433a85d9 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 14 Jun 2024 17:57:31 -0300 Subject: [PATCH 24/39] Propagate secret key to backend client --- packages/nextjs/src/server/buildClerkProps.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/nextjs/src/server/buildClerkProps.ts b/packages/nextjs/src/server/buildClerkProps.ts index 0248a461233..ffffa401d8c 100644 --- a/packages/nextjs/src/server/buildClerkProps.ts +++ b/packages/nextjs/src/server/buildClerkProps.ts @@ -1,6 +1,7 @@ import type { Organization, Session, User } from '@clerk/backend'; import { AuthStatus, + constants, makeAuthObjectSerializable, signedInAuthObject, signedOutAuthObject, @@ -10,7 +11,7 @@ import { decodeJwt } from '@clerk/backend/jwt'; import { API_URL, API_VERSION, SECRET_KEY } from './constants'; import type { RequestLike } from './types'; -import { getAuthKeyFromRequest, injectSSRStateIntoObject } from './utils'; +import { decryptClerkRequestData, getAuthKeyFromRequest, getHeader, injectSSRStateIntoObject } from './utils'; type BuildClerkPropsInitState = { user?: User | null; session?: Session | null; organization?: Organization | null }; @@ -38,8 +39,11 @@ export const buildClerkProps: BuildClerkProps = (req, initState = {}) => { const authMessage = getAuthKeyFromRequest(req, 'AuthMessage'); const authReason = getAuthKeyFromRequest(req, 'AuthReason'); + const encryptedRequestData = getHeader(req, constants.Headers.ClerkRequestData); + const decryptedRequestData = decryptClerkRequestData(encryptedRequestData); + const options = { - secretKey: SECRET_KEY, + secretKey: decryptedRequestData.secretKey || SECRET_KEY, apiUrl: API_URL, apiVersion: API_VERSION, authStatus, From 7109105d89b5d341f3b178eb77e9cd01319a18c4 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 14 Jun 2024 18:39:47 -0300 Subject: [PATCH 25/39] Update changeset --- .changeset/young-pigs-live.md | 9 ++++----- packages/nextjs/src/server/utils.ts | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.changeset/young-pigs-live.md b/.changeset/young-pigs-live.md index af09ee32332..551bab4b483 100644 --- a/.changeset/young-pigs-live.md +++ b/.changeset/young-pigs-live.md @@ -4,9 +4,8 @@ '@clerk/shared': minor --- -Propagate Next.js middleware options to the application server, solving the following issues: -- `auth` couldn't assert the session token signature due to not having access to `secretKey` passed as an option. -- `auth.redirectToSignIn` doesn't redirect to `signInUrl` provided as option. +Enables server-side options propagation for `clerkMiddleware` to the Next.js application server, allowing access for server-side helpers like `auth`. Options such as `signUpUrl`, `signInUrl`, and `secretKey` are securely encrypted using AES algorithm. -`CLERK_ENCRYPTION_KEY` must be defined when providing `secretKey` as an option, in order to securely propagated this value -between servers. For more information regarding options propagation, refer to https://clerk.com/docs/references/nextjs/clerk-middleware#server-side-options-propagation +When using `secretKey`, `CLERK_ENCRYPTION_KEY` is required as the encryption key. If `secretKey` is not provided, `CLERK_SECRET_KEY` is used by default. + +For more information, refer to the documentation: https://clerk.com/docs/references/nextjs/clerk-middleware#server-side-options-propagation diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index e00d94cede6..7270bc52663 100644 --- a/packages/nextjs/src/server/utils.ts +++ b/packages/nextjs/src/server/utils.ts @@ -278,7 +278,7 @@ export function decryptClerkRequestData( } try { - const decryptedBytes = AES.decrypt(encryptedRequestData, ENCRYPTION_KEY ?? SECRET_KEY); + const decryptedBytes = AES.decrypt(encryptedRequestData, ENCRYPTION_KEY || SECRET_KEY); const encoded = decryptedBytes.toString(encUtf8); return JSON.parse(encoded); } catch (err) { From 6ee667be433ad2d4ef08051df4fa6b2f4f3009ee Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 18 Jun 2024 14:12:43 -0300 Subject: [PATCH 26/39] Propagate `publishableKey` to `auth` --- packages/nextjs/src/app-router/server/auth.ts | 4 +--- packages/nextjs/src/server/clerkMiddleware.ts | 2 +- packages/nextjs/src/server/utils.ts | 5 +++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/nextjs/src/app-router/server/auth.ts b/packages/nextjs/src/app-router/server/auth.ts index 254540d08f2..97302ea3d77 100644 --- a/packages/nextjs/src/app-router/server/auth.ts +++ b/packages/nextjs/src/app-router/server/auth.ts @@ -35,9 +35,7 @@ export const auth = (): Auth => { redirectAdapter: redirect, devBrowserToken: devBrowserToken, baseUrl: clerkRequest.clerkUrl.toString(), - // TODO: Support runtime-value configuration of these options - // via setting and reading headers from clerkMiddleware - publishableKey: PUBLISHABLE_KEY, + publishableKey: decryptedRequestData.publishableKey || PUBLISHABLE_KEY, signInUrl: decryptedRequestData.signInUrl || SIGN_IN_URL, signUpUrl: decryptedRequestData.signUpUrl || SIGN_UP_URL, }).redirectToSignIn({ diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index 9509a73879e..8d4a00dcad8 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -153,7 +153,7 @@ export const clerkMiddleware: ClerkMiddleware = withLogger('clerkMiddleware', lo setRequestHeadersOnNextResponse(handlerResult, clerkRequest, { [constants.Headers.EnableDebug]: 'true' }); } - decorateRequest(clerkRequest, handlerResult, requestState, { secretKey, signInUrl, signUpUrl }); + decorateRequest(clerkRequest, handlerResult, requestState, { secretKey, signInUrl, signUpUrl, publishableKey }); return handlerResult; }; diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index 7270bc52663..313548e0398 100644 --- a/packages/nextjs/src/server/utils.ts +++ b/packages/nextjs/src/server/utils.ts @@ -121,7 +121,8 @@ export function decorateRequest( secretKey, signInUrl, signUpUrl, - }: Pick & + publishableKey, + }: Pick & Required>, ): Response { const { reason, message, status, token } = requestState; @@ -157,7 +158,7 @@ export function decorateRequest( } if (rewriteURL) { - const clerkRequestData = encryptClerkRequestData({ secretKey, signInUrl, signUpUrl }); + const clerkRequestData = encryptClerkRequestData({ secretKey, signInUrl, signUpUrl, publishableKey }); setRequestHeadersOnNextResponse(res, req, { [constants.Headers.AuthStatus]: status, From b618edfdfe423a96f459daa9af571af76813dd9d Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 18 Jun 2024 16:45:37 -0300 Subject: [PATCH 27/39] Update changeset to mention `Dynamic Keys` --- .changeset/young-pigs-live.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/young-pigs-live.md b/.changeset/young-pigs-live.md index 551bab4b483..d94fb98faa3 100644 --- a/.changeset/young-pigs-live.md +++ b/.changeset/young-pigs-live.md @@ -4,8 +4,8 @@ '@clerk/shared': minor --- -Enables server-side options propagation for `clerkMiddleware` to the Next.js application server, allowing access for server-side helpers like `auth`. Options such as `signUpUrl`, `signInUrl`, and `secretKey` are securely encrypted using AES algorithm. +Introduces dynamic keys from `clerkMiddleware`, allowing access by server-side helpers like `auth`. Keys such as `signUpUrl`, `signInUrl`, and `secretKey` are securely encrypted using AES algorithm. -When using `secretKey`, `CLERK_ENCRYPTION_KEY` is required as the encryption key. If `secretKey` is not provided, `CLERK_SECRET_KEY` is used by default. +When providing `secretKey`, `CLERK_ENCRYPTION_KEY` is required as the encryption key. If `secretKey` is not provided, `CLERK_SECRET_KEY` is used by default. -For more information, refer to the documentation: https://clerk.com/docs/references/nextjs/clerk-middleware#server-side-options-propagation +For more information, refer to the documentation: https://clerk.com/docs/references/nextjs/clerk-middleware#dynamic-keys From d8cc06ea40aaea0ca81c0c40d8d332fc712db7c2 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 18 Jun 2024 16:54:57 -0300 Subject: [PATCH 28/39] Update encryption key env var for integration tests --- integration/constants.ts | 9 +++++---- integration/presets/envs.ts | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/integration/constants.ts b/integration/constants.ts index 38113a45320..d58e396ed11 100644 --- a/integration/constants.ts +++ b/integration/constants.ts @@ -68,11 +68,12 @@ export const constants = { */ E2E_CLERK_VERSION: process.env.E2E_CLERK_VERSION, /** - * PK and SK pairs from the env to use for integration tests. + * Key used to encrypt request data for Next.js dynamic keys. + * @ref https://clerk.com/docs/references/nextjs/clerk-middleware#dynamic-keys */ - INTEGRATION_INSTANCE_KEYS: process.env.INTEGRATION_INSTANCE_KEYS, + E2E_CLERK_ENCRYPTION_KEY: process.env.CLERK_ENCRYPTION_KEY, /** - * Key used to encrypt request data for Next.js middleware options propagation. + * PK and SK pairs from the env to use for integration tests. */ - CLERK_ENCRYPTION_KEY: process.env.CLERK_ENCRYPTION_KEY, + INTEGRATION_INSTANCE_KEYS: process.env.INTEGRATION_INSTANCE_KEYS, } as const; diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts index cbd98ae0eb4..13dad47aa48 100644 --- a/integration/presets/envs.ts +++ b/integration/presets/envs.ts @@ -30,7 +30,7 @@ const withEmailCodes = environmentConfig() .setEnvVariable('public', 'CLERK_SIGN_IN_URL', '/sign-in') .setEnvVariable('public', 'CLERK_SIGN_UP_URL', '/sign-up') .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') - .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); + .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.E2E_CLERK_ENCRYPTION_KEY); const withEmailLinks = environmentConfig() .setId('withEmailLinks') @@ -40,7 +40,7 @@ const withEmailLinks = environmentConfig() .setEnvVariable('public', 'CLERK_SIGN_IN_URL', '/sign-in') .setEnvVariable('public', 'CLERK_SIGN_UP_URL', '/sign-up') .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') - .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); + .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.E2E_CLERK_ENCRYPTION_KEY); const withCustomRoles = environmentConfig() .setId('withCustomRoles') From 7d462a6c110c81b8ef600edb2b4e00471b1c7848 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 18 Jun 2024 17:38:51 -0300 Subject: [PATCH 29/39] Add `CLERK_ENCRYPTION_KEY` to CI --- .github/workflows/ci.yml | 1 + packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e3973e5159..ce95993ec22 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -206,6 +206,7 @@ jobs: E2E_CLERK_VERSION: 'latest' E2E_NEXTJS_VERSION: ${{ matrix.next-version }} E2E_PROJECT: ${{ matrix.test-project }} + E2E_CLERK_ENCRYPTION_KEY: ${{ matrix.clerk-encryption-key }} INTEGRATION_INSTANCE_KEYS: ${{ secrets.INTEGRATION_INSTANCE_KEYS }} MAILSAC_API_KEY: ${{ secrets.MAILSAC_API_KEY }} diff --git a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts index f70e3ebb8ca..d4e19aa1609 100644 --- a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts +++ b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts @@ -220,9 +220,10 @@ describe('clerkMiddleware(params)', () => { expect(signInResp?.headers.get('a-custom-header')).toEqual('1'); }); - it('propagates middleware options to the next request', async () => { + it('propagates middleware dynamic keys to the next request', async () => { const options = { secretKey: 'sk_test_xxxxxxxxxxxxxxxxxx', + publishableKey: 'pk_test_xxxxxxxxxxxxx', signInUrl: '/foo', signUpUrl: '/bar', }; From f7df0eed3942f4fbe31987f0fb93eba32859d774 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 27 Jun 2024 18:52:07 -0300 Subject: [PATCH 30/39] feat(nextjs): Read dynamic keys from `clerkClient` within middleware runtime (#3617) --- integration/presets/envs.ts | 29 ++-- integration/tests/dynamic-keys.test.ts | 101 ++++++++++++ integration/tests/handshake.test.ts | 2 +- .../src/app-router/server/currentUser.ts | 2 +- .../src/server/__tests__/clerkClient.test.ts | 2 +- .../server/__tests__/clerkMiddleware.test.ts | 34 ++-- packages/nextjs/src/server/clerkClient.ts | 67 +++++++- packages/nextjs/src/server/clerkMiddleware.ts | 146 +++++++++--------- 8 files changed, 278 insertions(+), 105 deletions(-) create mode 100644 integration/tests/dynamic-keys.test.ts diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts index 13dad47aa48..8ef8d7e9651 100644 --- a/integration/presets/envs.ts +++ b/integration/presets/envs.ts @@ -29,8 +29,7 @@ const withEmailCodes = environmentConfig() .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['with-email-codes'].pk) .setEnvVariable('public', 'CLERK_SIGN_IN_URL', '/sign-in') .setEnvVariable('public', 'CLERK_SIGN_UP_URL', '/sign-up') - .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') - .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.E2E_CLERK_ENCRYPTION_KEY); + .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js'); const withEmailLinks = environmentConfig() .setId('withEmailLinks') @@ -39,8 +38,7 @@ const withEmailLinks = environmentConfig() .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['with-email-links'].pk) .setEnvVariable('public', 'CLERK_SIGN_IN_URL', '/sign-in') .setEnvVariable('public', 'CLERK_SIGN_UP_URL', '/sign-up') - .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') - .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.E2E_CLERK_ENCRYPTION_KEY); + .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js'); const withCustomRoles = environmentConfig() .setId('withCustomRoles') @@ -51,8 +49,7 @@ const withCustomRoles = environmentConfig() .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['with-custom-roles'].pk) .setEnvVariable('public', 'CLERK_SIGN_IN_URL', '/sign-in') .setEnvVariable('public', 'CLERK_SIGN_UP_URL', '/sign-up') - .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') - .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); + .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js'); const withEmailCodesQuickstart = withEmailCodes .removeEnvVariable('public', 'CLERK_SIGN_IN_URL') @@ -63,30 +60,33 @@ const withAPCore1ClerkLatest = environmentConfig() .setEnvVariable('public', 'CLERK_TELEMETRY_DISABLED', true) .setEnvVariable('private', 'CLERK_SECRET_KEY', envKeys['with-email-codes'].sk) .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['with-email-codes'].pk) - .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') - .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); + .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js'); const withAPCore1ClerkV4 = environmentConfig() .setId('withAPCore1ClerkV4') .setEnvVariable('public', 'CLERK_TELEMETRY_DISABLED', true) .setEnvVariable('private', 'CLERK_SECRET_KEY', envKeys['with-email-codes'].sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['with-email-codes'].pk) - .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['with-email-codes'].pk); const withAPCore2ClerkLatest = environmentConfig() .setId('withAPCore2ClerkLatest') .setEnvVariable('public', 'CLERK_TELEMETRY_DISABLED', true) .setEnvVariable('private', 'CLERK_SECRET_KEY', envKeys['core-2-all-enabled'].sk) .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['core-2-all-enabled'].pk) - .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') - .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); + .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js'); const withAPCore2ClerkV4 = environmentConfig() .setId('withAPCore2ClerkV4') .setEnvVariable('public', 'CLERK_TELEMETRY_DISABLED', true) .setEnvVariable('private', 'CLERK_SECRET_KEY', envKeys['core-2-all-enabled'].sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['core-2-all-enabled'].pk) - .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['core-2-all-enabled'].pk); + +const withDynamicKeys = withEmailCodes + .clone() + .setId('withDynamicKeys') + .setEnvVariable('private', 'CLERK_SECRET_KEY', '') + .setEnvVariable('private', 'CLERK_DYNAMIC_SECRET_KEY', envKeys['with-email-codes'].sk) + .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.E2E_CLERK_ENCRYPTION_KEY); export const envs = { withEmailCodes, @@ -97,4 +97,5 @@ export const envs = { withAPCore1ClerkV4, withAPCore2ClerkLatest, withAPCore2ClerkV4, + withDynamicKeys, } as const; diff --git a/integration/tests/dynamic-keys.test.ts b/integration/tests/dynamic-keys.test.ts new file mode 100644 index 00000000000..4c08e8656ce --- /dev/null +++ b/integration/tests/dynamic-keys.test.ts @@ -0,0 +1,101 @@ +import { expect, test } from '@playwright/test'; + +import type { Application } from '../models/application'; +import { appConfigs } from '../presets'; +import { createTestUtils } from '../testUtils'; + +test.describe('dynamic keys @nextjs', () => { + test.describe.configure({ mode: 'parallel' }); + let app: Application; + + test.beforeAll(async () => { + app = await appConfigs.next.appRouter + .clone() + .addFile( + 'src/middleware.ts', + () => `import { clerkClient, clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server' + import { NextResponse } from 'next/server' + + const isProtectedRoute = createRouteMatcher(['/protected']); + const shouldFetchBapi = createRouteMatcher(['/fetch-bapi-from-middleware']); + + export default clerkMiddleware(async (auth, request) => { + if (isProtectedRoute(request)) { + auth().protect(); + } + + if (shouldFetchBapi(request)){ + const count = await clerkClient().users.getCount(); + + if (count){ + return NextResponse.redirect(new URL('/users-count', request.url)) + } + } + }, { + secretKey: process.env.CLERK_DYNAMIC_SECRET_KEY, + signInUrl: '/foobar' + }); + + export const config = { + matcher: ['/((?!.*\\\\..*|_next).*)', '/', '/(api|trpc)(.*)'], + };`, + ) + .addFile( + 'src/app/users-count/page.tsx', + () => `import { clerkClient } from '@clerk/nextjs/server' + + export default async function Page(){ + const count = await clerkClient().users.getCount() + + return

Users count: {count}

+ } + `, + ) + .commit(); + + await app.setup(); + await app.withEnv(appConfigs.envs.withDynamicKeys); + await app.dev(); + }); + + test.afterAll(async () => { + await app.teardown(); + }); + + test.afterEach(async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.page.signOut(); + await u.page.context().clearCookies(); + }); + + test('redirects to `signInUrl` on `auth().protect()`', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + + await u.page.goToStart(); + + await u.po.expect.toBeSignedOut(); + + await u.page.goToRelative('/protected'); + + await u.page.waitForURL(/foobar/); + }); + + test('resolves auth signature with `secretKey` on `auth().protect()`', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.page.goToRelative('/page-protected'); + await u.page.waitForURL(/foobar/); + }); + + test('calls `clerkClient` with dynamic keys from application runtime', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.page.goToRelative('/users-count'); + await expect(u.page.getByText(/Users count/i)).toBeVisible(); + }); + + test('calls `clerkClient` with dynamic keys from middleware runtime', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.page.goToRelative('/fetch-bapi-from-middleware'); + await u.page.waitForAppUrl('/users-count'); + await expect(u.page.getByText(/Users count/i)).toBeVisible(); + }); +}); diff --git a/integration/tests/handshake.test.ts b/integration/tests/handshake.test.ts index 5057145ef21..7d8cd203e08 100644 --- a/integration/tests/handshake.test.ts +++ b/integration/tests/handshake.test.ts @@ -8,7 +8,7 @@ import { generateConfig, getJwksFromSecretKey } from '../testUtils/handshake'; const PORT = 4199; -test.skip('Client handshake @generic', () => { +test.describe('Client handshake @generic', () => { test.describe.configure({ mode: 'serial' }); let app: Application; diff --git a/packages/nextjs/src/app-router/server/currentUser.ts b/packages/nextjs/src/app-router/server/currentUser.ts index ae3a2090f85..4d7cb2d7be1 100644 --- a/packages/nextjs/src/app-router/server/currentUser.ts +++ b/packages/nextjs/src/app-router/server/currentUser.ts @@ -9,5 +9,5 @@ export async function currentUser(): Promise { return null; } - return clerkClient.users.getUser(userId); + return clerkClient().users.getUser(userId); } diff --git a/packages/nextjs/src/server/__tests__/clerkClient.test.ts b/packages/nextjs/src/server/__tests__/clerkClient.test.ts index 1ceedefc0f7..58b6e883ddd 100644 --- a/packages/nextjs/src/server/__tests__/clerkClient.test.ts +++ b/packages/nextjs/src/server/__tests__/clerkClient.test.ts @@ -4,7 +4,7 @@ import { clerkClient } from '../clerkClient'; describe('clerkClient', () => { it('should pass version package to userAgent', async () => { - await clerkClient.users.getUser('user_test'); + await clerkClient().users.getUser('user_test'); expect(global.fetch).toBeCalled(); expect((global.fetch as any).mock.calls[0][1].headers).toMatchObject({ diff --git a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts index d4e19aa1609..2763d06f9ba 100644 --- a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts +++ b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts @@ -253,7 +253,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toContain('sign-in'); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('redirects to sign-in url when redirectToSignIn is called with the correct returnBackUrl', async () => { @@ -270,7 +270,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toContain('sign-in'); expect(new URL(resp!.headers.get('location')!).searchParams.get('redirect_url')).toContain('/protected'); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('redirects to sign-in url with redirect_url set to the provided returnBackUrl param', async () => { @@ -289,7 +289,7 @@ describe('clerkMiddleware(params)', () => { expect(new URL(resp!.headers.get('location')!).searchParams.get('redirect_url')).toEqual( 'https://www.clerk.com/hello', ); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('redirects to sign-in url without a redirect_url when returnBackUrl is null', async () => { @@ -306,7 +306,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toContain('sign-in'); expect(new URL(resp!.headers.get('location')!).searchParams.get('redirect_url')).toBeNull(); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); }); @@ -331,7 +331,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toContain('sign-in'); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('does not redirect to sign-in url when protect is called, the user is signed in and the request is a page request', async () => { @@ -354,7 +354,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(200); expect(resp?.headers.get('location')).toBeFalsy(); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('throws a not found error when protect is called, the user is signed out, and is not a page request', async () => { @@ -377,7 +377,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(200); expect(resp?.headers.get(constants.Headers.AuthReason)).toContain('protect-rewrite'); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('throws a not found error when protect is called with RBAC params the user does not fulfill, and is a page request', async () => { @@ -400,7 +400,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(200); expect(resp?.headers.get(constants.Headers.AuthReason)).toContain('protect-rewrite'); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('redirects to unauthenticatedUrl when protect is called with the redirectUrl param, the user is signed out, and is a page request', async () => { @@ -424,7 +424,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toEqual('https://www.clerk.com/hello'); expect(resp?.headers.get(constants.Headers.ClerkRedirectTo)).toEqual('true'); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('redirects to unauthorizedUrl when protect is called with the redirectUrl param, the user does not fulfill the RBAC params, and is a page request', async () => { @@ -454,7 +454,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toEqual('https://www.clerk.com/discover'); expect(resp?.headers.get(constants.Headers.ClerkRedirectTo)).toEqual('true'); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); }); @@ -479,7 +479,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toContain('sign-in'); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('forwards headers from authenticateRequest when auth().protect() is called', async () => { @@ -507,7 +507,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.headers.get('X-Clerk-Auth')).toEqual('1'); expect(resp?.headers.get('Set-Cookie')).toEqual('session=;'); expect(resp?.headers.get('location')).toContain('sign-in'); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('redirects to unauthenticatedUrl when protect is called with the unauthenticatedUrl param, the user is signed out, and is a page request', async () => { @@ -534,7 +534,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toContain('https://www.clerk.com/unauthenticatedUrl'); expect(resp?.headers.get(constants.Headers.ClerkRedirectTo)).toEqual('true'); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('redirects to unauthorizedUrl when protect is called with the unauthorizedUrl param, the user is signed in but does not have permissions, and is a page request', async () => { @@ -564,7 +564,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toContain('https://www.clerk.com/unauthorizedUrl'); expect(resp?.headers.get(constants.Headers.ClerkRedirectTo)).toEqual('true'); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); }); }); @@ -592,7 +592,7 @@ describe('Dev Browser JWT when redirecting to cross origin for page requests', f expect(resp?.headers.get('location')).toEqual( 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected', ); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('appends the Dev Browser JWT to the search when cookie __clerk_db_jwt exists and location is an Account Portal URL', async () => { @@ -616,7 +616,7 @@ describe('Dev Browser JWT when redirecting to cross origin for page requests', f expect(resp?.headers.get('location')).toEqual( 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected&__clerk_db_jwt=test_jwt', ); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('does NOT append the Dev Browser JWT if x-clerk-redirect-to header is not set (user-returned redirect)', async () => { @@ -642,6 +642,6 @@ describe('Dev Browser JWT when redirecting to cross origin for page requests', f expect(resp?.headers.get('location')).toEqual( 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected', ); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); }); diff --git a/packages/nextjs/src/server/clerkClient.ts b/packages/nextjs/src/server/clerkClient.ts index 41b6559b5a3..eb8006408ec 100644 --- a/packages/nextjs/src/server/clerkClient.ts +++ b/packages/nextjs/src/server/clerkClient.ts @@ -1,5 +1,10 @@ +import type { ClerkClient } from '@clerk/backend'; import { createClerkClient } from '@clerk/backend'; +import type { AuthenticateRequestOptions } from '@clerk/backend/internal'; +import { constants } from '@clerk/backend/internal'; +import { deprecated } from '@clerk/shared/deprecated'; +import { buildRequestLike } from '../app-router/server/utils'; import { API_URL, API_VERSION, @@ -12,8 +17,9 @@ import { TELEMETRY_DEBUG, TELEMETRY_DISABLED, } from './constants'; +import { decryptClerkRequestData, getHeader } from './utils'; -const clerkClient = createClerkClient({ +const clerkClientDefaultOptions = { secretKey: SECRET_KEY, publishableKey: PUBLISHABLE_KEY, apiUrl: API_URL, @@ -27,6 +33,65 @@ const clerkClient = createClerkClient({ disabled: TELEMETRY_DISABLED, debug: TELEMETRY_DEBUG, }, +}; + +export const clerkClientStorage = new AsyncLocalStorage>(); + +const createClerkClientWithOptions: typeof createClerkClient = options => + createClerkClient({ ...clerkClientDefaultOptions, ...options }); + +const clerkClientSingleton = createClerkClient(clerkClientDefaultOptions); + +/** + * @deprecated + * This singleton is deprecated and will be removed in a future release. Please use `clerkClient()` as a function instead. + */ +const clerkClientSingletonProxy = new Proxy(clerkClientSingleton, { + get(target, prop, receiver) { + if (Object.getPrototypeOf(target) === Object.getPrototypeOf(clerkClientSingleton)) { + deprecated('clerkClient object', 'Use `clerkClient()` as a function instead.'); + } + + return Reflect.get(target, prop, receiver); + }, }); +/** + * Constructs a BAPI client that accesses request data within the runtime. + * Necessary if middleware dynamic keys are used. + */ +const clerkClientForRequest = () => { + let requestData: Partial | undefined; + + /** + * For BAPI client usage inside middleware runtime, fallbacks to AsyncLocalStorage to access request data + */ + const clerkClientStore = clerkClientStorage.getStore(); + if (clerkClientStore) { + requestData = clerkClientStore; + } else { + /** + * For BAPI usage from application server, fallbacks to access request data via `NextRequest` + */ + const request = buildRequestLike(); + const encryptedRequestData = getHeader(request, constants.Headers.ClerkRequestData); + requestData = decryptClerkRequestData(encryptedRequestData); + } + + if (requestData?.secretKey || requestData?.publishableKey) { + return createClerkClientWithOptions({ + secretKey: requestData.secretKey, + publishableKey: requestData.publishableKey, + }); + } + + return clerkClientSingleton; +}; + +const clerkClient: ClerkClient & typeof clerkClientForRequest = Object.assign( + clerkClientForRequest, + // TODO SDK-1839 - Remove `clerkClient` singleton in the next major version of `@clerk/nextjs` + clerkClientSingletonProxy, +); + export { clerkClient }; diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index 8d4a00dcad8..6de46fe0283 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -12,7 +12,7 @@ import { NextResponse } from 'next/server'; import { isRedirect, serverRedirectWithAuth, setHeader } from '../utils'; import { withLogger } from '../utils/debugLogger'; -import { clerkClient } from './clerkClient'; +import { clerkClient, clerkClientStorage } from './clerkClient'; import { PUBLISHABLE_KEY, SECRET_KEY, SIGN_IN_URL, SIGN_UP_URL } from './constants'; import { errorThrower } from './errorThrower'; import type { AuthProtect } from './protect'; @@ -91,82 +91,88 @@ export const clerkMiddleware: ClerkMiddleware = withLogger('clerkMiddleware', lo signUpUrl, }; - clerkClient.telemetry.record( - eventMethodCalled('clerkMiddleware', { - handler: Boolean(handler), - satellite: Boolean(options.isSatellite), - proxy: Boolean(options.proxyUrl), - }), - ); - - const nextMiddleware: NextMiddleware = async (request, event) => { - const clerkRequest = createClerkRequest(request); - logger.debug('options', options); - logger.debug('url', () => clerkRequest.toJSON()); - - const requestState = await clerkClient.authenticateRequest( - clerkRequest, - createAuthenticateRequestOptions(clerkRequest, options), + return clerkClientStorage.run(options, () => { + clerkClient().telemetry.record( + eventMethodCalled('clerkMiddleware', { + handler: Boolean(handler), + satellite: Boolean(options.isSatellite), + proxy: Boolean(options.proxyUrl), + }), ); - logger.debug('requestState', () => ({ - status: requestState.status, - headers: JSON.stringify(Object.fromEntries(requestState.headers)), - reason: requestState.reason, - })); - - const locationHeader = requestState.headers.get(constants.Headers.Location); - if (locationHeader) { - return new Response(null, { status: 307, headers: requestState.headers }); - } else if (requestState.status === AuthStatus.Handshake) { - throw new Error('Clerk: handshake status without redirect'); - } - - const authObject = requestState.toAuth(); - logger.debug('auth', () => ({ auth: authObject, debug: authObject.debug() })); + const nextMiddleware: NextMiddleware = async (request, event) => { + const clerkRequest = createClerkRequest(request); + logger.debug('options', options); + logger.debug('url', () => clerkRequest.toJSON()); - const redirectToSignIn = createMiddlewareRedirectToSignIn(clerkRequest); - const protect = createMiddlewareProtect(clerkRequest, authObject, redirectToSignIn); - const authObjWithMethods: ClerkMiddlewareAuthObject = Object.assign(authObject, { protect, redirectToSignIn }); - - let handlerResult: Response = NextResponse.next(); - try { - handlerResult = (await handler?.(() => authObjWithMethods, request, event)) || handlerResult; - } catch (e: any) { - handlerResult = handleControlFlowErrors(e, clerkRequest, requestState); - } - - // TODO @nikos: we need to make this more generic - // and move the logic in clerk/backend - if (requestState.headers) { - requestState.headers.forEach((value, key) => { - handlerResult.headers.append(key, value); - }); - } + const requestState = await clerkClient().authenticateRequest( + clerkRequest, + createAuthenticateRequestOptions(clerkRequest, options), + ); - if (isRedirect(handlerResult)) { - logger.debug('handlerResult is redirect'); - return serverRedirectWithAuth(clerkRequest, handlerResult, options); - } + logger.debug('requestState', () => ({ + status: requestState.status, + headers: JSON.stringify(Object.fromEntries(requestState.headers)), + reason: requestState.reason, + })); + + const locationHeader = requestState.headers.get(constants.Headers.Location); + if (locationHeader) { + return new Response(null, { status: 307, headers: requestState.headers }); + } else if (requestState.status === AuthStatus.Handshake) { + throw new Error('Clerk: handshake status without redirect'); + } + + const authObject = requestState.toAuth(); + logger.debug('auth', () => ({ auth: authObject, debug: authObject.debug() })); + + const redirectToSignIn = createMiddlewareRedirectToSignIn(clerkRequest); + const protect = createMiddlewareProtect(clerkRequest, authObject, redirectToSignIn); + const authObjWithMethods: ClerkMiddlewareAuthObject = Object.assign(authObject, { protect, redirectToSignIn }); + + let handlerResult: Response = NextResponse.next(); + try { + handlerResult = + (await clerkClientStorage.run( + options, + async () => await handler?.(() => authObjWithMethods, request, event), + )) || handlerResult; + } catch (e: any) { + handlerResult = handleControlFlowErrors(e, clerkRequest, requestState); + } + + // TODO @nikos: we need to make this more generic + // and move the logic in clerk/backend + if (requestState.headers) { + requestState.headers.forEach((value, key) => { + handlerResult.headers.append(key, value); + }); + } + + if (isRedirect(handlerResult)) { + logger.debug('handlerResult is redirect'); + return serverRedirectWithAuth(clerkRequest, handlerResult, options); + } + + if (options.debug) { + setRequestHeadersOnNextResponse(handlerResult, clerkRequest, { [constants.Headers.EnableDebug]: 'true' }); + } + + decorateRequest(clerkRequest, handlerResult, requestState, options); + + return handlerResult; + }; - if (options.debug) { - setRequestHeadersOnNextResponse(handlerResult, clerkRequest, { [constants.Headers.EnableDebug]: 'true' }); + // If we have a request and event, we're being called as a middleware directly + // eg, export default clerkMiddleware; + if (request && event) { + return nextMiddleware(request, event); } - decorateRequest(clerkRequest, handlerResult, requestState, { secretKey, signInUrl, signUpUrl, publishableKey }); - - return handlerResult; - }; - - // If we have a request and event, we're being called as a middleware directly - // eg, export default clerkMiddleware; - if (request && event) { - return nextMiddleware(request, event); - } - - // Otherwise, return a middleware that can be called with a request and event - // eg, export default clerkMiddleware(auth => { ... }); - return nextMiddleware; + // Otherwise, return a middleware that can be called with a request and event + // eg, export default clerkMiddleware(auth => { ... }); + return nextMiddleware; + }); }); const parseRequestAndEvent = (args: unknown[]) => { From f3d83c8aee13a7872f94e3b8b87e3fdd20d92e8f Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 27 Jun 2024 19:40:25 -0300 Subject: [PATCH 31/39] Rollback changes on `authMiddleware` --- .../server/__tests__/authMiddleware.test.ts | 1 - packages/nextjs/src/server/authMiddleware.ts | 7 +----- packages/nextjs/src/server/clerkMiddleware.ts | 2 +- packages/nextjs/src/server/utils.ts | 23 +++++++++---------- 4 files changed, 13 insertions(+), 20 deletions(-) diff --git a/packages/nextjs/src/server/__tests__/authMiddleware.test.ts b/packages/nextjs/src/server/__tests__/authMiddleware.test.ts index 31deb0b2be6..59457f284fe 100644 --- a/packages/nextjs/src/server/__tests__/authMiddleware.test.ts +++ b/packages/nextjs/src/server/__tests__/authMiddleware.test.ts @@ -16,7 +16,6 @@ jest.mock('../constants', () => { return { PUBLISHABLE_KEY: 'pk_test_Y2xlcmsuaW5jbHVkZWQua2F0eWRpZC05Mi5sY2wuZGV2JA', SECRET_KEY: 'sk_test_xxxxxxxxxxxxxxxxxx', - ENCRYPTION_KEY: 'encryption-key', }; }); diff --git a/packages/nextjs/src/server/authMiddleware.ts b/packages/nextjs/src/server/authMiddleware.ts index f878d6c31c4..e9803a0322e 100644 --- a/packages/nextjs/src/server/authMiddleware.ts +++ b/packages/nextjs/src/server/authMiddleware.ts @@ -219,12 +219,7 @@ const authMiddleware: AuthMiddleware = (...args: unknown[]) => { logger.debug(`Added ${constants.Headers.EnableDebug} on request`); } - const result = - decorateRequest(clerkRequest, finalRes, requestState, { - secretKey, - signInUrl: params.signInUrl, - signUpUrl: params.signUpUrl, - }) || NextResponse.next(); + const result = decorateRequest(clerkRequest, finalRes, requestState, { secretKey }) || NextResponse.next(); if (requestState.headers) { requestState.headers.forEach((value, key) => { diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index 6de46fe0283..062724d04ba 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -158,7 +158,7 @@ export const clerkMiddleware: ClerkMiddleware = withLogger('clerkMiddleware', lo setRequestHeadersOnNextResponse(handlerResult, clerkRequest, { [constants.Headers.EnableDebug]: 'true' }); } - decorateRequest(clerkRequest, handlerResult, requestState, options); + decorateRequest(clerkRequest, handlerResult, requestState, params); return handlerResult; }; diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index 313548e0398..c3937ef6142 100644 --- a/packages/nextjs/src/server/utils.ts +++ b/packages/nextjs/src/server/utils.ts @@ -117,13 +117,7 @@ export function decorateRequest( req: ClerkRequest, res: Response, requestState: RequestState, - { - secretKey, - signInUrl, - signUpUrl, - publishableKey, - }: Pick & - Required>, + requestData?: AuthenticateRequestOptions, ): Response { const { reason, message, status, token } = requestState; // pass-through case, convert to next() @@ -158,12 +152,12 @@ export function decorateRequest( } if (rewriteURL) { - const clerkRequestData = encryptClerkRequestData({ secretKey, signInUrl, signUpUrl, publishableKey }); + const clerkRequestData = encryptClerkRequestData(requestData); setRequestHeadersOnNextResponse(res, req, { [constants.Headers.AuthStatus]: status, [constants.Headers.AuthToken]: token || '', - [constants.Headers.AuthSignature]: token ? createTokenSignature(token, secretKey) : '', + [constants.Headers.AuthSignature]: token ? createTokenSignature(token, requestData?.secretKey ?? SECRET_KEY) : '', [constants.Headers.AuthMessage]: message || '', [constants.Headers.AuthReason]: reason || '', [constants.Headers.ClerkUrl]: req.clerkUrl.toString(), @@ -252,17 +246,22 @@ export function assertTokenSignature(token: string, key: string, signature?: str * Encrypt request data propagated between server requests. * @internal **/ -export function encryptClerkRequestData(options: Partial) { - if (options.secretKey && !ENCRYPTION_KEY) { +export function encryptClerkRequestData(requestData?: Partial) { + if (!requestData || !Object.values(requestData).length) { + return; + } + + if (requestData.secretKey && !ENCRYPTION_KEY) { // TODO SDK-1833: change this to an error in the next major version of `@clerk/nextjs` logger.warnOnce( 'Clerk: Missing `CLERK_ENCRYPTION_KEY`. Required for propagating `secretKey` middleware option. See docs: https://clerk.com/docs/references/nextjs/clerk-middleware#server-side-options-propagation', ); + return; } return AES.encrypt( - JSON.stringify(options), + JSON.stringify(requestData), ENCRYPTION_KEY ?? assertKey(SECRET_KEY, () => errorThrower.throwMissingSecretKeyError()), ).toString(); } From 25eaaf2982b35f66fa6491dea03bc5ac5982f5de Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 28 Jun 2024 12:05:27 -0300 Subject: [PATCH 32/39] Fix unit tests and Proxy --- .changeset/young-pigs-live.md | 2 +- .../server/__tests__/clerkMiddleware.test.ts | 4 +- packages/nextjs/src/server/clerkClient.ts | 38 +++++++++---------- packages/nextjs/src/server/clerkMiddleware.ts | 9 +++-- 4 files changed, 26 insertions(+), 27 deletions(-) diff --git a/.changeset/young-pigs-live.md b/.changeset/young-pigs-live.md index d94fb98faa3..035324714b4 100644 --- a/.changeset/young-pigs-live.md +++ b/.changeset/young-pigs-live.md @@ -4,7 +4,7 @@ '@clerk/shared': minor --- -Introduces dynamic keys from `clerkMiddleware`, allowing access by server-side helpers like `auth`. Keys such as `signUpUrl`, `signInUrl`, and `secretKey` are securely encrypted using AES algorithm. +Introduces dynamic keys from `clerkMiddleware`, allowing access by server-side helpers like `auth`. Keys such as `signUpUrl`, `signInUrl`, `publishableKey` and `secretKey` are securely encrypted using AES algorithm. When providing `secretKey`, `CLERK_ENCRYPTION_KEY` is required as the encryption key. If `secretKey` is not provided, `CLERK_SECRET_KEY` is used by default. diff --git a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts index 2763d06f9ba..d793c9493ec 100644 --- a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts +++ b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts @@ -14,10 +14,10 @@ const authenticateRequestMock = jest.fn().mockResolvedValue({ jest.mock('../clerkClient', () => { return { - clerkClient: { + clerkClient: () => ({ authenticateRequest: authenticateRequestMock, telemetry: { record: jest.fn() }, - }, + }), }; }); diff --git a/packages/nextjs/src/server/clerkClient.ts b/packages/nextjs/src/server/clerkClient.ts index eb8006408ec..41da10c4e10 100644 --- a/packages/nextjs/src/server/clerkClient.ts +++ b/packages/nextjs/src/server/clerkClient.ts @@ -5,6 +5,7 @@ import { constants } from '@clerk/backend/internal'; import { deprecated } from '@clerk/shared/deprecated'; import { buildRequestLike } from '../app-router/server/utils'; +import { clerkMiddlewareRequestDataStore } from './clerkMiddleware'; import { API_URL, API_VERSION, @@ -35,26 +36,14 @@ const clerkClientDefaultOptions = { }, }; -export const clerkClientStorage = new AsyncLocalStorage>(); - const createClerkClientWithOptions: typeof createClerkClient = options => createClerkClient({ ...clerkClientDefaultOptions, ...options }); -const clerkClientSingleton = createClerkClient(clerkClientDefaultOptions); - /** * @deprecated * This singleton is deprecated and will be removed in a future release. Please use `clerkClient()` as a function instead. */ -const clerkClientSingletonProxy = new Proxy(clerkClientSingleton, { - get(target, prop, receiver) { - if (Object.getPrototypeOf(target) === Object.getPrototypeOf(clerkClientSingleton)) { - deprecated('clerkClient object', 'Use `clerkClient()` as a function instead.'); - } - - return Reflect.get(target, prop, receiver); - }, -}); +const clerkClientSingleton = createClerkClient(clerkClientDefaultOptions); /** * Constructs a BAPI client that accesses request data within the runtime. @@ -66,9 +55,9 @@ const clerkClientForRequest = () => { /** * For BAPI client usage inside middleware runtime, fallbacks to AsyncLocalStorage to access request data */ - const clerkClientStore = clerkClientStorage.getStore(); - if (clerkClientStore) { - requestData = clerkClientStore; + const store = clerkMiddlewareRequestDataStore.getStore(); + if (store) { + requestData = store; } else { /** * For BAPI usage from application server, fallbacks to access request data via `NextRequest` @@ -88,10 +77,17 @@ const clerkClientForRequest = () => { return clerkClientSingleton; }; -const clerkClient: ClerkClient & typeof clerkClientForRequest = Object.assign( - clerkClientForRequest, - // TODO SDK-1839 - Remove `clerkClient` singleton in the next major version of `@clerk/nextjs` - clerkClientSingletonProxy, -); +interface ClerkClientExport extends ClerkClient { + (): ClerkClient; +} + +// TODO SDK-1839 - Remove `clerkClient` singleton in the next major version of `@clerk/nextjs` +const clerkClient = new Proxy(Object.assign(clerkClientForRequest, clerkClientSingleton), { + get(target, prop: string, receiver) { + deprecated('clerkClient object', 'Use `clerkClient()` as a function instead.'); + + return Reflect.get(target, prop, receiver); + }, +}) as ClerkClientExport; export { clerkClient }; diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index 062724d04ba..7c110cc6e6d 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -7,12 +7,13 @@ import type { } from '@clerk/backend/internal'; import { AuthStatus, constants, createClerkRequest, createRedirect } from '@clerk/backend/internal'; import { eventMethodCalled } from '@clerk/shared/telemetry'; +import { AsyncLocalStorage } from 'async_hooks'; import type { NextMiddleware } from 'next/server'; import { NextResponse } from 'next/server'; import { isRedirect, serverRedirectWithAuth, setHeader } from '../utils'; import { withLogger } from '../utils/debugLogger'; -import { clerkClient, clerkClientStorage } from './clerkClient'; +import { clerkClient } from './clerkClient'; import { PUBLISHABLE_KEY, SECRET_KEY, SIGN_IN_URL, SIGN_UP_URL } from './constants'; import { errorThrower } from './errorThrower'; import type { AuthProtect } from './protect'; @@ -69,6 +70,8 @@ interface ClerkMiddleware { (request: NextMiddlewareRequestParam, event: NextMiddlewareEvtParam): NextMiddlewareReturn; } +export const clerkMiddlewareRequestDataStore = new AsyncLocalStorage>(); + export const clerkMiddleware: ClerkMiddleware = withLogger('clerkMiddleware', logger => (...args: unknown[]): any => { const [request, event] = parseRequestAndEvent(args); const [handler, params] = parseHandlerAndOptions(args); @@ -91,7 +94,7 @@ export const clerkMiddleware: ClerkMiddleware = withLogger('clerkMiddleware', lo signUpUrl, }; - return clerkClientStorage.run(options, () => { + return clerkMiddlewareRequestDataStore.run(options, () => { clerkClient().telemetry.record( eventMethodCalled('clerkMiddleware', { handler: Boolean(handler), @@ -133,7 +136,7 @@ export const clerkMiddleware: ClerkMiddleware = withLogger('clerkMiddleware', lo let handlerResult: Response = NextResponse.next(); try { handlerResult = - (await clerkClientStorage.run( + (await clerkMiddlewareRequestDataStore.run( options, async () => await handler?.(() => authObjWithMethods, request, event), )) || handlerResult; From e2b4078260a0041430552e307d85a37a64a6b4e4 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 28 Jun 2024 13:08:22 -0300 Subject: [PATCH 33/39] Mention `clerkClient` on changeset --- .changeset/young-pigs-live.md | 3 ++- packages/nextjs/src/server/clerkClient.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.changeset/young-pigs-live.md b/.changeset/young-pigs-live.md index 035324714b4..bc6a63b5f8f 100644 --- a/.changeset/young-pigs-live.md +++ b/.changeset/young-pigs-live.md @@ -6,6 +6,7 @@ Introduces dynamic keys from `clerkMiddleware`, allowing access by server-side helpers like `auth`. Keys such as `signUpUrl`, `signInUrl`, `publishableKey` and `secretKey` are securely encrypted using AES algorithm. -When providing `secretKey`, `CLERK_ENCRYPTION_KEY` is required as the encryption key. If `secretKey` is not provided, `CLERK_SECRET_KEY` is used by default. +- When providing `secretKey`, `CLERK_ENCRYPTION_KEY` is required as the encryption key. If `secretKey` is not provided, `CLERK_SECRET_KEY` is used by default. +- `clerkClient` from `@clerk/nextjs` should now be called as a function, and it's singleton is deprecated, in favor of reading dynamic keys from Next.js runtime. For more information, refer to the documentation: https://clerk.com/docs/references/nextjs/clerk-middleware#dynamic-keys diff --git a/packages/nextjs/src/server/clerkClient.ts b/packages/nextjs/src/server/clerkClient.ts index 41da10c4e10..aea88ebcf5a 100644 --- a/packages/nextjs/src/server/clerkClient.ts +++ b/packages/nextjs/src/server/clerkClient.ts @@ -84,7 +84,7 @@ interface ClerkClientExport extends ClerkClient { // TODO SDK-1839 - Remove `clerkClient` singleton in the next major version of `@clerk/nextjs` const clerkClient = new Proxy(Object.assign(clerkClientForRequest, clerkClientSingleton), { get(target, prop: string, receiver) { - deprecated('clerkClient object', 'Use `clerkClient()` as a function instead.'); + deprecated('clerkClient singleton', 'Use `clerkClient()` as a function instead.'); return Reflect.get(target, prop, receiver); }, From 89a8a4c229a3d1385d38a8ea52c88212018d0a66 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 28 Jun 2024 13:32:18 -0300 Subject: [PATCH 34/39] Fallback to ALS on `headers` error --- integration/presets/envs.ts | 6 +++--- packages/nextjs/src/server/clerkClient.ts | 17 +++++------------ packages/nextjs/src/server/errors.ts | 2 +- packages/nextjs/src/server/utils.ts | 2 +- 4 files changed, 10 insertions(+), 17 deletions(-) diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts index 8ef8d7e9651..ffa819df68a 100644 --- a/integration/presets/envs.ts +++ b/integration/presets/envs.ts @@ -29,7 +29,8 @@ const withEmailCodes = environmentConfig() .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['with-email-codes'].pk) .setEnvVariable('public', 'CLERK_SIGN_IN_URL', '/sign-in') .setEnvVariable('public', 'CLERK_SIGN_UP_URL', '/sign-up') - .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js'); + .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') + .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.E2E_CLERK_ENCRYPTION_KEY); const withEmailLinks = environmentConfig() .setId('withEmailLinks') @@ -85,8 +86,7 @@ const withDynamicKeys = withEmailCodes .clone() .setId('withDynamicKeys') .setEnvVariable('private', 'CLERK_SECRET_KEY', '') - .setEnvVariable('private', 'CLERK_DYNAMIC_SECRET_KEY', envKeys['with-email-codes'].sk) - .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.E2E_CLERK_ENCRYPTION_KEY); + .setEnvVariable('private', 'CLERK_DYNAMIC_SECRET_KEY', envKeys['with-email-codes'].sk); export const envs = { withEmailCodes, diff --git a/packages/nextjs/src/server/clerkClient.ts b/packages/nextjs/src/server/clerkClient.ts index aea88ebcf5a..2c5692caff1 100644 --- a/packages/nextjs/src/server/clerkClient.ts +++ b/packages/nextjs/src/server/clerkClient.ts @@ -1,6 +1,5 @@ import type { ClerkClient } from '@clerk/backend'; import { createClerkClient } from '@clerk/backend'; -import type { AuthenticateRequestOptions } from '@clerk/backend/internal'; import { constants } from '@clerk/backend/internal'; import { deprecated } from '@clerk/shared/deprecated'; @@ -50,21 +49,15 @@ const clerkClientSingleton = createClerkClient(clerkClientDefaultOptions); * Necessary if middleware dynamic keys are used. */ const clerkClientForRequest = () => { - let requestData: Partial | undefined; + let requestData; - /** - * For BAPI client usage inside middleware runtime, fallbacks to AsyncLocalStorage to access request data - */ - const store = clerkMiddlewareRequestDataStore.getStore(); - if (store) { - requestData = store; - } else { - /** - * For BAPI usage from application server, fallbacks to access request data via `NextRequest` - */ + try { const request = buildRequestLike(); const encryptedRequestData = getHeader(request, constants.Headers.ClerkRequestData); requestData = decryptClerkRequestData(encryptedRequestData); + } catch (err) { + // When outside of middleware runtime, fallbacks to access request data from `NextRequest` + requestData = clerkMiddlewareRequestDataStore.getStore(); } if (requestData?.secretKey || requestData?.publishableKey) { diff --git a/packages/nextjs/src/server/errors.ts b/packages/nextjs/src/server/errors.ts index f619ee01a48..25906b22283 100644 --- a/packages/nextjs/src/server/errors.ts +++ b/packages/nextjs/src/server/errors.ts @@ -99,4 +99,4 @@ For additional information about middleware, please visit https://clerk.com/docs export const authSignatureInvalid = `Clerk: Unable to verify request, this usually means the Clerk middleware did not run. Ensure Clerk's middleware is properly integrated and matches the current route. For more information, see: https://clerk.com/docs/nextjs/middleware. (code=auth_signature_invalid)`; export const encryptionKeyInvalid = `Clerk: Unable to decrypt request data, this usually means the encryption key is invalid. Ensure the encryption key is properly set. (code=encryption_key_invalid) -For additional information, please visit https://clerk.com/docs/references/nextjs/clerk-middleware#server-side-options-propagation.`; +For additional information, please visit https://clerk.com/docs/references/nextjs/clerk-middleware#dynamic-keys.`; diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index c3937ef6142..43c961f04f2 100644 --- a/packages/nextjs/src/server/utils.ts +++ b/packages/nextjs/src/server/utils.ts @@ -254,7 +254,7 @@ export function encryptClerkRequestData(requestData?: Partial Date: Mon, 1 Jul 2024 14:34:46 -0300 Subject: [PATCH 35/39] Update changeset --- .changeset/young-pigs-live.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/young-pigs-live.md b/.changeset/young-pigs-live.md index bc6a63b5f8f..1484035e8eb 100644 --- a/.changeset/young-pigs-live.md +++ b/.changeset/young-pigs-live.md @@ -7,6 +7,6 @@ Introduces dynamic keys from `clerkMiddleware`, allowing access by server-side helpers like `auth`. Keys such as `signUpUrl`, `signInUrl`, `publishableKey` and `secretKey` are securely encrypted using AES algorithm. - When providing `secretKey`, `CLERK_ENCRYPTION_KEY` is required as the encryption key. If `secretKey` is not provided, `CLERK_SECRET_KEY` is used by default. -- `clerkClient` from `@clerk/nextjs` should now be called as a function, and it's singleton is deprecated, in favor of reading dynamic keys from Next.js runtime. +- `clerkClient` from `@clerk/nextjs` should now be called as a function, and its singleton form is deprecated. This change allows the Clerk backend client to read keys from the current request, which is necessary to support dynamic keys. For more information, refer to the documentation: https://clerk.com/docs/references/nextjs/clerk-middleware#dynamic-keys From ed3aa79877b8e66ac60ddb41dc3aefe519d11749 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Mon, 1 Jul 2024 14:36:05 -0300 Subject: [PATCH 36/39] Fix error formatting --- packages/nextjs/src/server/errors.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/nextjs/src/server/errors.ts b/packages/nextjs/src/server/errors.ts index 25906b22283..eba3589e233 100644 --- a/packages/nextjs/src/server/errors.ts +++ b/packages/nextjs/src/server/errors.ts @@ -98,5 +98,4 @@ For additional information about middleware, please visit https://clerk.com/docs export const authSignatureInvalid = `Clerk: Unable to verify request, this usually means the Clerk middleware did not run. Ensure Clerk's middleware is properly integrated and matches the current route. For more information, see: https://clerk.com/docs/nextjs/middleware. (code=auth_signature_invalid)`; -export const encryptionKeyInvalid = `Clerk: Unable to decrypt request data, this usually means the encryption key is invalid. Ensure the encryption key is properly set. (code=encryption_key_invalid) -For additional information, please visit https://clerk.com/docs/references/nextjs/clerk-middleware#dynamic-keys.`; +export const encryptionKeyInvalid = `Clerk: Unable to decrypt request data, this usually means the encryption key is invalid. Ensure the encryption key is properly set. For more information, see: https://clerk.com/docs/references/nextjs/clerk-middleware#dynamic-keys. (code=encryption_key_invalid)`; From 7170e313af5c920865b2a10a5c35cda2483ecb84 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Mon, 1 Jul 2024 15:09:48 -0300 Subject: [PATCH 37/39] Extract middleware handler to separate variable --- packages/nextjs/src/server/clerkClient.ts | 9 +++++---- packages/nextjs/src/server/clerkMiddleware.ts | 9 ++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/nextjs/src/server/clerkClient.ts b/packages/nextjs/src/server/clerkClient.ts index 2c5692caff1..60a5e204739 100644 --- a/packages/nextjs/src/server/clerkClient.ts +++ b/packages/nextjs/src/server/clerkClient.ts @@ -51,13 +51,14 @@ const clerkClientSingleton = createClerkClient(clerkClientDefaultOptions); const clerkClientForRequest = () => { let requestData; - try { + const middlewareStore = clerkMiddlewareRequestDataStore.getStore(); + if (Object.values(middlewareStore ?? {}).length) { + requestData = middlewareStore; + } else { + // When outside of middleware runtime, fallbacks to access request data from `NextRequest` const request = buildRequestLike(); const encryptedRequestData = getHeader(request, constants.Headers.ClerkRequestData); requestData = decryptClerkRequestData(encryptedRequestData); - } catch (err) { - // When outside of middleware runtime, fallbacks to access request data from `NextRequest` - requestData = clerkMiddlewareRequestDataStore.getStore(); } if (requestData?.secretKey || requestData?.publishableKey) { diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index 7c110cc6e6d..586df62c117 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -135,11 +135,10 @@ export const clerkMiddleware: ClerkMiddleware = withLogger('clerkMiddleware', lo let handlerResult: Response = NextResponse.next(); try { - handlerResult = - (await clerkMiddlewareRequestDataStore.run( - options, - async () => await handler?.(() => authObjWithMethods, request, event), - )) || handlerResult; + const userHandlerResult = await clerkMiddlewareRequestDataStore.run(options, async () => + handler?.(() => authObjWithMethods, request, event), + ); + handlerResult = userHandlerResult || handlerResult; } catch (e: any) { handlerResult = handleControlFlowErrors(e, clerkRequest, requestState); } From db5fc1ebc962852d0d27f74898ffbb8f98e9e304 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:26:27 -0300 Subject: [PATCH 38/39] Re-throw errors related to static generation bail-out --- .../nextjs/src/app-router/server/utils.ts | 2 +- packages/nextjs/src/server/clerkClient.ts | 21 +++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/nextjs/src/app-router/server/utils.ts b/packages/nextjs/src/app-router/server/utils.ts index e8375c319bd..5d732c1b9e8 100644 --- a/packages/nextjs/src/app-router/server/utils.ts +++ b/packages/nextjs/src/app-router/server/utils.ts @@ -1,6 +1,6 @@ import { NextRequest } from 'next/server'; -const isPrerenderingBailout = (e: unknown) => { +export const isPrerenderingBailout = (e: unknown) => { if (!(e instanceof Error) || !('message' in e)) { return false; } diff --git a/packages/nextjs/src/server/clerkClient.ts b/packages/nextjs/src/server/clerkClient.ts index 60a5e204739..86e7302b3b8 100644 --- a/packages/nextjs/src/server/clerkClient.ts +++ b/packages/nextjs/src/server/clerkClient.ts @@ -3,7 +3,7 @@ import { createClerkClient } from '@clerk/backend'; import { constants } from '@clerk/backend/internal'; import { deprecated } from '@clerk/shared/deprecated'; -import { buildRequestLike } from '../app-router/server/utils'; +import { buildRequestLike, isPrerenderingBailout } from '../app-router/server/utils'; import { clerkMiddlewareRequestDataStore } from './clerkMiddleware'; import { API_URL, @@ -51,21 +51,20 @@ const clerkClientSingleton = createClerkClient(clerkClientDefaultOptions); const clerkClientForRequest = () => { let requestData; - const middlewareStore = clerkMiddlewareRequestDataStore.getStore(); - if (Object.values(middlewareStore ?? {}).length) { - requestData = middlewareStore; - } else { - // When outside of middleware runtime, fallbacks to access request data from `NextRequest` + try { const request = buildRequestLike(); const encryptedRequestData = getHeader(request, constants.Headers.ClerkRequestData); requestData = decryptClerkRequestData(encryptedRequestData); + } catch (err) { + if (err && isPrerenderingBailout(err)) { + throw err; + } } - if (requestData?.secretKey || requestData?.publishableKey) { - return createClerkClientWithOptions({ - secretKey: requestData.secretKey, - publishableKey: requestData.publishableKey, - }); + // Fallbacks between options from `NextRequest` on application server and middleware runtime store + const options = Object.values(requestData ?? {}).length ? requestData : clerkMiddlewareRequestDataStore.getStore(); + if (options?.secretKey || options?.publishableKey) { + return createClerkClientWithOptions(options); } return clerkClientSingleton; From c458ab75a574f1403dc4d82267c6ab50a8c3e5be Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Mon, 1 Jul 2024 18:27:11 -0300 Subject: [PATCH 39/39] Remove `@clerk/shared` from changeset --- .changeset/young-pigs-live.md | 1 - packages/nextjs/src/server/clerkClient.ts | 4 ++-- packages/nextjs/src/server/utils.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.changeset/young-pigs-live.md b/.changeset/young-pigs-live.md index 1484035e8eb..5e4ce82895c 100644 --- a/.changeset/young-pigs-live.md +++ b/.changeset/young-pigs-live.md @@ -1,7 +1,6 @@ --- '@clerk/backend': minor '@clerk/nextjs': minor -'@clerk/shared': minor --- Introduces dynamic keys from `clerkMiddleware`, allowing access by server-side helpers like `auth`. Keys such as `signUpUrl`, `signInUrl`, `publishableKey` and `secretKey` are securely encrypted using AES algorithm. diff --git a/packages/nextjs/src/server/clerkClient.ts b/packages/nextjs/src/server/clerkClient.ts index 86e7302b3b8..62d7a19cfd5 100644 --- a/packages/nextjs/src/server/clerkClient.ts +++ b/packages/nextjs/src/server/clerkClient.ts @@ -61,8 +61,8 @@ const clerkClientForRequest = () => { } } - // Fallbacks between options from `NextRequest` on application server and middleware runtime store - const options = Object.values(requestData ?? {}).length ? requestData : clerkMiddlewareRequestDataStore.getStore(); + // Fallbacks between options from middleware runtime and `NextRequest` from application server + const options = clerkMiddlewareRequestDataStore.getStore() ?? requestData; if (options?.secretKey || options?.publishableKey) { return createClerkClientWithOptions(options); } diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index 43c961f04f2..8ec537538b6 100644 --- a/packages/nextjs/src/server/utils.ts +++ b/packages/nextjs/src/server/utils.ts @@ -262,7 +262,7 @@ export function encryptClerkRequestData(requestData?: Partial errorThrower.throwMissingSecretKeyError()), + ENCRYPTION_KEY || assertKey(SECRET_KEY, () => errorThrower.throwMissingSecretKeyError()), ).toString(); }