Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
6 changes: 6 additions & 0 deletions .changeset/wet-dryers-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@clerk/backend": patch
---

Extend `buildRedirectToHandshake` to accept search params
and track delta between `session.iat` and `client.uat` in case `iat < uat`
12 changes: 12 additions & 0 deletions packages/backend/src/tokens/__tests__/handshake.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,18 @@ describe('HandshakeService', () => {
);
});

it('should include additional search params when provided', () => {
const headers = handshakeService.buildRedirectToHandshake('test-reason', { iat_uat_delta: '100' });
const location = headers.get(constants.Headers.Location);
expect(location).toBeDefined();
if (!location) {
throw new Error('Location header is missing');
}
Comment thread
jfoshee marked this conversation as resolved.
Outdated
const url = new URL(location);

expect(url.searchParams.get('iat_uat_delta')).toBe('100');
});

it('should use proxy URL when available', () => {
mockAuthenticateContext.proxyUrl = 'https://my-proxy.example.com';
// Simulate what parsePublishableKey does when proxy URL is provided
Expand Down
11 changes: 10 additions & 1 deletion packages/backend/src/tokens/handshake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,11 @@ export class HandshakeService {
/**
* Builds the redirect headers for a handshake request
* @param reason - The reason for the handshake (e.g. 'session-token-expired')
* @param additionalSearchParams - Additional search params to append to the handshake URL (e.g. to help with observability)
* @returns Headers object containing the Location header for redirect
* @throws Error if clerkUrl is missing in authenticateContext
*/
buildRedirectToHandshake(reason: string): Headers {
buildRedirectToHandshake(reason: string, additionalSearchParams?: Record<string, string | undefined>): Headers {
if (!this.authenticateContext?.clerkUrl) {
throw new Error('Missing clerkUrl in authenticateContext');
}
Expand Down Expand Up @@ -163,6 +164,14 @@ export class HandshakeService {
});
}

if (additionalSearchParams) {
Object.entries(additionalSearchParams).forEach(([key, value]) => {
if (typeof value !== 'undefined') {
url.searchParams.append(key, value);
}
});
}

return new Headers({ [constants.Headers.Location]: url.href });
}

Expand Down
13 changes: 11 additions & 2 deletions packages/backend/src/tokens/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ export const authenticateRequest: AuthenticateRequest = (async (
reason: string,
message: string,
headers?: Headers,
handshakeSearchParams?: Record<string, string | undefined>,
): SignedInState | SignedOutState | HandshakeState {
if (!handshakeService.isRequestEligibleForHandshake()) {
return signedOut({
Expand All @@ -306,7 +307,7 @@ export const authenticateRequest: AuthenticateRequest = (async (

// Right now the only usage of passing in different headers is for multi-domain sync, which redirects somewhere else.
// In the future if we want to decorate the handshake redirect with additional headers per call we need to tweak this logic.
const handshakeHeaders = headers ?? handshakeService.buildRedirectToHandshake(reason);
const handshakeHeaders = headers ?? handshakeService.buildRedirectToHandshake(reason, handshakeSearchParams);

// Chrome aggressively caches inactive tabs. If we don't set the header here,
// all 307 redirects will be cached and the handshake will end up in an infinite loop.
Expand Down Expand Up @@ -534,7 +535,15 @@ export const authenticateRequest: AuthenticateRequest = (async (
}

if (decodeResult.payload.iat < authenticateContext.clientUat) {
return handleMaybeHandshakeStatus(authenticateContext, AuthErrorReason.SessionTokenIATBeforeClientUAT, '');
// Track delta for observability
const delta = authenticateContext.clientUat - decodeResult.payload.iat;
return handleMaybeHandshakeStatus(
authenticateContext,
AuthErrorReason.SessionTokenIATBeforeClientUAT,
'',
undefined,
{ iat_uat_delta: delta.toString() },
);
}

try {
Expand Down
Loading