diff --git a/packages/javascript/src/api/__tests__/executeEmbeddedSignInFlow.test.ts b/packages/javascript/src/api/__tests__/executeEmbeddedSignInFlow.test.ts index bca7908..25d1947 100644 --- a/packages/javascript/src/api/__tests__/executeEmbeddedSignInFlow.test.ts +++ b/packages/javascript/src/api/__tests__/executeEmbeddedSignInFlow.test.ts @@ -34,6 +34,12 @@ const captureRequestBody = (): Record => { return JSON.parse(requestInit.body as string) as Record; }; +const captureRequestHeaders = (): Record => { + const calls = (fetch as ReturnType).mock.calls; + const requestInit = calls[calls.length - 1][1] as RequestInit; + return requestInit.headers as Record; +}; + describe('executeEmbeddedSignInFlow', (): void => { beforeEach((): void => { vi.resetAllMocks(); @@ -134,6 +140,37 @@ describe('executeEmbeddedSignInFlow', (): void => { }); }); + describe('Flow-Secret header', (): void => { + it('sends the Flow Secret header on a new flow start', async (): Promise => { + await executeEmbeddedSignInFlow({ + flowSecret: 'flow-secret-123', + payload: {applicationId: 'app-1', flowType: 'AUTHENTICATION'}, + url: URL, + }); + + expect(captureRequestHeaders()).toMatchObject({'Flow-Secret': 'flow-secret-123'}); + }); + + it('does NOT send the Flow Secret header on a step submission', async (): Promise => { + await executeEmbeddedSignInFlow({ + flowSecret: 'flow-secret-123', + payload: {action: 'submit', executionId: 'exec-abc', inputs: {password: 'secret', username: 'user'}}, + url: URL, + }); + + expect(captureRequestHeaders()).not.toHaveProperty('Flow-Secret'); + }); + + it('does NOT send the Flow Secret header when no flowSecret is provided', async (): Promise => { + await executeEmbeddedSignInFlow({ + payload: {applicationId: 'app-1', flowType: 'AUTHENTICATION'}, + url: URL, + }); + + expect(captureRequestHeaders()).not.toHaveProperty('Flow-Secret'); + }); + }); + it('throws when payload is missing', async (): Promise => { await expect(executeEmbeddedSignInFlow({url: URL})).rejects.toThrow('Authorization payload is required'); }); diff --git a/packages/javascript/src/api/executeEmbeddedSignInFlow.ts b/packages/javascript/src/api/executeEmbeddedSignInFlow.ts index 156c8b2..6eeb80e 100644 --- a/packages/javascript/src/api/executeEmbeddedSignInFlow.ts +++ b/packages/javascript/src/api/executeEmbeddedSignInFlow.ts @@ -26,6 +26,7 @@ const executeEmbeddedSignInFlow = async ({ baseUrl, payload, authId, + flowSecret, ...requestConfig }: EmbeddedFlowExecuteRequestConfig): Promise => { if (!payload) { @@ -74,6 +75,9 @@ const executeEmbeddedSignInFlow = async ({ headers: { Accept: 'application/json', 'Content-Type': 'application/json', + // A backend/server-side application presents its Flow Secret to initiate a new flow. It is + // only relevant on initiation, so it is omitted from continuation requests. + ...(isNewFlowStart && flowSecret ? {'Flow-Secret': flowSecret} : {}), ...requestConfig.headers, } as HeadersInit, method: requestConfig.method || 'POST', diff --git a/packages/javascript/src/models/embedded-flow.ts b/packages/javascript/src/models/embedded-flow.ts index 780194b..234b3da 100644 --- a/packages/javascript/src/models/embedded-flow.ts +++ b/packages/javascript/src/models/embedded-flow.ts @@ -35,6 +35,13 @@ export enum EmbeddedFlowResponseType { */ export interface EmbeddedFlowExecuteRequestConfigBase extends Partial { baseUrl?: string; + /** + * Flow Secret authenticating a backend/server-side application when it initiates a new native + * flow directly. When set, it is sent in the `Flow-Secret` request header, and only on a new + * flow initiation (a payload carrying `applicationId` and `flowType`) — never on continuation + * requests. Not applicable to public clients or redirect-based applications. + */ + flowSecret?: string; payload?: T; url?: string; } diff --git a/packages/nextjs/src/ThunderIDNextClient.ts b/packages/nextjs/src/ThunderIDNextClient.ts index 0963fae..50cfa0f 100644 --- a/packages/nextjs/src/ThunderIDNextClient.ts +++ b/packages/nextjs/src/ThunderIDNextClient.ts @@ -265,6 +265,7 @@ class ThunderIDNextClient e return executeEmbeddedSignInFlow({ baseUrl: configData?.baseUrl, + flowSecret: arg2?.flowSecret, payload: arg1, url: arg2?.url, }) as unknown as Promise;