diff --git a/.changeset/invalid-key-cli-error.md b/.changeset/invalid-key-cli-error.md new file mode 100644 index 00000000000..b45981f8b61 --- /dev/null +++ b/.changeset/invalid-key-cli-error.md @@ -0,0 +1,6 @@ +--- +'@clerk/shared': patch +'@clerk/nextjs': patch +--- + +Invalid publishable key errors now recommend the Clerk CLI: the shared invalid-key message points at `npx clerk@latest init`, and `clerkMiddleware()` validates the publishable key format upfront, throwing a setup error in development (`npx clerk@latest init`) or a deploy-oriented error in production (`npx clerk@latest env pull --instance prod`) instead of failing later with `Publishable key not valid.` diff --git a/packages/nextjs/src/server/__tests__/clerkMiddlewareInvalidKeys.test.ts b/packages/nextjs/src/server/__tests__/clerkMiddlewareInvalidKeys.test.ts new file mode 100644 index 00000000000..993c2ed8c76 --- /dev/null +++ b/packages/nextjs/src/server/__tests__/clerkMiddlewareInvalidKeys.test.ts @@ -0,0 +1,59 @@ +import { automatedEnvironmentVariables } from '@clerk/shared/utils'; +import type { NextFetchEvent } from 'next/server'; +import { NextRequest } from 'next/server'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +// The mock SHOULD exist before the imports: keys are present but not parseable as Clerk keys, so +// the invalid-key error path is reachable. +vi.mock(import('../constants.js'), async importOriginal => { + const actual = await importOriginal(); + return { + ...actual, + PUBLISHABLE_KEY: 'pk_test_placeholder', + SECRET_KEY: 'sk_test_placeholder', + }; +}); + +describe('clerkMiddleware when Clerk env vars are invalid', () => { + beforeEach(() => { + vi.stubEnv('NODE_ENV', 'development'); + automatedEnvironmentVariables.forEach(name => { + vi.stubEnv(name, undefined); + vi.stubGlobal(name, undefined); + }); + vi.resetModules(); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + vi.unstubAllGlobals(); + vi.resetModules(); + }); + + const runMiddleware = async () => { + const { clerkMiddleware } = await import('../clerkMiddleware.js'); + const request = new NextRequest('https://example.com/protected'); + return clerkMiddleware()(request, {} as NextFetchEvent); + }; + + it('throws the invalid-key error pointing at the CLI', async () => { + await expect(runMiddleware()).rejects.toThrow(/npx clerk@latest init/); + await expect(runMiddleware()).rejects.toThrow(/\(code=invalid_env_keys\)/); + }); + + it('throws the production invalid-key error pointing at env pull', async () => { + vi.stubEnv('NODE_ENV', 'production'); + await expect(runMiddleware()).rejects.toThrow(/npx clerk@latest env pull --instance prod/); + await expect(runMiddleware()).rejects.toThrow(/\(code=invalid_env_keys_production\)/); + }); + + it('names the env var and the expected key format in the message', async () => { + const { invalidEnvKeys, productionInvalidEnvKeys } = await import('../errors.js'); + for (const message of [invalidEnvKeys, productionInvalidEnvKeys]) { + expect(message).toContain('NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY'); + expect(message).toContain('pk_test_'); + } + expect(invalidEnvKeys).toContain('npx clerk@latest init'); + expect(productionInvalidEnvKeys).toContain('npx clerk@latest env pull --instance prod'); + }); +}); diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index 866d4ea95e5..1d6d3b8f316 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -38,7 +38,7 @@ import { canUseKeyless } from '../utils/feature-flags'; import { clerkClient } from './clerkClient'; import { DOMAIN, PROXY_URL, PUBLISHABLE_KEY, SECRET_KEY, SIGN_IN_URL, SIGN_UP_URL } from './constants'; import { type ContentSecurityPolicyOptions, createContentSecurityPolicyHeaders } from './content-security-policy'; -import { keylessMissingEnvVars, productionMissingEnvVars } from './errors'; +import { invalidEnvKeys, keylessMissingEnvVars, productionInvalidEnvKeys, productionMissingEnvVars } from './errors'; import { errorThrower } from './errorThrower'; import { clerkMiddlewareRequestDataStorage, clerkMiddlewareRequestDataStore } from './middleware-storage'; import { @@ -163,6 +163,10 @@ export const clerkMiddleware = ((...args: unknown[]): NextMiddleware | NextMiddl throw new Error(productionMissingEnvVars); }); + if (!parsePublishableKey(publishableKey)) { + throw new Error(isDevelopmentEnvironment() ? invalidEnvKeys : productionInvalidEnvKeys); + } + // Handle Frontend API proxy requests early, before authentication const requestUrl = new URL(request.nextUrl.href); let frontendApiProxyConfig = resolvedParams.frontendApiProxy; diff --git a/packages/nextjs/src/server/errors.ts b/packages/nextjs/src/server/errors.ts index cdebd4702db..8e73ad23fd8 100644 --- a/packages/nextjs/src/server/errors.ts +++ b/packages/nextjs/src/server/errors.ts @@ -50,6 +50,22 @@ To deploy this application to production, run: This provisions a production Clerk instance and walks you through DNS and OAuth setup. If you already have a production instance, run \`npx clerk@latest env pull --instance prod\` to write its keys to your env file, and set the same keys in your deployment environment. (code=missing_env_keys_production)`; +export const invalidEnvKeys = `Clerk: Invalid environment keys. NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY is set but is not a valid Clerk publishable key (expected format: pk_test_... or pk_live_...). + +To set up Clerk for this project with valid keys, run: + + npx clerk@latest init + +The command is non-interactive and requires no Clerk account or login: it provisions temporary development keys automatically and writes them to your .env.local file. Then restart your dev server. Do not invent placeholder keys - they will fail. If you already have a Clerk application, copy its keys from https://dashboard.clerk.com into .env.local instead. (code=invalid_env_keys)`; + +export const productionInvalidEnvKeys = `Clerk: Invalid environment keys. NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY is set but is not a valid Clerk publishable key (expected format: pk_test_... or pk_live_...). + +To write the keys of an existing Clerk instance to your env file, run: + + npx clerk@latest env pull --instance prod + +Set the same keys in your deployment environment. If you do not have a production Clerk instance yet, run \`npx clerk@latest deploy\` to provision one. (code=invalid_env_keys_production)`; + 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/reference/nextjs/clerk-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. For more information, see: https://clerk.com/docs/reference/nextjs/clerk-middleware#dynamic-keys. (code=encryption_key_invalid)`; diff --git a/packages/shared/src/__tests__/error.spec.ts b/packages/shared/src/__tests__/error.spec.ts index 6c347f58994..17df7462f50 100644 --- a/packages/shared/src/__tests__/error.spec.ts +++ b/packages/shared/src/__tests__/error.spec.ts @@ -16,7 +16,7 @@ describe('ErrorThrower', () => { it('throws the correct error message and interpolates pkg and known parameters', () => { expect(() => errorThrower.throwInvalidPublishableKeyError({ key: 'whatever' })).toThrow( - '@clerk/test-package: The publishableKey passed to Clerk is invalid. You can get your Publishable key at https://dashboard.clerk.com/last-active?path=api-keys. (key=whatever)', + '@clerk/test-package: The publishableKey passed to Clerk is invalid (key=whatever). To create a Clerk application with valid keys, run:', ); }); diff --git a/packages/shared/src/errors/errorThrower.ts b/packages/shared/src/errors/errorThrower.ts index 1c2c7f5cf42..a6c23db0ce9 100644 --- a/packages/shared/src/errors/errorThrower.ts +++ b/packages/shared/src/errors/errorThrower.ts @@ -1,6 +1,10 @@ const DefaultMessages = Object.freeze({ InvalidProxyUrlErrorMessage: `The proxyUrl passed to Clerk is invalid. The expected value for proxyUrl is an absolute URL or a relative path with a leading '/'. (key={{url}})`, - InvalidPublishableKeyErrorMessage: `The publishableKey passed to Clerk is invalid. You can get your Publishable key at https://dashboard.clerk.com/last-active?path=api-keys. (key={{key}})`, + InvalidPublishableKeyErrorMessage: `The publishableKey passed to Clerk is invalid (key={{key}}). To create a Clerk application with valid keys, run: + + npx clerk@latest init + +Do not invent placeholder keys - they will fail. If you already have a Clerk application, copy its Publishable key from https://dashboard.clerk.com/last-active?path=api-keys instead.`, MissingPublishableKeyErrorMessage: `Missing publishableKey. To set up Clerk for this project, run: npx clerk@latest init