Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
877b55b
Require signing key to be defined when using runtime options
LauraBeatris Jun 5, 2024
04a4ceb
Add constant definition for `x-clerk-data` header
LauraBeatris Jun 5, 2024
604d08d
Add utilities to encrypt/decrypt request data
LauraBeatris Jun 5, 2024
b83f919
Use propogated secret key
LauraBeatris Jun 5, 2024
1dc02f7
Assert against signing key value
LauraBeatris Jun 6, 2024
a7cd636
Fix CryptoJS import statement
LauraBeatris Jun 6, 2024
b0954f9
Handle error when signing key is invalid
LauraBeatris Jun 7, 2024
614fac5
Rename signing key to encryption key
LauraBeatris Jun 7, 2024
5e0eff1
Add test to assert against propagation
LauraBeatris Jun 7, 2024
977bc38
Propagate `signInUrl` and `signUpUrl`
LauraBeatris Jun 7, 2024
6111133
Do not allow to pass `signInUrl` and `signUpUrl` from ClerkProvider
LauraBeatris Jun 7, 2024
c0f37a8
Restructure test block and add todo
LauraBeatris Jun 12, 2024
bc510b8
Rollback props changes on `ClerkProvider`
LauraBeatris Jun 12, 2024
d02913e
Introduce fallback for encryption key
LauraBeatris Jun 12, 2024
3e5758b
Implement test case for `signInUrl` option
LauraBeatris Jun 12, 2024
919ec6e
Add `CLERK_ENCRYPTION_KEY` to integration tests
LauraBeatris Jun 13, 2024
73942bd
Access redirect URLs from middleware options
LauraBeatris Jun 13, 2024
853d90e
Add changeset
LauraBeatris Jun 13, 2024
c1856cb
Mock `ENCRYPTION_KEY` on `authMiddleware` unit tests
LauraBeatris Jun 14, 2024
0be0727
Update snapshot for request headers
LauraBeatris Jun 14, 2024
f520025
Warn when encryption key is missing instead of throwing
LauraBeatris Jun 14, 2024
8059461
Reference to docs on error message and changeset
LauraBeatris Jun 14, 2024
4deca28
Add todo comment for next major version
LauraBeatris Jun 14, 2024
aa532db
Propagate secret key to backend client
LauraBeatris Jun 14, 2024
7109105
Update changeset
LauraBeatris Jun 14, 2024
6ee667b
Propagate `publishableKey` to `auth`
LauraBeatris Jun 18, 2024
b618edf
Update changeset to mention `Dynamic Keys`
LauraBeatris Jun 18, 2024
d8cc06e
Update encryption key env var for integration tests
LauraBeatris Jun 18, 2024
7d462a6
Add `CLERK_ENCRYPTION_KEY` to CI
LauraBeatris Jun 18, 2024
f7df0ee
feat(nextjs): Read dynamic keys from `clerkClient` within middleware …
LauraBeatris Jun 27, 2024
f3d83c8
Rollback changes on `authMiddleware`
LauraBeatris Jun 27, 2024
25eaaf2
Fix unit tests and Proxy
LauraBeatris Jun 28, 2024
e2b4078
Mention `clerkClient` on changeset
LauraBeatris Jun 28, 2024
89a8a4c
Fallback to ALS on `headers` error
LauraBeatris Jun 28, 2024
b76e2bb
Update changeset
LauraBeatris Jul 1, 2024
ed3aa79
Fix error formatting
LauraBeatris Jul 1, 2024
7170e31
Extract middleware handler to separate variable
LauraBeatris Jul 1, 2024
db5fc1e
Re-throw errors related to static generation bail-out
LauraBeatris Jul 1, 2024
c458ab7
Remove `@clerk/shared` from changeset
LauraBeatris Jul 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/young-pigs-live.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@clerk/backend': minor
'@clerk/nextjs': 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.
Comment thread
LauraBeatris marked this conversation as resolved.

- 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 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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}

Expand Down
5 changes: 5 additions & 0 deletions integration/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ export const constants = {
* The version of the dependency to use, controlled programmatically.
*/
E2E_CLERK_VERSION: process.env.E2E_CLERK_VERSION,
/**
* Key used to encrypt request data for Next.js dynamic keys.
* @ref https://clerk.com/docs/references/nextjs/clerk-middleware#dynamic-keys
*/
E2E_CLERK_ENCRYPTION_KEY: process.env.CLERK_ENCRYPTION_KEY,
/**
* PK and SK pairs from the env to use for integration tests.
*/
Expand Down
10 changes: 9 additions & 1 deletion integration/presets/envs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -81,6 +82,12 @@ const withAPCore2ClerkV4 = environmentConfig()
.setEnvVariable('private', 'CLERK_SECRET_KEY', envKeys['core-2-all-enabled'].sk)
.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);

export const envs = {
withEmailCodes,
withEmailLinks,
Expand All @@ -90,4 +97,5 @@ export const envs = {
withAPCore1ClerkV4,
withAPCore2ClerkLatest,
withAPCore2ClerkV4,
withDynamicKeys,
} as const;
101 changes: 101 additions & 0 deletions integration/tests/dynamic-keys.test.ts
Original file line number Diff line number Diff line change
@@ -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 <p>Users count: {count}</p>
}
`,
)
.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();
});
});
2 changes: 1 addition & 1 deletion integration/tests/handshake.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const Headers = {
AuthMessage: 'x-clerk-auth-message',
ClerkUrl: 'x-clerk-clerk-url',
EnableDebug: 'x-clerk-debug',
ClerkRequestData: 'x-clerk-request-data',
ClerkRedirectTo: 'x-clerk-redirect-to',
CloudFrontForwardedProto: 'cloudfront-forwarded-proto',
Authorization: 'authorization',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 7 additions & 6 deletions packages/nextjs/src/app-router/server/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReturnType<typeof redirect>> };
Expand All @@ -28,15 +28,16 @@ export const auth = (): Auth => {
clerkRequest.clerkUrl.searchParams.get(constants.QueryParameters.DevBrowser) ||
clerkRequest.cookies.get(constants.Cookies.DevBrowser);

const encryptedRequestData = getHeader(request, constants.Headers.ClerkRequestData);
const decryptedRequestData = decryptClerkRequestData(encryptedRequestData);

return createRedirect({
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,
signInUrl: SIGN_IN_URL,
signUpUrl: SIGN_UP_URL,
publishableKey: decryptedRequestData.publishableKey || PUBLISHABLE_KEY,
signInUrl: decryptedRequestData.signInUrl || SIGN_IN_URL,
signUpUrl: decryptedRequestData.signUpUrl || SIGN_UP_URL,
}).redirectToSignIn({
returnBackUrl: opts.returnBackUrl === null ? '' : opts.returnBackUrl || clerkUrl?.toString(),
});
Expand Down
2 changes: 1 addition & 1 deletion packages/nextjs/src/app-router/server/currentUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ export async function currentUser(): Promise<User | null> {
return null;
}

return clerkClient.users.getUser(userId);
return clerkClient().users.getUser(userId);
}
2 changes: 1 addition & 1 deletion packages/nextjs/src/app-router/server/utils.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/nextjs/src/server/__tests__/clerkClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading