Skip to content

Commit 3a979b6

Browse files
committed
feat(js-api-client): Crystallize Signature helper
1 parent 6e7cc51 commit 3a979b6

File tree

6 files changed

+93
-1
lines changed

6 files changed

+93
-1
lines changed

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ So far, the available helpers are:
2020
- Folders
2121
- CustomerManager
2222
- Subscription Contract Manager
23+
- Signature Verification
2324

2425
## Installation
2526

@@ -473,6 +474,39 @@ An Update method exists as well:
473474
await CrystallizeSubscriptionContractManager.update(contractId, cleanUpdateContract);
474475
```
475476

477+
## Signature Verification
478+
479+
The full documentation is here https://crystallize.com/learn/developer-guides/api-overview/signature-verification
480+
This library makes it simple, assuming:
481+
482+
- you have your `CRYSTALLIZE_SIGNATURE_SECRET` from the environment variable
483+
- you retrieve the Signature from the Header in `signatureJwt`
484+
485+
you can use the `createSignatureVerifier`
486+
487+
```javascript
488+
const guard = createSignatureVerifier({
489+
secret: `${process.env.CRYSTALLIZE_SIGNATURE_SECRET}`,
490+
sha256: (data: string) => crypto.createHash('sha256').update(data).digest('hex'),
491+
jwtVerify: (token: string, secret: string) => jwt.verify(token, secret) as CrystallizeSignature,
492+
});
493+
494+
guard(signatureJwt, {
495+
url: request.url, // full URL here, including https:// etc. request.href in some framework
496+
method: 'POST',
497+
body: 'THE RAW JSON BODY', // the library parse it for you cf. doc
498+
});
499+
```
500+
501+
If the signature is not valid:
502+
503+
- JWT signature is not verified
504+
- HMAC is invalid (man in the middle)
505+
506+
The guard function will trigger an exception.
507+
508+
> We let you provide the `sha256` and `jwtVerify` methods to stay agnostic of any library.
509+
476510
## Mass Call Client
477511

478512
Sometimes, when you have many calls to do, whether they are queries or mutations, you want to be able to manage them asynchronously. This is the purpose of the Mass Call Client. It will let you be asynchronous, managing the heavy lifting of lifecycle, retry, incremental increase or decrease of the pace, etc.

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

src/core/verifySignature.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { CrystallizeSignature } from '../types/signature';
2+
3+
export type SimplifiedRequest = {
4+
url?: string;
5+
method?: string;
6+
body?: any;
7+
};
8+
9+
export type CreateSignatureVerifierParams = {
10+
sha256: (data: string) => string;
11+
jwtVerify: (token: string, secret: string, options?: any) => CrystallizeSignature;
12+
secret: string;
13+
};
14+
15+
export const createSignatureVerifier = ({ sha256, jwtVerify, secret }: CreateSignatureVerifierParams) => {
16+
return (signature: string, request: SimplifiedRequest): any => {
17+
try {
18+
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');
30+
}
31+
return payload;
32+
} catch (exception: any) {
33+
throw new Error('Invalid signature. ' + exception.message);
34+
}
35+
};
36+
};

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export * from './core/search';
88
export * from './core/subscription';
99
export * from './core/customer';
1010
export * from './core/pricing';
11+
export * from './core/verifySignature';
1112
export * from './types/product';
1213
export * from './types/order';
1314
export * from './types/payment';

src/types/signature.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ export type CrystallizeSignature = {
77
userId?: string;
88
tenantId: string;
99
tenantIdentifier: string;
10+
hmac: string;
1011
};

tests/signature.test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const { createSignatureVerifier } = require('../dist/index.js');
2+
var crypto = require('crypto');
3+
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+
}),
11+
});
12+
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+
);
20+
});

0 commit comments

Comments
 (0)