Skip to content
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',

Check failure

Code scanning / CodeQL

Hard-coded credentials

The hard-coded value "deadbeef" is used as [authorization header](1).
});

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