Skip to content

Commit

Permalink
feat: sign hook payload data
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaoyijun committed May 22, 2023
1 parent b92508d commit e80604c
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 5 deletions.
14 changes: 12 additions & 2 deletions packages/core/src/libraries/hook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { got } from 'got';
import type { Interaction } from './hook.js';

const { jest } = import.meta;
const { mockEsmWithActual } = createMockUtils(jest);
const { mockEsmWithActual, mockEsm } = createMockUtils(jest);

const nanoIdMock = 'mockId';
await mockEsmWithActual('@logto/shared', () => ({
Expand All @@ -15,6 +15,12 @@ await mockEsmWithActual('@logto/shared', () => ({
generateStandardId: () => nanoIdMock,
}));

const mockSignature = 'mockSignature';
mockEsm('#src/utils/sign.js', () => ({
sign: () => mockSignature,
signAsync: jest.fn().mockResolvedValue(mockSignature),
}));

const { MockQueries } = await import('#src/test-utils/tenant.js');

const url = 'https://logto.gg';
Expand Down Expand Up @@ -78,7 +84,11 @@ describe('triggerInteractionHooksIfNeeded()', () => {

expect(findAllHooks).toHaveBeenCalled();
expect(post).toHaveBeenCalledWith(url, {
headers: { 'user-agent': 'Logto (https://logto.io)', bar: 'baz' },
headers: {
'user-agent': 'Logto (https://logto.io/)',
bar: 'baz',
'logto-signature-256': mockSignature,
},
json: {
hookId: 'foo',
event: 'PostSignIn',
Expand Down
13 changes: 10 additions & 3 deletions packages/core/src/libraries/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import {
} from '@logto/schemas';
import { generateStandardId } from '@logto/shared';
import { conditional, pick, trySafe } from '@silverhand/essentials';
import type { Response } from 'got';
import { got, HTTPError } from 'got';
import { type Response } from 'got';
import type Provider from 'oidc-provider';

import { LogEntry } from '#src/middleware/koa-audit-log.js';
import type Queries from '#src/tenants/Queries.js';
import { consoleLog } from '#src/utils/console.js';
import { signAsync } from '#src/utils/sign.js';

const parseResponse = ({ statusCode, body }: Response) => ({
statusCode,
Expand Down Expand Up @@ -81,7 +82,7 @@ export const createHookLibrary = (queries: Queries) => {
} satisfies Omit<HookEventPayload, 'hookId'>;

await Promise.all(
rows.map(async ({ config: { url, headers, retries }, id }) => {
rows.map(async ({ config: { url, headers, retries }, id, signingKey }) => {
consoleLog.info(`\tTriggering hook ${id} due to ${hookEvent} event`);
const json: HookEventPayload = { hookId: id, ...payload };
const logEntry = new LogEntry(`TriggerHook.${hookEvent}`);
Expand All @@ -91,7 +92,13 @@ export const createHookLibrary = (queries: Queries) => {
// Trigger web hook and log response
await got
.post(url, {
headers: { 'user-agent': 'Logto (https://logto.io)', ...headers },
headers: {
'user-agent': 'Logto (https://logto.io/)',
...headers,
...conditional(
signingKey && { 'logto-signature-256': await signAsync(signingKey, payload) }
),
},
json,
retry: { limit: retries ?? 3 },
timeout: { request: 10_000 },
Expand Down
31 changes: 31 additions & 0 deletions packages/core/src/utils/sign.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { sign, signAsync } from './sign.js';

describe('sign', () => {
it('should generate correct signature with both async and sync version', async () => {
const signingKey = 'foo';
const payload = {
bar: 'bar',
foo: 'foo',
};

const signature = sign(signingKey, payload);
const signatureByAsync = await signAsync(signingKey, payload);
const expectedResult =
'sha256=436958f1dbfefab37712fb3927760490fbf7757da8c0b2306ee7b485f0360eee';

expect(signature).toBe(expectedResult);
expect(signatureByAsync).toBe(expectedResult);
});

it('should generate correct signature if payload is empty with both async and sync version', async () => {
const signingKey = 'foo';
const payload = {};
const signature = sign(signingKey, payload);
const signatureByAsync = await signAsync(signingKey, payload);
const expectedResult =
'sha256=c76356efa19d219d1d7e08ccb20b1d26db53b143156f406c99dcb8e0876d6c55';

expect(signature).toBe(expectedResult);
expect(signatureByAsync).toBe(expectedResult);
});
});
14 changes: 14 additions & 0 deletions packages/core/src/utils/sign.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createHmac } from 'node:crypto';
import { promisify } from 'node:util';

export const sign = (signingKey: string, payload: Record<string, unknown>): string => {
const hmac = createHmac('sha256', signingKey);
const payloadString = JSON.stringify(payload);
hmac.update(payloadString);
return `sha256=${hmac.digest('hex')}`;
};

export const signAsync = async (signingKey: string, payload: Record<string, unknown>) =>
promisify<string>((callback) => {
callback(null, sign(signingKey, payload));
})();

0 comments on commit e80604c

Please sign in to comment.