Skip to content

Commit b87f7b9

Browse files
dimklnikosdouvlis
andauthored
feat(backend,shared,clerk-js): Support suffixed cookies (#3506)
Co-authored-by: Nikos Douvlis <[email protected]>
1 parent bfdd9c1 commit b87f7b9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+1784
-223
lines changed

.changeset/calm-readers-call.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@clerk/clerk-js': minor
3+
'@clerk/backend': minor
4+
'@clerk/shared': minor
5+
---
6+
7+
Support reading / writing / removing suffixed/un-suffixed cookies from `@clerk/clerk-js` and `@clerk/backend`.
8+
The `__session`, `__clerk_db_jwt` and `__client_uat` cookies will now include a suffix derived from the instance's publishakeKey. The cookie name suffixes are used to prevent cookie collisions, effectively enabling support for multiple Clerk applications running on the same domain.

.github/actions/init/action.yml

+21-2
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,33 @@ runs:
106106
- name: Setup NodeJS ${{ inputs.node-version }}
107107
uses: actions/setup-node@v4
108108
with:
109-
cache: 'npm'
110109
node-version: ${{ inputs.node-version }}
111110
registry-url: ${{ inputs.registry-url }}
112111

112+
- name: NPM debug
113+
shell: bash
114+
run: npm config ls -l
115+
116+
- name: Restore node_modules
117+
uses: actions/cache/restore@v4
118+
id: cache-npm
119+
with:
120+
path: ./node_modules
121+
key: ${{ runner.os }}-node-${{ inputs.node-version }}-node-modules-${{ hashFiles('**/package-lock.json') }}-v6
122+
restore-keys: ${{ runner.os }}-node-${{ inputs.node-version }}-node-modules-
123+
113124
- name: Install NPM Dependencies
114-
run: mkdir node_modules && npm ci --cache /home/runner/.npm --audit=false --fund=false --prefer-offline
125+
# if: steps.cache-npm.outputs.cache-hit != 'true'
126+
run: npm ci --prefer-offline --audit=false --fund=false --verbose
115127
shell: bash
116128

129+
- name: Cache node_modules
130+
uses: actions/cache/save@v4
131+
if: steps.cache-npm.outputs.cache-hit != 'true'
132+
with:
133+
path: ./node_modules
134+
key: ${{ runner.os }}-node-${{ inputs.node-version }}-node-modules-${{ hashFiles('**/package-lock.json') }}-v6
135+
117136
- name: Get Playwright Version
118137
if: inputs.playwright-enabled == 'true'
119138
shell: bash

.github/workflows/ci.yml

+36-16
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,6 @@ jobs:
3737
turbo-team: ${{ vars.TURBO_TEAM }}
3838
turbo-token: ${{ secrets.TURBO_TOKEN }}
3939

40-
- name: List node_modules
41-
run: npm ls
42-
shell: bash
43-
4440
- name: Require Changeset
4541
if: ${{ !(github.event_name == 'merge_group') }}
4642
run: if [[ "${{ github.event.pull_request.user.login }}" = "clerk-cookie" || "${{ github.event.pull_request.user.login }}" = "renovate[bot]" ]]; then echo 'Skipping' && exit 0; else npx changeset status --since=origin/main; fi
@@ -140,7 +136,7 @@ jobs:
140136

141137
strategy:
142138
matrix:
143-
test-name: ['generic', 'express', 'quickstart', 'ap-flows', 'elements', 'astro']
139+
test-name: ['generic', 'express', 'quickstart', 'ap-flows', 'elements', 'astro', 'sessions']
144140
test-project: ['chrome']
145141
include:
146142
- test-name: 'nextjs'
@@ -157,6 +153,15 @@ jobs:
157153
fetch-depth: 0
158154
show-progress: false
159155

156+
- name: Setup
157+
id: config
158+
uses: ./.github/actions/init
159+
with:
160+
turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
161+
turbo-team: ${{ vars.TURBO_TEAM }}
162+
turbo-token: ${{ secrets.TURBO_TOKEN }}
163+
playwright-enabled: true
164+
160165
- name: Task Status
161166
id: task-status
162167
env:
@@ -170,16 +175,6 @@ jobs:
170175
(npx turbo-ignore --task=test:integration:${{ matrix.test-name }} --fallback=${{ github.base_ref || 'refs/heads/main' }}) || AFFECTED=1
171176
echo "affected=${AFFECTED}" >> $GITHUB_OUTPUT
172177
173-
- name: Setup
174-
if: ${{ steps.task-status.outputs.affected == '1' }}
175-
id: config
176-
uses: ./.github/actions/init
177-
with:
178-
turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
179-
turbo-team: ${{ vars.TURBO_TEAM }}
180-
turbo-token: ${{ secrets.TURBO_TOKEN }}
181-
playwright-enabled: true
182-
183178
- name: Verdaccio
184179
if: ${{ steps.task-status.outputs.affected == '1' }}
185180
uses: ./.github/actions/verdaccio
@@ -201,10 +196,33 @@ jobs:
201196
if: ${{ matrix.test-name == 'astro' }}
202197
run: cd packages/astro && npm run copy:components
203198

199+
- name: Write all ENV certificates to files in integration/certs
200+
if: ${{ steps.task-status.outputs.affected == '1' }}
201+
uses: actions/github-script@v7
202+
env:
203+
INTEGRATION_CERTS: '${{secrets.INTEGRATION_CERTS}}'
204+
INTEGRATION_ROOT_CA: '${{secrets.INTEGRATION_ROOT_CA}}'
205+
with:
206+
script: |
207+
const fs = require('fs');
208+
const path = require('path');
209+
const rootCa = process.env.INTEGRATION_ROOT_CA;
210+
console.log('rootCa', rootCa);
211+
fs.writeFileSync(path.join(process.env.GITHUB_WORKSPACE, 'integration/certs', 'rootCA.pem'), rootCa);
212+
const certs = JSON.parse(process.env.INTEGRATION_CERTS);
213+
for (const [name, cert] of Object.entries(certs)) {
214+
fs.writeFileSync(path.join(process.env.GITHUB_WORKSPACE, 'integration/certs', name), cert);
215+
}
216+
217+
- name: LS certs
218+
if: ${{ steps.task-status.outputs.affected == '1' }}
219+
working-directory: ./integration/certs
220+
run: ls -la && pwd
221+
204222
- name: Run Integration Tests
205223
if: ${{ steps.task-status.outputs.affected == '1' }}
206224
id: integration-tests
207-
run: npx turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS --only -- --project=${{ matrix.test-project }}
225+
run: sudo --preserve-env npx turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS --only -- --project=${{ matrix.test-project }}
208226
env:
209227
E2E_APP_CLERK_JS_DIR: ${{runner.temp}}
210228
E2E_CLERK_VERSION: 'latest'
@@ -213,6 +231,8 @@ jobs:
213231
E2E_CLERK_ENCRYPTION_KEY: ${{ matrix.clerk-encryption-key }}
214232
INTEGRATION_INSTANCE_KEYS: ${{ secrets.INTEGRATION_INSTANCE_KEYS }}
215233
MAILSAC_API_KEY: ${{ secrets.MAILSAC_API_KEY }}
234+
NODE_EXTRA_CA_CERTS: ${{ github.workspace }}/integration/certs/rootCA.pem
235+
216236

217237
- name: Upload test-results
218238
if: ${{ cancelled() || failure() }}

integration/README.md

+4
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,10 @@ This is why you created the `.keys.json` file in the [initial setup](#initial-se
577577
578578
They keys defined in `.keys.json.sample` correspond with the Clerk instances in the **Integration testing** organization.
579579
580+
### Test isolation
581+
582+
Before writing tests, it's important to understand how Playwright handles test isolation. Refer to the [Playwright documentation](https://playwright.dev/docs/browser-contexts) for more details.
583+
580584
> [!NOTE]
581585
> The test suite also uses these environment variables to run some tests:
582586
>

integration/certs/README.md

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
### Introduction
2+
3+
Some of our e2e test suites require self-signed SSL certificates to be installed on the local machine. This short guide will walk you through the process of generating self-signed SSL certificates using `mkcert`.
4+
5+
### Prerequisites
6+
7+
Good news! If you've set up your local development environment for Clerk, you've already installed `mkcert` as part of our `make deps` command. If you haven't, you can install it by following the instructions [here](https://github.com/FiloSottile/mkcert)
8+
9+
### Generate SSL Certificates
10+
11+
To generate a new cert/key pair, you can simply run the following command:
12+
13+
```bash
14+
mkcert -cert-file example.pem -key-file example-key.pem "example.com" "*.example.com"
15+
```
16+
17+
The command above will create a `example.pem` and a `example-key.pem` file in the current directory. The certificate will be valid for `example.com` and all subdomains of `example.com`.
18+
19+
### Using the Certificates
20+
21+
During installation, `mkcert` automatically adds its root CA to your machine's trust store. All certificates generated by `mkcert` from that point on, will you that specific root CA. This means that you can use the generated certificates in your local development environment without any additional configuration. There's an important caveat though: `node` does not use the system root store, so it won't accept mkcert certificates automatically. Instead, you will have to set the `NODE_EXTRA_CA_CERTS` environment variable.
22+
23+
```shell
24+
export NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem"
25+
```
26+
27+
or provide the `NODE_EXTRA_CA_CERTS` when runnning your tests:
28+
29+
```shell
30+
NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem" playwright test...
31+
```
32+
33+
For more details, see [here](https://github.com/FiloSottile/mkcert?tab=readme-ov-file#changing-the-location-of-the-ca-files)
34+
35+
### Github actions
36+
37+
In order to avoid install mkcert and generating self-signed certificates in our CI/CD pipeline, we have added the generated certificates and the root CA to the repository's secrets:
38+
39+
```shell
40+
secrets.INTEGRATION_ROOT_CA
41+
secrets.INTEGRATION_CERTS
42+
```
43+
44+
During the CICD run, the certificates are loaded from the ENV and written to the `ingration/certs` directory.

integration/constants.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as path from 'node:path';
44

55
export const constants = {
66
TMP_DIR: path.join(os.tmpdir(), '.temp_integration'),
7+
CERTS_DIR: path.join(process.cwd(), 'integration/certs'),
78
APPS_STATE_FILE: path.join(os.tmpdir(), '.temp_integration', 'state.json'),
89
/**
910
* A URL to a running app that will be used to run the tests against.

integration/models/application.ts

+23-10
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import type { EnvironmentConfig } from './environment.js';
66

77
export type Application = ReturnType<typeof application>;
88

9-
export const application = (config: ApplicationConfig, appDirPath: string, appDirName: string) => {
9+
export const application = (
10+
config: ApplicationConfig,
11+
appDirPath: string,
12+
appDirName: string,
13+
serverUrl: string | undefined,
14+
) => {
1015
const { name, scripts, envWriter } = config;
1116
const logger = createLogger({ prefix: `${appDirName}` });
1217
const state = { completedSetup: false, serverUrl: '', env: {} as EnvironmentConfig };
@@ -39,18 +44,24 @@ export const application = (config: ApplicationConfig, appDirPath: string, appDi
3944
state.completedSetup = true;
4045
}
4146
},
42-
dev: async (opts: { port?: number; manualStart?: boolean; detached?: boolean } = {}) => {
47+
dev: async (opts: { port?: number; manualStart?: boolean; detached?: boolean; serverUrl?: string } = {}) => {
4348
const log = logger.child({ prefix: 'dev' }).info;
4449
const port = opts.port || (await getPort());
45-
const serverUrl = `http://localhost:${port}`;
46-
log(`Will try to serve app at ${serverUrl}`);
50+
const getServerUrl = () => {
51+
if (opts.serverUrl) {
52+
return opts.serverUrl.includes(':') ? opts.serverUrl : `${opts.serverUrl}:${port}`;
53+
}
54+
return serverUrl || `http://localhost:${port}`;
55+
};
56+
const runtimeServerUrl = getServerUrl();
57+
log(`Will try to serve app at ${runtimeServerUrl}`);
4758
if (opts.manualStart) {
4859
// for debugging, you can start the dev server manually by cd'ing into the temp dir
4960
// and running the corresponding dev command
5061
// this allows the test to run as normally, while setup is controlled by you,
5162
// so you can inspect the running up outside the PW lifecycle
52-
state.serverUrl = serverUrl;
53-
return { port, serverUrl };
63+
state.serverUrl = runtimeServerUrl;
64+
return { port, serverUrl: runtimeServerUrl };
5465
}
5566

5667
const proc = run(scripts.dev, {
@@ -61,12 +72,13 @@ export const application = (config: ApplicationConfig, appDirPath: string, appDi
6172
stderr: opts.detached ? fs.openSync(stderrFilePath, 'a') : undefined,
6273
log: opts.detached ? undefined : log,
6374
});
75+
6476
const shouldExit = () => !!proc.exitCode && proc.exitCode !== 0;
65-
await waitForServer(serverUrl, { log, maxAttempts: Infinity, shouldExit });
66-
log(`Server started at ${serverUrl}, pid: ${proc.pid}`);
77+
await waitForServer(runtimeServerUrl, { log, maxAttempts: Infinity, shouldExit });
78+
log(`Server started at ${runtimeServerUrl}, pid: ${proc.pid}`);
6779
cleanupFns.push(() => awaitableTreekill(proc.pid, 'SIGKILL'));
68-
state.serverUrl = serverUrl;
69-
return { port, serverUrl, pid: proc.pid };
80+
state.serverUrl = runtimeServerUrl;
81+
return { port, serverUrl: runtimeServerUrl, pid: proc.pid };
7082
},
7183
build: async () => {
7284
const log = logger.child({ prefix: 'build' }).info;
@@ -83,6 +95,7 @@ export const application = (config: ApplicationConfig, appDirPath: string, appDi
8395
},
8496
serve: async (opts: { port?: number; manualStart?: boolean } = {}) => {
8597
const port = opts.port || (await getPort());
98+
// TODO: get serverUrl as in dev()
8699
const serverUrl = `http://localhost:${port}`;
87100
// If this is ever used as a background process, we need to make sure
88101
// it's not using the log function. See the dev() method above

integration/models/applicationConfig.ts

+14-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type Scripts = { dev: string; build: string; setup: string; serve: string };
1212

1313
export const applicationConfig = () => {
1414
let name = '';
15+
let serverUrl = '';
1516
const templates: string[] = [];
1617
const files = new Map<string, string>();
1718
const scripts: Scripts = { dev: 'npm run dev', serve: 'npm run serve', build: 'npm run build', setup: 'npm i' };
@@ -35,6 +36,10 @@ export const applicationConfig = () => {
3536
name = _name;
3637
return self;
3738
},
39+
setServerUrl: (_serverUrl: string) => {
40+
serverUrl = _serverUrl;
41+
return self;
42+
},
3843
addFile: (filePath: string, cbOrPath: (helpers: Helpers) => string) => {
3944
files.set(filePath, cbOrPath(helpers));
4045
return self;
@@ -96,7 +101,7 @@ export const applicationConfig = () => {
96101
await fs.writeJSON(packageJsonPath, contents, { spaces: 2 });
97102
}
98103

99-
return application(self, appDirPath, appDirName);
104+
return application(self, appDirPath, appDirName, serverUrl);
100105
},
101106
setEnvWriter: () => {
102107
throw new Error('not implemented');
@@ -115,9 +120,15 @@ export const applicationConfig = () => {
115120
logger.info(`Creating env file ".env" -> ${envDest}`);
116121
await fs.writeFile(
117122
path.join(appDir, '.env'),
118-
[...env.publicVariables].map(([k, v]) => `${envFormatters.public(k)}=${v}`).join('\n') +
123+
[...env.publicVariables]
124+
.filter(([_, v]) => v)
125+
.map(([k, v]) => `${envFormatters.public(k)}=${v}`)
126+
.join('\n') +
119127
'\n' +
120-
[...env.privateVariables].map(([k, v]) => `${envFormatters.private(k)}=${v}`).join('\n'),
128+
[...env.privateVariables]
129+
.filter(([_, v]) => v)
130+
.map(([k, v]) => `${envFormatters.private(k)}=${v}`)
131+
.join('\n'),
121132
);
122133
};
123134
return defaultWriter;

integration/models/environment.ts

-5
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ export type EnvironmentConfig = {
77
get id(): string;
88
setId(newId: string): EnvironmentConfig;
99
setEnvVariable(type: keyof EnvironmentVariables, name: string, value: any): EnvironmentConfig;
10-
removeEnvVariable(type: keyof EnvironmentVariables, name: string): EnvironmentConfig;
1110
get publicVariables(): EnvironmentVariables['public'];
1211
get privateVariables(): EnvironmentVariables['private'];
1312
toJson(): { public: Record<string, string>; private: Record<string, string> };
@@ -34,10 +33,6 @@ export const environmentConfig = () => {
3433
envVars[type].set(name, value);
3534
return self;
3635
},
37-
removeEnvVariable: (type, name) => {
38-
envVars[type].delete(name);
39-
return self;
40-
},
4136
get publicVariables() {
4237
return envVars.public;
4338
},

integration/playwright.config.ts

+2
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ export const common: PlaywrightTestConfig = {
1818
workers: process.env.CI ? '50%' : '70%',
1919
reporter: process.env.CI ? 'line' : 'list',
2020
use: {
21+
ignoreHTTPSErrors: true,
2122
trace: 'retain-on-failure',
2223
bypassCSP: true, // We probably need to limit this to specific tests
2324
},
2425
} as const;
2526

2627
export default defineConfig({
2728
...common,
29+
2830
projects: [
2931
{
3032
name: 'setup',

0 commit comments

Comments
 (0)