From 6a2dfb45acd1030c4bcefd2de984c42383bcafd0 Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 12 May 2025 09:42:04 -0500 Subject: [PATCH 1/7] feat(backend): signal support for handshake nonce --- packages/backend/src/constants.ts | 1 + packages/backend/src/tokens/__tests__/handshake.test.ts | 2 ++ packages/backend/src/tokens/handshake.ts | 1 + 3 files changed, 4 insertions(+) diff --git a/packages/backend/src/constants.ts b/packages/backend/src/constants.ts index 08e90a3164f..eebe9439718 100644 --- a/packages/backend/src/constants.ts +++ b/packages/backend/src/constants.ts @@ -35,6 +35,7 @@ const QueryParameters = { LegacyDevBrowser: '__dev_session', HandshakeReason: '__clerk_hs_reason', HandshakeNonce: Cookies.HandshakeNonce, + SupportsHandshakeNonce: '__clerk_supports_handshake_nonce', } as const; const Headers = { diff --git a/packages/backend/src/tokens/__tests__/handshake.test.ts b/packages/backend/src/tokens/__tests__/handshake.test.ts index 48044a17c8a..97b8e7e8ba4 100644 --- a/packages/backend/src/tokens/__tests__/handshake.test.ts +++ b/packages/backend/src/tokens/__tests__/handshake.test.ts @@ -154,6 +154,7 @@ describe('HandshakeService', () => { expect(url.searchParams.get('redirect_url')).toBe('https://example.com/'); expect(url.searchParams.get(constants.QueryParameters.SuffixedCookies)).toBe('true'); expect(url.searchParams.get(constants.QueryParameters.HandshakeReason)).toBe('test-reason'); + expect(url.searchParams.get(constants.QueryParameters.SupportsHandshakeNonce)).toBe('true'); }); it('should include dev browser token in development mode', () => { @@ -167,6 +168,7 @@ describe('HandshakeService', () => { const url = new URL(location); expect(url.searchParams.get(constants.QueryParameters.DevBrowser)).toBe('dev-token'); + expect(url.searchParams.get(constants.QueryParameters.SupportsHandshakeNonce)).toBe('true'); }); it('should throw error if clerkUrl is missing', () => { diff --git a/packages/backend/src/tokens/handshake.ts b/packages/backend/src/tokens/handshake.ts index 417bab8c999..d827b81533f 100644 --- a/packages/backend/src/tokens/handshake.ts +++ b/packages/backend/src/tokens/handshake.ts @@ -145,6 +145,7 @@ export class HandshakeService { this.authenticateContext.usesSuffixedCookies().toString(), ); url.searchParams.append(constants.QueryParameters.HandshakeReason, reason); + url.searchParams.append(constants.QueryParameters.SupportsHandshakeNonce, 'true'); if (this.authenticateContext.instanceType === 'development' && this.authenticateContext.devBrowserToken) { url.searchParams.append(constants.QueryParameters.DevBrowser, this.authenticateContext.devBrowserToken); From 6480559008bd6284805532c52ffb5be28dff6dae Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 12 May 2025 09:46:49 -0500 Subject: [PATCH 2/7] changeset --- .changeset/six-ears-wash.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/six-ears-wash.md diff --git a/.changeset/six-ears-wash.md b/.changeset/six-ears-wash.md new file mode 100644 index 00000000000..c6b32a544ab --- /dev/null +++ b/.changeset/six-ears-wash.md @@ -0,0 +1,5 @@ +--- +'@clerk/backend': minor +--- + +When redirecting for a handshake to FAPI, send signal that current version of @clerk/backend supports the handshake nonce From bfebb0014058a8e00b5522a0cbc5dc252f8400fe Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 12 May 2025 09:57:56 -0500 Subject: [PATCH 3/7] rename param to format --- packages/backend/src/constants.ts | 2 +- packages/backend/src/tokens/__tests__/handshake.test.ts | 4 ++-- packages/backend/src/tokens/handshake.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/constants.ts b/packages/backend/src/constants.ts index eebe9439718..8570b0ba404 100644 --- a/packages/backend/src/constants.ts +++ b/packages/backend/src/constants.ts @@ -35,7 +35,7 @@ const QueryParameters = { LegacyDevBrowser: '__dev_session', HandshakeReason: '__clerk_hs_reason', HandshakeNonce: Cookies.HandshakeNonce, - SupportsHandshakeNonce: '__clerk_supports_handshake_nonce', + HandshakeFormat: 'format', } as const; const Headers = { diff --git a/packages/backend/src/tokens/__tests__/handshake.test.ts b/packages/backend/src/tokens/__tests__/handshake.test.ts index 97b8e7e8ba4..cbcf005507d 100644 --- a/packages/backend/src/tokens/__tests__/handshake.test.ts +++ b/packages/backend/src/tokens/__tests__/handshake.test.ts @@ -154,7 +154,7 @@ describe('HandshakeService', () => { expect(url.searchParams.get('redirect_url')).toBe('https://example.com/'); expect(url.searchParams.get(constants.QueryParameters.SuffixedCookies)).toBe('true'); expect(url.searchParams.get(constants.QueryParameters.HandshakeReason)).toBe('test-reason'); - expect(url.searchParams.get(constants.QueryParameters.SupportsHandshakeNonce)).toBe('true'); + expect(url.searchParams.get(constants.QueryParameters.HandshakeFormat)).toBe('nonce'); }); it('should include dev browser token in development mode', () => { @@ -168,7 +168,7 @@ describe('HandshakeService', () => { const url = new URL(location); expect(url.searchParams.get(constants.QueryParameters.DevBrowser)).toBe('dev-token'); - expect(url.searchParams.get(constants.QueryParameters.SupportsHandshakeNonce)).toBe('true'); + expect(url.searchParams.get(constants.QueryParameters.HandshakeFormat)).toBe('nonce'); }); it('should throw error if clerkUrl is missing', () => { diff --git a/packages/backend/src/tokens/handshake.ts b/packages/backend/src/tokens/handshake.ts index d827b81533f..e5f04696684 100644 --- a/packages/backend/src/tokens/handshake.ts +++ b/packages/backend/src/tokens/handshake.ts @@ -145,7 +145,7 @@ export class HandshakeService { this.authenticateContext.usesSuffixedCookies().toString(), ); url.searchParams.append(constants.QueryParameters.HandshakeReason, reason); - url.searchParams.append(constants.QueryParameters.SupportsHandshakeNonce, 'true'); + url.searchParams.append(constants.QueryParameters.HandshakeFormat, 'nonce'); if (this.authenticateContext.instanceType === 'development' && this.authenticateContext.devBrowserToken) { url.searchParams.append(constants.QueryParameters.DevBrowser, this.authenticateContext.devBrowserToken); From 7e3ad6e188beae1c854e8b840cc494637ec70d85 Mon Sep 17 00:00:00 2001 From: Jacek Date: Wed, 25 Jun 2025 22:15:55 -0500 Subject: [PATCH 4/7] update changeset --- .changeset/six-ears-wash.md | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/.changeset/six-ears-wash.md b/.changeset/six-ears-wash.md index c6b32a544ab..e384ff754e4 100644 --- a/.changeset/six-ears-wash.md +++ b/.changeset/six-ears-wash.md @@ -2,4 +2,35 @@ '@clerk/backend': minor --- -When redirecting for a handshake to FAPI, send signal that current version of @clerk/backend supports the handshake nonce +**Optimize handshake payload delivery with nonce-based fetching** + +This change introduces a significant optimization to the handshake flow by replacing direct payload delivery with a nonce-based approach to overcome browser cookie size limitations. + +**Problem Solved:** +Previously, the handshake payload (an encoded JWT containing set-cookie headers) was sent directly in a cookie. Since browsers limit cookies to ~4KB, this severely restricted the practical size of session tokens, which are also JWTs stored in cookies but embedded within the handshake payload. + +**Solution:** +We now use a conditional approach based on payload size: +- **Small payloads (≤2KB)**: Continue using the direct approach for optimal performance +- **Large payloads (>2KB)**: Use nonce-based fetching to avoid cookie size limits + +For large payloads, we: +1. Generate a short nonce (ID) for each handshake instance +2. Send only the nonce in the `__clerk_handshake_nonce` cookie +3. Use the nonce to fetch the actual handshake payload via a dedicated BAPI endpoint + +**New Handshake Flow (for payloads >2KB):** +1. User visits `example.com` +2. Trigger handshake → `307 /v1/client/handshake` +3. Handshake resolves → `307 ecxample.com` with `__clerk_handshake_nonce` cookie containing the nonce +4. Client makes `GET BAPI/v1/clients/handshake_payload?nonce=` request +5. BAPI returns array of set-cookie header values +6. Headers are applied to the response + +**Traditional Flow (for payloads ≤2KB):** +Continues to work as before with direct payload delivery in cookies for optimal performance. + +**Trade-offs:** +- **Added**: One additional BAPI call per handshake (only for payloads >2KB) +- **Removed**: Cookie size restrictions that previously limited session token size + From 73e2e5c94f9e6f49f5597c0e0171c0e7fe220d1e Mon Sep 17 00:00:00 2001 From: Jacek Date: Wed, 25 Jun 2025 22:21:27 -0500 Subject: [PATCH 5/7] fix typo --- .changeset/six-ears-wash.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/six-ears-wash.md b/.changeset/six-ears-wash.md index e384ff754e4..cdf6947a46e 100644 --- a/.changeset/six-ears-wash.md +++ b/.changeset/six-ears-wash.md @@ -22,7 +22,7 @@ For large payloads, we: **New Handshake Flow (for payloads >2KB):** 1. User visits `example.com` 2. Trigger handshake → `307 /v1/client/handshake` -3. Handshake resolves → `307 ecxample.com` with `__clerk_handshake_nonce` cookie containing the nonce +3. Handshake resolves → `307 example.com` with `__clerk_handshake_nonce` cookie containing the nonce 4. Client makes `GET BAPI/v1/clients/handshake_payload?nonce=` request 5. BAPI returns array of set-cookie header values 6. Headers are applied to the response From be1acc3d2ad26a95de9381616e4dd25a0ce7e1a8 Mon Sep 17 00:00:00 2001 From: Jacek Date: Wed, 25 Jun 2025 22:22:59 -0500 Subject: [PATCH 6/7] wip --- .changeset/six-ears-wash.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.changeset/six-ears-wash.md b/.changeset/six-ears-wash.md index cdf6947a46e..509a2b8bcfb 100644 --- a/.changeset/six-ears-wash.md +++ b/.changeset/six-ears-wash.md @@ -2,14 +2,14 @@ '@clerk/backend': minor --- -**Optimize handshake payload delivery with nonce-based fetching** +## Optimize handshake payload delivery with nonce-based fetching This change introduces a significant optimization to the handshake flow by replacing direct payload delivery with a nonce-based approach to overcome browser cookie size limitations. -**Problem Solved:** +## Problem Solved Previously, the handshake payload (an encoded JWT containing set-cookie headers) was sent directly in a cookie. Since browsers limit cookies to ~4KB, this severely restricted the practical size of session tokens, which are also JWTs stored in cookies but embedded within the handshake payload. -**Solution:** +## Solution We now use a conditional approach based on payload size: - **Small payloads (≤2KB)**: Continue using the direct approach for optimal performance - **Large payloads (>2KB)**: Use nonce-based fetching to avoid cookie size limits @@ -19,7 +19,7 @@ For large payloads, we: 2. Send only the nonce in the `__clerk_handshake_nonce` cookie 3. Use the nonce to fetch the actual handshake payload via a dedicated BAPI endpoint -**New Handshake Flow (for payloads >2KB):** +## New Handshake Flow (for payloads >2KB) 1. User visits `example.com` 2. Trigger handshake → `307 /v1/client/handshake` 3. Handshake resolves → `307 example.com` with `__clerk_handshake_nonce` cookie containing the nonce @@ -27,10 +27,10 @@ For large payloads, we: 5. BAPI returns array of set-cookie header values 6. Headers are applied to the response -**Traditional Flow (for payloads ≤2KB):** -Continues to work as before with direct payload delivery in cookies for optimal performance. +## Traditional Flow (for payloads ≤2KB) +No changes. Continues to work as before with direct payload delivery in cookies for optimal performance. -**Trade-offs:** +## Trade-offs - **Added**: One additional BAPI call per handshake (only for payloads >2KB) - **Removed**: Cookie size restrictions that previously limited session token size From 87cf34e2e05bcf0a1dff46d5aff522f105584888 Mon Sep 17 00:00:00 2001 From: Jacek Date: Wed, 25 Jun 2025 22:33:45 -0500 Subject: [PATCH 7/7] added clarifications --- .changeset/six-ears-wash.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.changeset/six-ears-wash.md b/.changeset/six-ears-wash.md index 509a2b8bcfb..749061db06e 100644 --- a/.changeset/six-ears-wash.md +++ b/.changeset/six-ears-wash.md @@ -21,11 +21,11 @@ For large payloads, we: ## New Handshake Flow (for payloads >2KB) 1. User visits `example.com` -2. Trigger handshake → `307 /v1/client/handshake` -3. Handshake resolves → `307 example.com` with `__clerk_handshake_nonce` cookie containing the nonce -4. Client makes `GET BAPI/v1/clients/handshake_payload?nonce=` request +2. Client app middleware triggers handshake → `307 FAPI/v1/client/handshake` +3. FAPI handshake resolves → `307 example.com` with `__clerk_handshake_nonce` cookie containing the nonce +4. Client app middleware makes `GET BAPI/v1/clients/handshake_payload?nonce=` request (BAPI) 5. BAPI returns array of set-cookie header values -6. Headers are applied to the response +6. Client app middleware applies headers to the response ## Traditional Flow (for payloads ≤2KB) No changes. Continues to work as before with direct payload delivery in cookies for optimal performance.