Skip to content

Commit 68c1ffa

Browse files
committed
fix: signature verification with GET
1 parent 38dca7a commit 68c1ffa

File tree

4 files changed

+110
-26
lines changed

4 files changed

+110
-26
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,20 @@ The guard function will trigger an exception.
558558

559559
> We let you provide the `sha256` and `jwtVerify` methods to stay agnostic of any library.
560560
561+
### Handling Signature with HTTP GET and Webhook
562+
563+
When using Webhook you may select GET as a HTTP Method. Even if you don't provide any GraphQL query Crystallize is going to call your endpoint with new parameters, in this case you need to provide an extra parameter to the `guard` function.
564+
565+
```javascript
566+
guard(signatureJwt, {
567+
url: request.url, // full URL here, including https:// etc. request.href in some framework
568+
webhookUrl: 'https://webhook.site/xxx', // the URL you have setup in the Webhook
569+
method: 'GET',
570+
}),
571+
```
572+
573+
Using that will instruct the JS API Client to extract URL Parameter and use it as a payload to verify the HMAC.
574+
561575
## Profiling the request
562576

563577
There is time when you want to log and see the raw queries sent to Crystallize and also the timings.

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.0.0",
4+
"version": "2.1.0",
55
"author": "Crystallize <[email protected]> (https://crystallize.com)",
66
"contributors": [
77
"Sébastien Morel <[email protected]>",

src/core/verifySignature.ts

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export type SimplifiedRequest = {
44
url?: string;
55
method?: string;
66
body?: any;
7+
webhookUrl?: string;
78
};
89

910
export type CreateSignatureVerifierParams = {
@@ -12,21 +13,53 @@ export type CreateSignatureVerifierParams = {
1213
secret: string;
1314
};
1415

16+
const newQueryParams = (webhookUrl: string, receivedUrl: string): Record<string, string> => {
17+
const parseQueryString = (url: string): Record<string, string> => {
18+
const urlParams = new URL(url).searchParams;
19+
let params: Record<string, string> = {};
20+
for (const [key, value] of urlParams.entries()) {
21+
params[key] = value;
22+
}
23+
return params;
24+
};
25+
const webhookOriginalParams = parseQueryString(webhookUrl);
26+
const receivedParams = parseQueryString(receivedUrl);
27+
const result: Record<string, string> = {};
28+
for (const [key, value] of Object.entries(receivedParams)) {
29+
if (!webhookOriginalParams.hasOwnProperty(key)) {
30+
result[key] = value;
31+
}
32+
}
33+
34+
return result;
35+
};
36+
1537
export const createSignatureVerifier = ({ sha256, jwtVerify, secret }: CreateSignatureVerifierParams) => {
1638
return (signature: string, request: SimplifiedRequest): any => {
1739
try {
1840
const payload = jwtVerify(signature, secret);
19-
const isValid =
20-
payload.hmac ===
21-
sha256(
22-
JSON.stringify({
23-
url: request.url,
24-
method: request.method,
25-
body: JSON.parse(request.body),
26-
}),
27-
);
28-
if (!isValid) {
29-
throw new Error('Invalid signature. HMAC does not match');
41+
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+
};
47+
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 > 1) {
52+
const newChallenge = {
53+
url: request.webhookUrl,
54+
method: request.method,
55+
body,
56+
};
57+
if (isValid(newChallenge)) {
58+
return payload;
59+
}
60+
}
61+
}
62+
throw new Error('Invalid signature. HMAC does not match.');
3063
}
3164
return payload;
3265
} catch (exception: any) {

tests/signature.test.js

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,57 @@
11
const { createSignatureVerifier } = require('../dist/index.js');
22
var crypto = require('crypto');
33

4-
test('Test Signature HMAC', () => {
5-
const guard = createSignatureVerifier({
6-
secret: 'xXx',
7-
sha256: (data) => crypto.createHash('sha256').update(data).digest('hex'),
8-
jwtVerify: (token, secret) => ({
9-
hmac: '1101b34dac8c55e5590a37271f1c41c3d745463854613494a1624a15be24f1f8',
10-
}),
4+
describe('Test Signature HMAC', () => {
5+
test('Test With a Body', () => {
6+
const guard = createSignatureVerifier({
7+
secret: 'xXx',
8+
sha256: (data) => crypto.createHash('sha256').update(data).digest('hex'),
9+
jwtVerify: (token, secret) => ({
10+
hmac: '1101b34dac8c55e5590a37271f1c41c3d745463854613494a1624a15be24f1f8',
11+
}),
12+
});
13+
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+
);
1121
});
1222

13-
expect(
14-
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-
);
23+
test('Test Without a Body App', () => {
24+
const guard = createSignatureVerifier({
25+
secret: 'xXx',
26+
sha256: (data) => crypto.createHash('sha256').update(data).digest('hex'),
27+
jwtVerify: (token, secret) => ({
28+
hmac: '157bee342a4856e14e964356fef54fd84b3a3508c1071ed674172d3f9b68892f',
29+
}),
30+
});
31+
32+
expect(
33+
guard('xXx.xXx.xXx', {
34+
url: 'https://helloworld.crystallize.app.local',
35+
method: 'GET',
36+
body: null,
37+
}),
38+
);
39+
});
40+
41+
test('Test Without a Body Webhook', () => {
42+
const guard = createSignatureVerifier({
43+
secret: 'xXx',
44+
sha256: (data) => crypto.createHash('sha256').update(data).digest('hex'),
45+
jwtVerify: (token, secret) => ({
46+
hmac: '61ce7a2e5072900a13369ac7f69b9e056e91c38c42f1bfe94389c80411d94b78',
47+
}),
48+
});
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',
54+
}),
55+
);
56+
});
2057
});

0 commit comments

Comments
 (0)