Skip to content

Commit 0769a9b

Browse files
authored
feat(backend): handle handshake nonce payload (#5865)
1 parent 4998547 commit 0769a9b

File tree

10 files changed

+250
-232
lines changed

10 files changed

+250
-232
lines changed

.changeset/eight-heads-act.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@clerk/clerk-js': minor
3+
'@clerk/backend': minor
4+
---
5+
6+
Add handling of new Handshake nonce flow when authenticating requests
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { http, HttpResponse } from 'msw';
2+
import { describe, expect, it } from 'vitest';
3+
4+
import { server, validateHeaders } from '../../mock-server';
5+
import { createBackendApiClient } from '../factory';
6+
7+
describe('ClientAPI', () => {
8+
const apiClient = createBackendApiClient({
9+
apiUrl: 'https://api.clerk.test',
10+
secretKey: 'deadbeef',
11+
});
12+
13+
describe('getHandshakePayload', () => {
14+
it('successfully fetches the handshake payload with a valid nonce', async () => {
15+
const mockHandshakePayload = {
16+
directives: ['directive1', 'directive2'],
17+
};
18+
19+
server.use(
20+
http.get(
21+
'https://api.clerk.test/v1/clients/handshake_payload',
22+
validateHeaders(({ request }) => {
23+
const url = new URL(request.url);
24+
expect(url.searchParams.get('nonce')).toBe('test-nonce-123');
25+
return HttpResponse.json(mockHandshakePayload);
26+
}),
27+
),
28+
);
29+
30+
const response = await apiClient.clients.getHandshakePayload({
31+
nonce: 'test-nonce-123',
32+
});
33+
34+
expect(response.directives).toEqual(['directive1', 'directive2']);
35+
expect(response.directives.length).toBe(2);
36+
});
37+
38+
it('handles error responses correctly', async () => {
39+
const mockErrorPayload = {
40+
code: 'invalid_nonce',
41+
message: 'Invalid nonce provided',
42+
long_message: 'The nonce provided is invalid or has expired',
43+
meta: { param_name: 'nonce' },
44+
};
45+
const traceId = 'trace_id_handshake';
46+
47+
server.use(
48+
http.get(
49+
'https://api.clerk.test/v1/clients/handshake_payload',
50+
validateHeaders(() => {
51+
return HttpResponse.json(
52+
{ errors: [mockErrorPayload], clerk_trace_id: traceId },
53+
{ status: 400, headers: { 'cf-ray': traceId } },
54+
);
55+
}),
56+
),
57+
);
58+
59+
const errResponse = await apiClient.clients.getHandshakePayload({ nonce: 'invalid-nonce' }).catch(err => err);
60+
61+
expect(errResponse.clerkTraceId).toBe(traceId);
62+
expect(errResponse.status).toBe(400);
63+
expect(errResponse.errors[0].code).toBe('invalid_nonce');
64+
expect(errResponse.errors[0].message).toBe('Invalid nonce provided');
65+
expect(errResponse.errors[0].longMessage).toBe('The nonce provided is invalid or has expired');
66+
expect(errResponse.errors[0].meta.paramName).toBe('nonce');
67+
});
68+
69+
it('requires a nonce parameter', async () => {
70+
server.use(
71+
http.get(
72+
'https://api.clerk.test/v1/clients/handshake_payload',
73+
validateHeaders(() => {
74+
return HttpResponse.json(
75+
{
76+
errors: [
77+
{
78+
code: 'missing_parameter',
79+
message: 'Missing required parameter',
80+
long_message: 'The nonce parameter is required',
81+
meta: { param_name: 'nonce' },
82+
},
83+
],
84+
},
85+
{ status: 400 },
86+
);
87+
}),
88+
),
89+
);
90+
91+
// @ts-expect-error Testing invalid input
92+
const errResponse = await apiClient.clients.getHandshakePayload({}).catch(err => err);
93+
94+
expect(errResponse.status).toBe(400);
95+
expect(errResponse.errors[0].code).toBe('missing_parameter');
96+
});
97+
});
98+
});

packages/backend/src/api/endpoints/ClientApi.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@ import type { ClerkPaginationRequest } from '@clerk/types';
33
import { joinPaths } from '../../util/path';
44
import type { Client } from '../resources/Client';
55
import type { PaginatedResourceResponse } from '../resources/Deserializer';
6+
import type { HandshakePayload } from '../resources/HandshakePayload';
67
import { AbstractAPI } from './AbstractApi';
78

89
const basePath = '/clients';
910

11+
type GetHandshakePayloadParams = {
12+
nonce: string;
13+
};
14+
1015
export class ClientAPI extends AbstractAPI {
1116
public async getClientList(params: ClerkPaginationRequest = {}) {
1217
return this.request<PaginatedResourceResponse<Client[]>>({
@@ -31,4 +36,12 @@ export class ClientAPI extends AbstractAPI {
3136
bodyParams: { token },
3237
});
3338
}
39+
40+
public async getHandshakePayload(queryParams: GetHandshakePayloadParams) {
41+
return this.request<HandshakePayload>({
42+
method: 'GET',
43+
path: joinPaths(basePath, 'handshake_payload'),
44+
queryParams,
45+
});
46+
}
3447
}

packages/backend/src/api/endpoints/HandshakePayloadApi.ts

Lines changed: 0 additions & 18 deletions
This file was deleted.
Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
export type HandshakePayloadJSON = {
2-
nonce: string;
3-
payload: string;
2+
directives: string[];
43
};
54

65
export class HandshakePayload {
7-
constructor(
8-
readonly nonce: string,
9-
readonly payload: string,
10-
) {}
6+
constructor(readonly directives: string[]) {}
117

128
static fromJSON(data: HandshakePayloadJSON): HandshakePayload {
13-
return new HandshakePayload(data.nonce, data.payload);
9+
return new HandshakePayload(data.directives);
1410
}
1511
}

0 commit comments

Comments
 (0)