Skip to content

Commit

Permalink
feat(js-api-client): introducing createAsyncSignatureVerifier
Browse files Browse the repository at this point in the history
  • Loading branch information
Plopix committed Mar 13, 2024
1 parent 8b6b6bb commit 2f79087
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 50 deletions.
2 changes: 1 addition & 1 deletion components/js-api-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ This library makes it simple, assuming:
- you have your `CRYSTALLIZE_SIGNATURE_SECRET` from the environment variable
- you retrieve the Signature from the Header in `signatureJwt`

you can use the `createSignatureVerifier`
you can use the `createSignatureVerifier` OR `createAsyncSignatureVerifier` based on your preferences/libs you are using to verify and/or hash

```javascript
const guard = createSignatureVerifier({
Expand Down
2 changes: 1 addition & 1 deletion components/js-api-client/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@crystallize/js-api-client",
"license": "MIT",
"version": "2.2.1",
"version": "2.3.0",
"author": "Crystallize <[email protected]> (https://crystallize.com)",
"contributors": [
"Sébastien Morel <[email protected]>",
Expand Down
89 changes: 64 additions & 25 deletions components/js-api-client/src/core/verifySignature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@ export type SimplifiedRequest = {
webhookUrl?: string;
};

export type CreateSignatureVerifierParams = {
sha256: (data: string) => string;
jwtVerify: (token: string, secret: string, options?: any) => CrystallizeSignature;
secret: string;
};

const newQueryParams = (webhookUrl: string, receivedUrl: string): Record<string, string> => {
const parseQueryString = (url: string): Record<string, string> => {
const urlParams = new URL(url).searchParams;
Expand All @@ -34,30 +28,75 @@ const newQueryParams = (webhookUrl: string, receivedUrl: string): Record<string,
return result;
};

export type CreateAsyncSignatureVerifierParams = {
sha256: (data: string) => Promise<string>;
jwtVerify: (token: string, secret: string, options?: any) => Promise<CrystallizeSignature>;
secret: string;
};

const buildChallenge = (request: SimplifiedRequest) => {
return {
url: request.url,
method: request.method,
body: request.body ? JSON.parse(request.body) : null,
};
};
const buildGETSituationChallenge = (request: SimplifiedRequest) => {
if (request.url && request.webhookUrl && request.method && request.method.toLowerCase() === 'get') {
const body = newQueryParams(request.webhookUrl, request.url);
if (Object.keys(body).length > 0) {
return {
url: request.webhookUrl,
method: request.method,
body,
};
}
}
return null;
};

export const createAsyncSignatureVerifier = ({ sha256, jwtVerify, secret }: CreateAsyncSignatureVerifierParams) => {
return async (signature: string, request: SimplifiedRequest): Promise<CrystallizeSignature> => {
try {
const payload = await jwtVerify(signature, secret);
const isValid = async (challenge: any) => payload.hmac === (await sha256(JSON.stringify(challenge)));
const challenge = buildChallenge(request);
if (!(await isValid(challenge))) {
const newChallenge = buildGETSituationChallenge(request);
if (newChallenge && (await isValid(newChallenge))) {
return payload;
}
throw new Error('Invalid signature. HMAC does not match.');
}
return payload;
} catch (exception: any) {
throw new Error('Invalid signature. ' + exception.message);
}
};
};

/**
* @deprecated you should use the `CreateAsyncSignatureVerifierParams` type instead
*/
export type CreateSignatureVerifierParams = {
sha256: (data: string) => string;
jwtVerify: (token: string, secret: string, options?: any) => CrystallizeSignature;
secret: string;
};

/**
* @deprecated you should use the `createAsyncSignatureVerifier` function instead
*/
export const createSignatureVerifier = ({ sha256, jwtVerify, secret }: CreateSignatureVerifierParams) => {
return (signature: string, request: SimplifiedRequest): any => {
return (signature: string, request: SimplifiedRequest): CrystallizeSignature => {
try {
const payload = jwtVerify(signature, secret);
const isValid = (challenge: any) => payload.hmac === sha256(JSON.stringify(challenge));
const challenge = {
url: request.url,
method: request.method,
body: request.body ? JSON.parse(request.body) : null,
};
const challenge = buildChallenge(request);
if (!isValid(challenge)) {
// we are going to do another check here for the webhook payload situation
if (request.url && request.webhookUrl && request.method && request.method.toLowerCase() === 'get') {
const body = newQueryParams(request.webhookUrl, request.url);
if (Object.keys(body).length > 0) {
const newChallenge = {
url: request.webhookUrl,
method: request.method,
body,
};
if (isValid(newChallenge)) {
return payload;
}
}
const newChallenge = buildGETSituationChallenge(request);
if (newChallenge && isValid(newChallenge)) {
return payload;
}
throw new Error('Invalid signature. HMAC does not match.');
}
Expand Down
97 changes: 74 additions & 23 deletions components/js-api-client/tests/signature.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const { createSignatureVerifier } = require('../dist/index.js');
const { createSignatureVerifier, createAsyncSignatureVerifier } = require('../dist/index.js');
var crypto = require('crypto');

describe('Test Signature HMAC', () => {
describe('Test Signature HMAC with Deprecated Sync Function', () => {
test('Test With a Body', () => {
const guard = createSignatureVerifier({
secret: 'xXx',
Expand All @@ -11,13 +11,12 @@ describe('Test Signature HMAC', () => {
}),
});

expect(
guard('xXx.xXx.xXx', {
url: 'https://a17e-2601-645-4500-330-b07d-351d-ece7-41c1.ngrok.io/test/signature',
method: 'POST',
body: '{"item":{"get":{"id":"63f2d3b2a94533f79fc6397b","createdAt":"2023-02-20T01:58:10.000Z","updatedAt":"2023-02-23T07:58:34.685Z","name":"test"}}}',
}),
);
const payload = guard('xXx.xXx.xXx', {
url: 'https://a17e-2601-645-4500-330-b07d-351d-ece7-41c1.ngrok.io/test/signature',
method: 'POST',
body: '{"item":{"get":{"id":"63f2d3b2a94533f79fc6397b","createdAt":"2023-02-20T01:58:10.000Z","updatedAt":"2023-02-23T07:58:34.685Z","name":"test"}}}',
});
expect(payload.hmac).toBe('1101b34dac8c55e5590a37271f1c41c3d745463854613494a1624a15be24f1f8');
});

test('Test Without a Body App', () => {
Expand All @@ -28,14 +27,12 @@ describe('Test Signature HMAC', () => {
hmac: '157bee342a4856e14e964356fef54fd84b3a3508c1071ed674172d3f9b68892f',
}),
});

expect(
guard('xXx.xXx.xXx', {
url: 'https://helloworld.crystallize.app.local',
method: 'GET',
body: null,
}),
);
const payload = guard('xXx.xXx.xXx', {
url: 'https://helloworld.crystallize.app.local',
method: 'GET',
body: null,
});
expect(payload.hmac).toBe('157bee342a4856e14e964356fef54fd84b3a3508c1071ed674172d3f9b68892f');
});

test('Test Without a Body Webhook', () => {
Expand All @@ -46,12 +43,66 @@ describe('Test Signature HMAC', () => {
hmac: '61ce7a2e5072900a13369ac7f69b9e056e91c38c42f1bfe94389c80411d94b78',
}),
});
expect(
guard('xXx.xXx.xXx', {
url: 'https://webhook.site/b56870a7-9600-41a6-86a0-98be0c7532fd?id=65d8fc4ce2ba75beec481ec1&userId=61f9933ec63b0a44d5004c2d&tenantId=61f9937c3b63c8386ea9e153&type=document&language=en',
webhookUrl: 'https://webhook.site/b56870a7-9600-41a6-86a0-98be0c7532fd',
method: 'GET',

const payload = guard('xXx.xXx.xXx', {
url: 'https://webhook.site/b56870a7-9600-41a6-86a0-98be0c7532fd?id=65d8fc4ce2ba75beec481ec1&userId=61f9933ec63b0a44d5004c2d&tenantId=61f9937c3b63c8386ea9e153&type=document&language=en',
webhookUrl: 'https://webhook.site/b56870a7-9600-41a6-86a0-98be0c7532fd',
method: 'GET',
});

expect(payload.hmac).toBe('61ce7a2e5072900a13369ac7f69b9e056e91c38c42f1bfe94389c80411d94b78');
});
});

describe('Test Signature HMAC with Async verifier and ASync Functions', () => {
test('Test With a Body', async () => {
const guard = createAsyncSignatureVerifier({
secret: 'xXx',
sha256: async (data) => crypto.createHash('sha256').update(data).digest('hex'),
jwtVerify: async (token, secret) => ({
hmac: '1101b34dac8c55e5590a37271f1c41c3d745463854613494a1624a15be24f1f8',
}),
});

const payload = await guard('xXx.xXx.xXx', {
url: 'https://a17e-2601-645-4500-330-b07d-351d-ece7-41c1.ngrok.io/test/signature',
method: 'POST',
body: '{"item":{"get":{"id":"63f2d3b2a94533f79fc6397b","createdAt":"2023-02-20T01:58:10.000Z","updatedAt":"2023-02-23T07:58:34.685Z","name":"test"}}}',
});
expect(payload.hmac).toBe('1101b34dac8c55e5590a37271f1c41c3d745463854613494a1624a15be24f1f8');
});

test('Test Without a Body App', async () => {
const guard = createAsyncSignatureVerifier({
secret: 'xXx',
sha256: async (data) => crypto.createHash('sha256').update(data).digest('hex'),
jwtVerify: async (token, secret) => ({
hmac: '157bee342a4856e14e964356fef54fd84b3a3508c1071ed674172d3f9b68892f',
}),
});
const payload = await guard('xXx.xXx.xXx', {
url: 'https://helloworld.crystallize.app.local',
method: 'GET',
body: null,
});
expect(payload.hmac).toBe('157bee342a4856e14e964356fef54fd84b3a3508c1071ed674172d3f9b68892f');
});

test('Test Without a Body Webhook', async () => {
const guard = createAsyncSignatureVerifier({
secret: 'xXx',
sha256: async (data) => crypto.createHash('sha256').update(data).digest('hex'),
jwtVerify: async (token, secret) => ({
hmac: '61ce7a2e5072900a13369ac7f69b9e056e91c38c42f1bfe94389c80411d94b78',
}),
);
});

const payload = await guard('xXx.xXx.xXx', {
url: 'https://webhook.site/b56870a7-9600-41a6-86a0-98be0c7532fd?id=65d8fc4ce2ba75beec481ec1&userId=61f9933ec63b0a44d5004c2d&tenantId=61f9937c3b63c8386ea9e153&type=document&language=en',
webhookUrl: 'https://webhook.site/b56870a7-9600-41a6-86a0-98be0c7532fd',
method: 'GET',
});

expect(payload.hmac).toBe('61ce7a2e5072900a13369ac7f69b9e056e91c38c42f1bfe94389c80411d94b78');
});
});

0 comments on commit 2f79087

Please sign in to comment.