diff --git a/.changeset/keyless-bootstrap-state-deprecated.md b/.changeset/keyless-bootstrap-state-deprecated.md new file mode 100644 index 00000000000..40c2859a47b --- /dev/null +++ b/.changeset/keyless-bootstrap-state-deprecated.md @@ -0,0 +1,5 @@ +--- +'@clerk/backend': patch +--- + +Mark the internal `createBootstrapSignedOutState` as deprecated. It is no longer used by `@clerk/nextjs` and is kept only for older published SDK versions. diff --git a/.changeset/keyless-cli-init-error.md b/.changeset/keyless-cli-init-error.md new file mode 100644 index 00000000000..14266760846 --- /dev/null +++ b/.changeset/keyless-cli-init-error.md @@ -0,0 +1,5 @@ +--- +'@clerk/nextjs': minor +--- + +In development, missing Clerk keys no longer activate keyless mode. When `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` are not set, the SDK now throws an error directing you to run `npx clerk@latest init`, which provisions a Clerk application and writes the keys to `.env.local`. Keyless credentials stored in the development keyless cookie are no longer read. Existing apps with configured or claimed keys are unaffected. diff --git a/integration/tests/next-middleware-keyless.test.ts b/integration/tests/next-middleware-keyless.test.ts index fa9b2c29df9..258ec4babd8 100644 --- a/integration/tests/next-middleware-keyless.test.ts +++ b/integration/tests/next-middleware-keyless.test.ts @@ -27,9 +27,11 @@ test.describe('Keyless mode | middleware authorization @nextjs', () => { await app.teardown(); }); - test('auth.protect() in middleware redirects to sign-in during keyless bootstrap', async ({ page }) => { - await page.goto(`${app.serverUrl}/protected`); - await page.waitForURL(/\/sign-in/); - await expect(page.getByTestId('protected')).not.toBeVisible(); + test('requests without keys fail with the missing env vars error instead of keyless bootstrap', async ({ page }) => { + const response = await page.goto(`${app.serverUrl}/protected`); + expect(response?.status()).toBe(500); + const content = await page.content(); + expect(content).toContain('Missing environment variables'); + expect(content).toContain('npx clerk@latest init'); }); }); diff --git a/integration/tests/next-quickstart-keyless.test.ts b/integration/tests/next-quickstart-keyless.test.ts index c57a5d805b3..319b8d0ea58 100644 --- a/integration/tests/next-quickstart-keyless.test.ts +++ b/integration/tests/next-quickstart-keyless.test.ts @@ -1,9 +1,12 @@ +import * as path from 'node:path'; + import { expect, test } from '@playwright/test'; import type { Application } from '../models/application'; import { appConfigs } from '../presets'; +import { fs } from '../scripts'; import { createTestUtils } from '../testUtils'; -import { mockClaimedInstanceEnvironmentCall, testToggleCollapsePopoverAndClaim } from '../testUtils/keylessHelpers'; +import { mockClaimedInstanceEnvironmentCall } from '../testUtils/keylessHelpers'; const commonSetup = appConfigs.next.appRouterQuickstart.clone(); @@ -17,15 +20,11 @@ test.describe('Keyless mode @quickstart', () => { }); let app: Application; - let dashboardUrl = 'https://dashboard.clerk.com/'; test.beforeAll(async () => { app = await commonSetup.commit(); await app.setup(); await app.withEnv(appConfigs.envs.withKeyless); - if (appConfigs.envs.withKeyless.privateVariables.get('CLERK_API_URL')?.includes('clerkstage')) { - dashboardUrl = 'https://dashboard.clerkstage.dev/'; - } await app.dev(); }); @@ -33,71 +32,44 @@ test.describe('Keyless mode @quickstart', () => { await app.teardown(); }); - test('Navigates to non-existent page (/_not-found) without a infinite redirect loop.', async ({ page, context }) => { - const u = createTestUtils({ app, page, context }); - await u.page.goToAppHome(); - await u.page.waitForClerkJsLoaded(); - await u.po.expect.toBeSignedOut(); - - await u.po.keylessPopover.waitForMounted(); - - const redirectMap = new Map(); - page.on('request', request => { - // Only count GET requests since Next.js server actions are sent with POST requests. - if (request.method() === 'GET') { - const url = request.url(); - redirectMap.set(url, (redirectMap.get(url) || 0) + 1); - expect(redirectMap.get(url)).toBeLessThanOrEqual(1); - } - }); - - await u.page.goToRelative('/something'); - await u.page.waitForAppUrl('/something'); - }); - - test('Toggle collapse popover and claim.', async ({ page, context }) => { - await testToggleCollapsePopoverAndClaim({ page, context, app, dashboardUrl, framework: 'nextjs' }); - }); - - test('Lands on claimed application with missing explicit keys, expanded by default, click to get keys from dashboard.', async ({ + test('Without keys, the app fails with the missing env vars error instead of keyless bootstrap.', async ({ page, - context, }) => { - await mockClaimedInstanceEnvironmentCall(page); - const u = createTestUtils({ app, page, context }); - await u.page.goToAppHome(); - await u.page.waitForClerkJsLoaded(); - - await u.po.keylessPopover.waitForMounted(); - expect(await u.po.keylessPopover.isExpanded()).toBe(true); - await expect(u.po.keylessPopover.promptToUseClaimedKeys()).toBeVisible(); - - const href = await u.po.keylessPopover.promptToUseClaimedKeys().getAttribute('href'); - expect(href).toBeTruthy(); - expect(href).toContain(dashboardUrl); + const response = await page.goto(`${app.serverUrl}/`); + expect(response?.status()).toBe(500); + const content = await page.content(); + expect(content).toContain('Missing environment variables'); + expect(content).toContain('npx clerk@latest init'); }); - test('Claimed application with keys inside .env, on dismiss, keyless prompt is removed.', async ({ + test('Claimed application with keys inside .env mounts the keyless prompt; on dismiss, it is removed.', async ({ page, context, }) => { - await mockClaimedInstanceEnvironmentCall(page); - const u = createTestUtils({ app, page, context }); - await u.page.goToAppHome(); - - await u.po.keylessPopover.waitForMounted(); - await expect(await u.po.keylessPopover.promptToUseClaimedKeys()).toBeVisible(); - /** - * Copy keys from `.clerk/.tmp/keyless.json to `.env` + * Seed claimed keyless state directly: the SDK no longer mints keys, so write the + * keys fixture to `.clerk/.tmp/keyless.json` and copy the matching keys into `.env`. */ + const publishableKey = appConfigs.envs.withEmailCodes.publicVariables.get('CLERK_PUBLISHABLE_KEY'); + const secretKey = appConfigs.envs.withEmailCodes.privateVariables.get('CLERK_SECRET_KEY'); + await fs.ensureDir(path.join(app.appDir, '.clerk', '.tmp')); + await fs.writeJSON(path.join(app.appDir, '.clerk', '.tmp', 'keyless.json'), { + publishableKey, + secretKey, + claimUrl: 'https://dashboard.clerk.com/apps/claim', + apiKeysUrl: 'https://dashboard.clerk.com/last-active?path=api-keys', + }); await app.keylessToEnv(); /** * wait a bit for the server to load the new env file */ await page.waitForTimeout(5_000); - await page.reload(); + await mockClaimedInstanceEnvironmentCall(page); + const u = createTestUtils({ app, page, context }); + await u.page.goToAppHome(); + await u.page.waitForClerkJsLoaded(); + await u.po.keylessPopover.waitForMounted(); await u.po.keylessPopover.promptToDismiss().click(); diff --git a/packages/backend/src/tokens/authStatus.ts b/packages/backend/src/tokens/authStatus.ts index 421c7bd61f4..358b29e0b69 100644 --- a/packages/backend/src/tokens/authStatus.ts +++ b/packages/backend/src/tokens/authStatus.ts @@ -291,6 +291,8 @@ type BootstrapSignedOutParams = { * `isSatellite` / `domain` / `proxyUrl` are carried through so that cross-origin * satellite redirects produced by `createRedirect` include the `__clerk_status=needs-sync` * marker required for the return-trip handshake. + * + * @deprecated No longer used by `@clerk/nextjs`; kept for older published SDK versions. Remove in the next major. */ export function createBootstrapSignedOutState({ signInUrl = '', diff --git a/packages/nextjs/src/app-router/client/ClerkProvider.tsx b/packages/nextjs/src/app-router/client/ClerkProvider.tsx index 109e1a38c87..812e8dea088 100644 --- a/packages/nextjs/src/app-router/client/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/client/ClerkProvider.tsx @@ -1,12 +1,12 @@ 'use client'; import { InternalClerkProvider as ReactClerkProvider, type Ui } from '@clerk/react/internal'; import { InitialStateProvider } from '@clerk/shared/react'; -import dynamic from 'next/dynamic'; import { useRouter } from 'next/navigation'; import React from 'react'; import { useSafeLayoutEffect } from '../../client-boundary/hooks/useSafeLayoutEffect'; import { ClerkNextOptionsProvider, useClerkNextOptions } from '../../client-boundary/NextOptionsContext'; +import { keylessMissingEnvVars } from '../../server/errors'; import type { NextClerkProviderProps } from '../../types'; import { canUseKeyless } from '../../utils/feature-flags'; import { mergeNextClerkPropsWithEnv } from '../../utils/mergeNextClerkPropsWithEnv'; @@ -16,14 +16,6 @@ import { ClerkScripts } from './ClerkScripts'; import { useAwaitablePush } from './useAwaitablePush'; import { useAwaitableReplace } from './useAwaitableReplace'; -/** - * LazyCreateKeylessApplication should only be loaded if the conditions below are met. - * Note: Using lazy() with Suspense instead of dynamic is not possible as React will throw a hydration error when `ClerkProvider` wraps `...` - */ -const LazyCreateKeylessApplication = dynamic(() => - import('./keyless-creator-reader.js').then(m => m.KeylessCreatorOrReader), -); - const NextClientClerkProvider = (props: NextClerkProviderProps) => { const { __internal_invokeMiddlewareOnAuthStateChange = true, __internal_scriptsSlot, children } = props; const router = useRouter(); @@ -115,9 +107,5 @@ export const ClientClerkProvider = ( return {children}; } - return ( - - {children} - - ); + throw new Error(keylessMissingEnvVars); }; diff --git a/packages/nextjs/src/app-router/client/keyless-cookie-sync.tsx b/packages/nextjs/src/app-router/client/keyless-cookie-sync.tsx deleted file mode 100644 index bf250d1cd39..00000000000 --- a/packages/nextjs/src/app-router/client/keyless-cookie-sync.tsx +++ /dev/null @@ -1,27 +0,0 @@ -'use client'; - -import type { AccountlessApplication } from '@clerk/backend'; -import { useSelectedLayoutSegments } from 'next/navigation'; -import type { PropsWithChildren } from 'react'; -import { useEffect } from 'react'; - -import { canUseKeyless } from '../../utils/feature-flags'; - -export function KeylessCookieSync(props: PropsWithChildren) { - const segments = useSelectedLayoutSegments(); - const isNotFoundRoute = segments[0]?.startsWith('/_not-found') || false; - - useEffect(() => { - if (canUseKeyless && !isNotFoundRoute) { - void import('../keyless-actions.js').then(m => - m.syncKeylessConfigAction({ - ...props, - // Preserve the current url and return back, once keys are synced in the middleware - returnUrl: window.location.href, - }), - ); - } - }, [isNotFoundRoute]); - - return props.children; -} diff --git a/packages/nextjs/src/app-router/client/keyless-creator-reader.tsx b/packages/nextjs/src/app-router/client/keyless-creator-reader.tsx deleted file mode 100644 index 7d481b744fa..00000000000 --- a/packages/nextjs/src/app-router/client/keyless-creator-reader.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { useSelectedLayoutSegments } from 'next/navigation'; -import React, { useEffect } from 'react'; - -import type { NextClerkProviderProps } from '../../types'; -import { createOrReadKeylessAction } from '../keyless-actions'; - -export const KeylessCreatorOrReader = (props: NextClerkProviderProps) => { - const { children } = props; - const segments = useSelectedLayoutSegments(); - const isNotFoundRoute = segments[0]?.startsWith('/_not-found') || false; - const [state, fetchKeys] = React.useActionState(createOrReadKeylessAction, null); - useEffect(() => { - if (isNotFoundRoute) { - return; - } - React.startTransition(() => { - fetchKeys(); - }); - }, [isNotFoundRoute]); - - if (!React.isValidElement(children)) { - return children; - } - - return React.cloneElement(children, { - key: state?.publishableKey, - publishableKey: state?.publishableKey, - __internal_keyless_claimKeylessApplicationUrl: state?.claimUrl, - __internal_keyless_copyInstanceKeysUrl: state?.apiKeysUrl, - __internal_bypassMissingPublishableKey: true, - } as any); -}; diff --git a/packages/nextjs/src/app-router/keyless-actions.ts b/packages/nextjs/src/app-router/keyless-actions.ts index 209f91226d8..02de03adb11 100644 --- a/packages/nextjs/src/app-router/keyless-actions.ts +++ b/packages/nextjs/src/app-router/keyless-actions.ts @@ -1,93 +1,8 @@ 'use server'; -import type { AccountlessApplication } from '@clerk/backend'; -import { cookies, headers } from 'next/headers'; -import { redirect, RedirectType } from 'next/navigation'; -import { errorThrower } from '../server/errorThrower'; -import { detectClerkMiddleware } from '../server/headers-utils'; -import { getKeylessCookieName, getKeylessCookieValue } from '../server/keyless'; -import { clerkDevelopmentCache, createKeylessModeMessage } from '../server/keyless-log-cache'; import { keyless } from '../server/keyless-node'; import { canUseKeyless } from '../utils/feature-flags'; -type SetCookieOptions = Parameters>['set']>[2]; - -const keylessCookieConfig = { - secure: false, - httpOnly: false, - sameSite: 'lax', -} satisfies SetCookieOptions; - -export async function syncKeylessConfigAction(args: AccountlessApplication & { returnUrl: string }): Promise { - const { claimUrl, publishableKey, secretKey, returnUrl } = args; - const cookieStore = await cookies(); - const request = new Request('https://placeholder.com', { headers: await headers() }); - - const keylessCookie = await getKeylessCookieValue(name => cookieStore.get(name)?.value); - const pksMatch = keylessCookie?.publishableKey === publishableKey; - const sksMatch = keylessCookie?.secretKey === secretKey; - if (pksMatch && sksMatch) { - // Return early, syncing in not needed. - return; - } - - // Set the new keys in the cookie. - cookieStore.set( - await getKeylessCookieName(), - JSON.stringify({ claimUrl, publishableKey, secretKey }), - keylessCookieConfig, - ); - - // Request works at runtime since detectClerkMiddleware checks for Request via isRequestWebAPI - if (detectClerkMiddleware(request as Parameters[0])) { - /** - * Force middleware to execute to read the new keys from the cookies and populate the authentication state correctly. - */ - redirect(`/clerk-sync-keyless?returnUrl=${returnUrl}`, RedirectType.replace); - } - - return; -} - -export async function createOrReadKeylessAction(): Promise> { - if (!canUseKeyless) { - return null; - } - - let result; - try { - result = await keyless().getOrCreateKeys(); - } catch { - result = null; - } - - if (!result) { - errorThrower.throwMissingPublishableKeyError(); - return null; - } - - /** - * Notify developers. - */ - clerkDevelopmentCache?.log({ - cacheKey: result.publishableKey, - msg: createKeylessModeMessage(result), - }); - - const { claimUrl, publishableKey, secretKey, apiKeysUrl } = result; - void (await cookies()).set( - await getKeylessCookieName(), - JSON.stringify({ claimUrl, publishableKey, secretKey }), - keylessCookieConfig, - ); - - return { - claimUrl, - publishableKey, - apiKeysUrl, - }; -} - export async function deleteKeylessAction() { if (!canUseKeyless) { return; diff --git a/packages/nextjs/src/app-router/server/ClerkProvider.tsx b/packages/nextjs/src/app-router/server/ClerkProvider.tsx index 9e454b83b4e..810d1215898 100644 --- a/packages/nextjs/src/app-router/server/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/server/ClerkProvider.tsx @@ -3,6 +3,7 @@ import type { InitialState, Without } from '@clerk/shared/types'; import React, { Suspense } from 'react'; import { getDynamicAuthData } from '../../server/buildClerkProps'; +import { keylessMissingEnvVars } from '../../server/errors'; import type { NextClerkProviderProps } from '../../types'; import { mergeNextClerkPropsWithEnv } from '../../utils/mergeNextClerkPropsWithEnv'; import { ClientClerkProvider } from '../client/ClerkProvider'; @@ -52,6 +53,9 @@ export async function ClerkProvider( ) : undefined; if (shouldRunAsKeyless) { + if (!propsWithEnvs.publishableKey) { + throw new Error(keylessMissingEnvVars); + } return ( { .then(mod => mod.keyless().getOrCreateKeys()) .catch(() => null); - const { clerkDevelopmentCache, createConfirmationMessage, createKeylessModeMessage } = - await import('../../server/keyless-log-cache.js'); + const { clerkDevelopmentCache, createConfirmationMessage } = await import('../../server/keyless-log-cache.js'); if (!newOrReadKeys) { // When case keyless should run, but keys are not available, then fallback to throwing for missing keys @@ -98,31 +95,7 @@ export const KeylessProvider = async (props: KeylessProviderProps) => { cacheKey: `${newOrReadKeys.publishableKey}_claimed`, msg: createConfirmationMessage(), }); - - return clientProvider; - } - - const KeylessCookieSync = await import('../client/keyless-cookie-sync.js').then(mod => mod.KeylessCookieSync); - - const headerStore = await headers(); - /** - * Allow developer to return to local application after claiming - */ - const host = headerStore.get('x-forwarded-host'); - const proto = headerStore.get('x-forwarded-proto'); - - const claimUrl = new URL(newOrReadKeys.claimUrl); - if (host && proto) { - onlyTry(() => claimUrl.searchParams.set('return_url', new URL(`${proto}://${host}`).href)); } - /** - * Notify developers. - */ - clerkDevelopmentCache?.log({ - cacheKey: newOrReadKeys.publishableKey, - msg: createKeylessModeMessage({ ...newOrReadKeys, claimUrl: claimUrl.href }), - }); - - return {clientProvider}; + return clientProvider; }; diff --git a/packages/nextjs/src/server/__tests__/clerkMiddlewareKeyless.test.ts b/packages/nextjs/src/server/__tests__/clerkMiddlewareKeyless.test.ts new file mode 100644 index 00000000000..cce1ef319b7 --- /dev/null +++ b/packages/nextjs/src/server/__tests__/clerkMiddlewareKeyless.test.ts @@ -0,0 +1,60 @@ +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: unlike clerkMiddleware.test.ts, keys are empty so the +// keyless env error path is reachable. +vi.mock(import('../constants.js'), async importOriginal => { + const actual = await importOriginal(); + return { + ...actual, + PUBLISHABLE_KEY: '', + SECRET_KEY: '', + }; +}); + +describe('clerkMiddleware when Clerk env vars are missing', () => { + beforeEach(() => { + vi.stubEnv('NODE_ENV', 'development'); + vi.stubEnv('NEXT_PUBLIC_CLERK_KEYLESS_DISABLED', undefined); + automatedEnvironmentVariables.forEach(name => { + vi.stubEnv(name, undefined); + vi.stubGlobal(name, undefined); + }); + vi.resetModules(); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + vi.unstubAllGlobals(); + vi.resetModules(); + }); + + const runMiddleware = async (headers?: Record) => { + const { clerkMiddleware } = await import('../clerkMiddleware.js'); + const request = new NextRequest('https://example.com/protected', { headers }); + return clerkMiddleware()(request, {} as NextFetchEvent); + }; + + it('throws the setup error pointing at the CLI', async () => { + await expect(runMiddleware()).rejects.toThrow(/npx clerk@latest init/); + await expect(runMiddleware()).rejects.toThrow(/\(code=missing_env_keys\)/); + }); + + it('throws the same setup error for machine-token requests', async () => { + await expect(runMiddleware({ authorization: 'Bearer mt_xxxxxxxx' })).rejects.toThrow(/npx clerk@latest init/); + }); + + it('falls back to the standard missing-key error when keyless is unavailable', async () => { + vi.stubEnv('NODE_ENV', 'production'); + await expect(runMiddleware()).rejects.toThrow(/publishableKey/i); + }); + + it('names both env vars and the CLI command in the message', async () => { + const { keylessMissingEnvVars } = await import('../errors.js'); + expect(keylessMissingEnvVars).toContain('NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY'); + expect(keylessMissingEnvVars).toContain('CLERK_SECRET_KEY'); + expect(keylessMissingEnvVars).toContain('npx clerk@latest init'); + }); +}); diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index 9e6699de60d..182617648e6 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -1,4 +1,4 @@ -import type { AccountlessApplication, AuthObject, ClerkClient } from '@clerk/backend'; +import type { AuthObject, ClerkClient } from '@clerk/backend'; import type { AuthenticatedState, AuthenticateRequestOptions, @@ -12,11 +12,9 @@ import type { import { AuthStatus, constants, - createBootstrapSignedOutState, createClerkRequest, createRedirect, getAuthObjectForAcceptedToken, - isMachineTokenByPrefix, isTokenTypeAccepted, makeAuthObjectSerializable, TokenType, @@ -39,9 +37,8 @@ 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 } from './errors'; import { errorThrower } from './errorThrower'; -import { getHeader } from './headers-utils'; -import { getKeylessCookieValue } from './keyless'; import { clerkMiddlewareRequestDataStorage, clerkMiddlewareRequestDataStore } from './middleware-storage'; import { isNextjsNotFoundError, @@ -151,14 +148,11 @@ export const clerkMiddleware = ((...args: unknown[]): NextMiddleware | NextMiddl // Handles the case where `options` is a callback function to dynamically access `NextRequest` const resolvedParams = typeof params === 'function' ? await params(request) : params; - const keyless = await getKeylessCookieValue(name => request.cookies.get(name)?.value); - - const publishableKey = assertKey( - resolvedParams.publishableKey || PUBLISHABLE_KEY || keyless?.publishableKey, - () => errorThrower.throwMissingPublishableKeyError(), + const publishableKey = assertKey(resolvedParams.publishableKey || PUBLISHABLE_KEY, () => + errorThrower.throwMissingPublishableKeyError(), ); - const secretKey = assertKey(resolvedParams.secretKey || SECRET_KEY || keyless?.secretKey, () => + const secretKey = assertKey(resolvedParams.secretKey || SECRET_KEY, () => errorThrower.throwMissingSecretKeyError(), ); @@ -240,71 +234,17 @@ export const clerkMiddleware = ((...args: unknown[]): NextMiddleware | NextMiddl handler, options, resolvedParams, - keyless, - logger, - }); - }); - - /** - * Runs the user's handler against a synthetic signed-out `RequestState` during the keyless - * bootstrap window, so authorization fails closed until a publishable key is provisioned. - */ - const bootstrapNextMiddleware: NextMiddleware = withLogger('clerkMiddleware', logger => async (request, event) => { - const resolvedParams = typeof params === 'function' ? await params(request) : params; - const keyless = await getKeylessCookieValue(name => request.cookies.get(name)?.value); - - const signInUrl = resolvedParams.signInUrl || SIGN_IN_URL || ''; - const signUpUrl = resolvedParams.signUpUrl || SIGN_UP_URL || ''; - - const options = { - publishableKey: '', - secretKey: '', - signInUrl, - signUpUrl, - ...resolvedParams, - }; - - clerkMiddlewareRequestDataStore.set('requestData', options); - - if (options.debug) { - logger.enable(); - } - - const clerkRequest = createClerkRequest(request); - logger.debug('keyless bootstrap (no publishable key)', () => ({ signInUrl, signUpUrl })); - logger.debug('url', () => clerkRequest.toJSON()); - - const requestState = createBootstrapSignedOutState({ signInUrl, signUpUrl }); - - return runHandlerWithRequestState({ - clerkRequest, - request, - event, - requestState, - handler, - options, - resolvedParams, - keyless, logger, }); }); const keylessMiddleware: NextMiddleware = async (request, event) => { - /** - * This mechanism replaces a full-page reload. Ensures that middleware will re-run and authenticate the request properly without the secret key or publishable key to be missing. - */ - if (isKeylessSyncRequest(request)) { - return returnBackFromKeylessSync(request); - } - const resolvedParams = typeof params === 'function' ? await params(request) : params; - const keyless = await getKeylessCookieValue(name => request.cookies.get(name)?.value); - const isMissingPublishableKey = !(resolvedParams.publishableKey || PUBLISHABLE_KEY || keyless?.publishableKey); - const authHeader = getHeader(request, constants.Headers.Authorization)?.replace('Bearer ', '') ?? ''; + const isMissingPublishableKey = !(resolvedParams.publishableKey || PUBLISHABLE_KEY); - if (isMissingPublishableKey && !isMachineTokenByPrefix(authHeader)) { - return bootstrapNextMiddleware(request, event); + if (isMissingPublishableKey) { + throw new Error(keylessMissingEnvVars); } return baseNextMiddleware(request, event); @@ -359,15 +299,12 @@ type RunHandlerWithRequestStateArgs = { signUpUrl: string; }; resolvedParams: ClerkMiddlewareOptions; - keyless: AccountlessApplication | undefined; logger: LoggerNoCommit; }; /** * Drives the post-authentication pipeline: handler invocation, CSP, redirects, header propagation, - * and response decoration. Accepts a pre-computed `requestState` so callers can supply either a - * real authentication result from `authenticateRequest()` or a synthetic signed-out state - * (e.g. during keyless bootstrap when no publishable key is available yet). + * and response decoration. */ async function runHandlerWithRequestState({ clerkRequest, @@ -377,10 +314,9 @@ async function runHandlerWithRequestState({ handler, options, resolvedParams, - keyless, logger, }: RunHandlerWithRequestStateArgs): Promise { - const { publishableKey, secretKey } = options; + const { publishableKey } = options; logger.debug('requestState', () => ({ status: requestState.status, @@ -470,38 +406,18 @@ async function runHandlerWithRequestState({ setRequestHeadersOnNextResponse(handlerResult, clerkRequest, { [constants.Headers.EnableDebug]: 'true' }); } - const keylessKeysForRequestData = - // Only pass keyless credentials when there are no explicit keys - secretKey === keyless?.secretKey - ? { - publishableKey: keyless?.publishableKey, - secretKey: keyless?.secretKey, - } - : {}; - decorateRequest( clerkRequest, handlerResult, requestState, resolvedParams, - keylessKeysForRequestData, + {}, authObject.tokenType === 'session_token' ? null : makeAuthObjectSerializable(authObject), ); return handlerResult; } -const isKeylessSyncRequest = (request: NextMiddlewareRequestParam) => - request.nextUrl.pathname === '/clerk-sync-keyless'; - -const returnBackFromKeylessSync = (request: NextMiddlewareRequestParam) => { - const returnUrl = request.nextUrl.searchParams.get('returnUrl'); - const url = new URL(request.url); - url.pathname = ''; - - return NextResponse.redirect(returnUrl || url.toString()); -}; - type AuthenticateRequest = Pick['authenticateRequest']; export const createAuthenticateRequestOptions = ( diff --git a/packages/nextjs/src/server/errors.ts b/packages/nextjs/src/server/errors.ts index 07eb7efdd04..4b2d3e375d2 100644 --- a/packages/nextjs/src/server/errors.ts +++ b/packages/nextjs/src/server/errors.ts @@ -34,6 +34,14 @@ For more details, see https://clerk.com/err/auth-middleware `; }; +export const keylessMissingEnvVars = `Clerk: Missing environment variables (NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY). + +To set up Clerk for this project, 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=missing_env_keys)`; + 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)`;