Skip to content

Commit 4bdcd79

Browse files
committed
feat(js-api-client): introducing createAsyncSignatureVerifier
1 parent 1c6d7b1 commit 4bdcd79

File tree

4 files changed

+140
-50
lines changed

4 files changed

+140
-50
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,7 @@ This library makes it simple, assuming:
533533
- you have your `CRYSTALLIZE_SIGNATURE_SECRET` from the environment variable
534534
- you retrieve the Signature from the Header in `signatureJwt`
535535

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

538538
```javascript
539539
const guard = createSignatureVerifier({

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@crystallize/js-api-client",
33
"license": "MIT",
4-
"version": "2.2.1",
4+
"version": "2.3.0",
55
"author": "Crystallize <[email protected]> (https://crystallize.com)",
66
"contributors": [
77
"Sébastien Morel <[email protected]>",

src/core/verifySignature.ts

Lines changed: 64 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,6 @@ export type SimplifiedRequest = {
77
webhookUrl?: string;
88
};
99

10-
export type CreateSignatureVerifierParams = {
11-
sha256: (data: string) => string;
12-
jwtVerify: (token: string, secret: string, options?: any) => CrystallizeSignature;
13-
secret: string;
14-
};
15-
1610
const newQueryParams = (webhookUrl: string, receivedUrl: string): Record<string, string> => {
1711
const parseQueryString = (url: string): Record<string, string> => {
1812
const urlParams = new URL(url).searchParams;
@@ -34,30 +28,75 @@ const newQueryParams = (webhookUrl: string, receivedUrl: string): Record<string,
3428
return result;
3529
};
3630

31+
export type CreateAsyncSignatureVerifierParams = {
32+
sha256: (data: string) => Promise<string>;
33+
jwtVerify: (token: string, secret: string, options?: any) => Promise<CrystallizeSignature>;
34+
secret: string;
35+
};
36+
37+
const buildChallenge = (request: SimplifiedRequest) => {
38+
return {
39+
url: request.url,
40+
method: request.method,
41+
body: request.body ? JSON.parse(request.body) : null,
42+
};
43+
};
44+
const buildGETSituationChallenge = (request: SimplifiedRequest) => {
45+
if (request.url && request.webhookUrl && request.method && request.method.toLowerCase() === 'get') {
46+
const body = newQueryParams(request.webhookUrl, request.url);
47+
if (Object.keys(body).length > 0) {
48+
return {
49+
url: request.webhookUrl,
50+
method: request.method,
51+
body,
52+
};
53+
}
54+
}
55+
return null;
56+
};
57+
58+
export const createAsyncSignatureVerifier = ({ sha256, jwtVerify, secret }: CreateAsyncSignatureVerifierParams) => {
59+
return async (signature: string, request: SimplifiedRequest): Promise<CrystallizeSignature> => {
60+
try {
61+
const payload = await jwtVerify(signature, secret);
62+
const isValid = async (challenge: any) => payload.hmac === (await sha256(JSON.stringify(challenge)));
63+
const challenge = buildChallenge(request);
64+
if (!(await isValid(challenge))) {
65+
const newChallenge = buildGETSituationChallenge(request);
66+
if (newChallenge && (await isValid(newChallenge))) {
67+
return payload;
68+
}
69+
throw new Error('Invalid signature. HMAC does not match.');
70+
}
71+
return payload;
72+
} catch (exception: any) {
73+
throw new Error('Invalid signature. ' + exception.message);
74+
}
75+
};
76+
};
77+
78+
/**
79+
* @deprecated you should use the `CreateAsyncSignatureVerifierParams` type instead
80+
*/
81+
export type CreateSignatureVerifierParams = {
82+
sha256: (data: string) => string;
83+
jwtVerify: (token: string, secret: string, options?: any) => CrystallizeSignature;
84+
secret: string;
85+
};
86+
87+
/**
88+
* @deprecated you should use the `createAsyncSignatureVerifier` function instead
89+
*/
3790
export const createSignatureVerifier = ({ sha256, jwtVerify, secret }: CreateSignatureVerifierParams) => {
38-
return (signature: string, request: SimplifiedRequest): any => {
91+
return (signature: string, request: SimplifiedRequest): CrystallizeSignature => {
3992
try {
4093
const payload = jwtVerify(signature, secret);
4194
const isValid = (challenge: any) => payload.hmac === sha256(JSON.stringify(challenge));
42-
const challenge = {
43-
url: request.url,
44-
method: request.method,
45-
body: request.body ? JSON.parse(request.body) : null,
46-
};
95+
const challenge = buildChallenge(request);
4796
if (!isValid(challenge)) {
48-
// we are going to do another check here for the webhook payload situation
49-
if (request.url && request.webhookUrl && request.method && request.method.toLowerCase() === 'get') {
50-
const body = newQueryParams(request.webhookUrl, request.url);
51-
if (Object.keys(body).length > 0) {
52-
const newChallenge = {
53-
url: request.webhookUrl,
54-
method: request.method,
55-
body,
56-
};
57-
if (isValid(newChallenge)) {
58-
return payload;
59-
}
60-
}
97+
const newChallenge = buildGETSituationChallenge(request);
98+
if (newChallenge && isValid(newChallenge)) {
99+
return payload;
61100
}
62101
throw new Error('Invalid signature. HMAC does not match.');
63102
}

tests/signature.test.js

Lines changed: 74 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
const { createSignatureVerifier } = require('../dist/index.js');
1+
const { createSignatureVerifier, createAsyncSignatureVerifier } = require('../dist/index.js');
22
var crypto = require('crypto');
33

4-
describe('Test Signature HMAC', () => {
4+
describe('Test Signature HMAC with Deprecated Sync Function', () => {
55
test('Test With a Body', () => {
66
const guard = createSignatureVerifier({
77
secret: 'xXx',
@@ -11,13 +11,12 @@ describe('Test Signature HMAC', () => {
1111
}),
1212
});
1313

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

2322
test('Test Without a Body App', () => {
@@ -28,14 +27,12 @@ describe('Test Signature HMAC', () => {
2827
hmac: '157bee342a4856e14e964356fef54fd84b3a3508c1071ed674172d3f9b68892f',
2928
}),
3029
});
31-
32-
expect(
33-
guard('xXx.xXx.xXx', {
34-
url: 'https://helloworld.crystallize.app.local',
35-
method: 'GET',
36-
body: null,
37-
}),
38-
);
30+
const payload = guard('xXx.xXx.xXx', {
31+
url: 'https://helloworld.crystallize.app.local',
32+
method: 'GET',
33+
body: null,
34+
});
35+
expect(payload.hmac).toBe('157bee342a4856e14e964356fef54fd84b3a3508c1071ed674172d3f9b68892f');
3936
});
4037

4138
test('Test Without a Body Webhook', () => {
@@ -46,12 +43,66 @@ describe('Test Signature HMAC', () => {
4643
hmac: '61ce7a2e5072900a13369ac7f69b9e056e91c38c42f1bfe94389c80411d94b78',
4744
}),
4845
});
49-
expect(
50-
guard('xXx.xXx.xXx', {
51-
url: 'https://webhook.site/b56870a7-9600-41a6-86a0-98be0c7532fd?id=65d8fc4ce2ba75beec481ec1&userId=61f9933ec63b0a44d5004c2d&tenantId=61f9937c3b63c8386ea9e153&type=document&language=en',
52-
webhookUrl: 'https://webhook.site/b56870a7-9600-41a6-86a0-98be0c7532fd',
53-
method: 'GET',
46+
47+
const payload = guard('xXx.xXx.xXx', {
48+
url: 'https://webhook.site/b56870a7-9600-41a6-86a0-98be0c7532fd?id=65d8fc4ce2ba75beec481ec1&userId=61f9933ec63b0a44d5004c2d&tenantId=61f9937c3b63c8386ea9e153&type=document&language=en',
49+
webhookUrl: 'https://webhook.site/b56870a7-9600-41a6-86a0-98be0c7532fd',
50+
method: 'GET',
51+
});
52+
53+
expect(payload.hmac).toBe('61ce7a2e5072900a13369ac7f69b9e056e91c38c42f1bfe94389c80411d94b78');
54+
});
55+
});
56+
57+
describe('Test Signature HMAC with Async verifier and ASync Functions', () => {
58+
test('Test With a Body', async () => {
59+
const guard = createAsyncSignatureVerifier({
60+
secret: 'xXx',
61+
sha256: async (data) => crypto.createHash('sha256').update(data).digest('hex'),
62+
jwtVerify: async (token, secret) => ({
63+
hmac: '1101b34dac8c55e5590a37271f1c41c3d745463854613494a1624a15be24f1f8',
64+
}),
65+
});
66+
67+
const payload = await guard('xXx.xXx.xXx', {
68+
url: 'https://a17e-2601-645-4500-330-b07d-351d-ece7-41c1.ngrok.io/test/signature',
69+
method: 'POST',
70+
body: '{"item":{"get":{"id":"63f2d3b2a94533f79fc6397b","createdAt":"2023-02-20T01:58:10.000Z","updatedAt":"2023-02-23T07:58:34.685Z","name":"test"}}}',
71+
});
72+
expect(payload.hmac).toBe('1101b34dac8c55e5590a37271f1c41c3d745463854613494a1624a15be24f1f8');
73+
});
74+
75+
test('Test Without a Body App', async () => {
76+
const guard = createAsyncSignatureVerifier({
77+
secret: 'xXx',
78+
sha256: async (data) => crypto.createHash('sha256').update(data).digest('hex'),
79+
jwtVerify: async (token, secret) => ({
80+
hmac: '157bee342a4856e14e964356fef54fd84b3a3508c1071ed674172d3f9b68892f',
81+
}),
82+
});
83+
const payload = await guard('xXx.xXx.xXx', {
84+
url: 'https://helloworld.crystallize.app.local',
85+
method: 'GET',
86+
body: null,
87+
});
88+
expect(payload.hmac).toBe('157bee342a4856e14e964356fef54fd84b3a3508c1071ed674172d3f9b68892f');
89+
});
90+
91+
test('Test Without a Body Webhook', async () => {
92+
const guard = createAsyncSignatureVerifier({
93+
secret: 'xXx',
94+
sha256: async (data) => crypto.createHash('sha256').update(data).digest('hex'),
95+
jwtVerify: async (token, secret) => ({
96+
hmac: '61ce7a2e5072900a13369ac7f69b9e056e91c38c42f1bfe94389c80411d94b78',
5497
}),
55-
);
98+
});
99+
100+
const payload = await guard('xXx.xXx.xXx', {
101+
url: 'https://webhook.site/b56870a7-9600-41a6-86a0-98be0c7532fd?id=65d8fc4ce2ba75beec481ec1&userId=61f9933ec63b0a44d5004c2d&tenantId=61f9937c3b63c8386ea9e153&type=document&language=en',
102+
webhookUrl: 'https://webhook.site/b56870a7-9600-41a6-86a0-98be0c7532fd',
103+
method: 'GET',
104+
});
105+
106+
expect(payload.hmac).toBe('61ce7a2e5072900a13369ac7f69b9e056e91c38c42f1bfe94389c80411d94b78');
56107
});
57108
});

0 commit comments

Comments
 (0)