Skip to content

feat(backend): handle handshake nonce payload #5865

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
May 9, 2025
6 changes: 6 additions & 0 deletions .changeset/eight-heads-act.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/clerk-js': minor
'@clerk/backend': minor
---

Add handling of new Handshake nonce flow when authenticating requests
98 changes: 98 additions & 0 deletions packages/backend/src/api/__tests__/ClientApi.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { http, HttpResponse } from 'msw';
import { describe, expect, it } from 'vitest';

import { server, validateHeaders } from '../../mock-server';
import { createBackendApiClient } from '../factory';

describe('ClientAPI', () => {
const apiClient = createBackendApiClient({
apiUrl: 'https://api.clerk.test',
secretKey: 'deadbeef',
});

describe('getHandshakePayload', () => {
it('successfully fetches the handshake payload with a valid nonce', async () => {
const mockHandshakePayload = {
directives: ['directive1', 'directive2'],
};

server.use(
http.get(
'https://api.clerk.test/v1/clients/handshake_payload',
validateHeaders(({ request }) => {
const url = new URL(request.url);
expect(url.searchParams.get('nonce')).toBe('test-nonce-123');
return HttpResponse.json(mockHandshakePayload);
}),
),
);

const response = await apiClient.clients.getHandshakePayload({
nonce: 'test-nonce-123',
});

expect(response.directives).toEqual(['directive1', 'directive2']);
expect(response.directives.length).toBe(2);
});

it('handles error responses correctly', async () => {
const mockErrorPayload = {
code: 'invalid_nonce',
message: 'Invalid nonce provided',
long_message: 'The nonce provided is invalid or has expired',
meta: { param_name: 'nonce' },
};
const traceId = 'trace_id_handshake';

server.use(
http.get(
'https://api.clerk.test/v1/clients/handshake_payload',
validateHeaders(() => {
return HttpResponse.json(
{ errors: [mockErrorPayload], clerk_trace_id: traceId },
{ status: 400, headers: { 'cf-ray': traceId } },
);
}),
),
);

const errResponse = await apiClient.clients.getHandshakePayload({ nonce: 'invalid-nonce' }).catch(err => err);

expect(errResponse.clerkTraceId).toBe(traceId);
expect(errResponse.status).toBe(400);
expect(errResponse.errors[0].code).toBe('invalid_nonce');
expect(errResponse.errors[0].message).toBe('Invalid nonce provided');
expect(errResponse.errors[0].longMessage).toBe('The nonce provided is invalid or has expired');
expect(errResponse.errors[0].meta.paramName).toBe('nonce');
});

it('requires a nonce parameter', async () => {
server.use(
http.get(
'https://api.clerk.test/v1/clients/handshake_payload',
validateHeaders(() => {
return HttpResponse.json(
{
errors: [
{
code: 'missing_parameter',
message: 'Missing required parameter',
long_message: 'The nonce parameter is required',
meta: { param_name: 'nonce' },
},
],
},
{ status: 400 },
);
}),
),
);

// @ts-expect-error Testing invalid input
const errResponse = await apiClient.clients.getHandshakePayload({}).catch(err => err);

expect(errResponse.status).toBe(400);
expect(errResponse.errors[0].code).toBe('missing_parameter');
});
});
});
13 changes: 13 additions & 0 deletions packages/backend/src/api/endpoints/ClientApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ import type { ClerkPaginationRequest } from '@clerk/types';
import { joinPaths } from '../../util/path';
import type { Client } from '../resources/Client';
import type { PaginatedResourceResponse } from '../resources/Deserializer';
import type { HandshakePayload } from '../resources/HandshakePayload';
import { AbstractAPI } from './AbstractApi';

const basePath = '/clients';

type GetHandshakePayloadParams = {
nonce: string;
};

export class ClientAPI extends AbstractAPI {
public async getClientList(params: ClerkPaginationRequest = {}) {
return this.request<PaginatedResourceResponse<Client[]>>({
Expand All @@ -31,4 +36,12 @@ export class ClientAPI extends AbstractAPI {
bodyParams: { token },
});
}

public async getHandshakePayload(queryParams: GetHandshakePayloadParams) {
return this.request<HandshakePayload>({
method: 'GET',
path: joinPaths(basePath, 'handshake_payload'),
queryParams,
});
}
}
18 changes: 0 additions & 18 deletions packages/backend/src/api/endpoints/HandshakePayloadApi.ts

This file was deleted.

10 changes: 3 additions & 7 deletions packages/backend/src/api/resources/HandshakePayload.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
export type HandshakePayloadJSON = {
nonce: string;
payload: string;
directives: string[];
};

export class HandshakePayload {
constructor(
readonly nonce: string,
readonly payload: string,
) {}
constructor(readonly directives: string[]) {}

static fromJSON(data: HandshakePayloadJSON): HandshakePayload {
return new HandshakePayload(data.nonce, data.payload);
return new HandshakePayload(data.directives);
}
}
Loading
Loading