Skip to content

Commit 4a5ff59

Browse files
authoredFeb 21, 2025··
Merge pull request #214 from webflow/devpl-2717-add-HMAC-verification-method
[DEVPL-2717] add hmac verification method to JS SDK
2 parents 47496a1 + 86ed9de commit 4a5ff59

File tree

5 files changed

+451
-17
lines changed

5 files changed

+451
-17
lines changed
 

‎.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
node_modules
22
.DS_Store
3-
/dist
3+
/dist
4+
.idea

‎package.json

+13-11
Original file line numberDiff line numberDiff line change
@@ -12,32 +12,34 @@
1212
"test": "jest"
1313
},
1414
"dependencies": {
15-
"url-join": "4.0.1",
15+
"crypto-browserify": "^3.12.1",
1616
"form-data": "^4.0.0",
1717
"formdata-node": "^6.0.3",
18+
"js-base64": "3.7.2",
1819
"node-fetch": "2.7.0",
1920
"qs": "6.11.2",
2021
"readable-stream": "^4.5.2",
21-
"js-base64": "3.7.2"
22+
"url-join": "4.0.1"
2223
},
2324
"devDependencies": {
24-
"@types/url-join": "4.0.1",
25-
"@types/qs": "6.9.8",
25+
"@types/jest": "29.5.5",
26+
"@types/node": "17.0.33",
2627
"@types/node-fetch": "2.6.9",
28+
"@types/qs": "6.9.8",
2729
"@types/readable-stream": "^4.0.15",
28-
"webpack": "^5.94.0",
29-
"ts-loader": "^9.3.1",
30+
"@types/url-join": "4.0.1",
3031
"jest": "29.7.0",
31-
"@types/jest": "29.5.5",
32-
"ts-jest": "29.1.1",
3332
"jest-environment-jsdom": "29.7.0",
34-
"@types/node": "17.0.33",
3533
"prettier": "2.7.1",
36-
"typescript": "4.6.4"
34+
"ts-jest": "29.1.1",
35+
"ts-loader": "^9.3.1",
36+
"typescript": "4.6.4",
37+
"webpack": "^5.94.0"
3738
},
3839
"browser": {
3940
"fs": false,
4041
"os": false,
41-
"path": false
42+
"path": false,
43+
"crypto": false
4244
}
4345
}

‎src/wrapper/WebflowClient.ts

+8
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,20 @@ import { OauthScope } from "../api/types/OAuthScope";
44
import * as core from "../core";
55
import * as errors from "../errors";
66
import { SDK_VERSION } from "../version";
7+
import { Client as Webhooks } from "./WebhooksClient";
78

89
export class WebflowClient extends FernClient {
910
constructor(protected readonly _options: FernClient.Options) {
1011
super(_options);
1112
}
1213

14+
protected _webhooks: Webhooks | undefined;
15+
16+
public get webhooks(): Webhooks {
17+
return (this._webhooks ??= new Webhooks(this._options));
18+
}
19+
20+
1321
/**
1422
* @param clientId The OAuth client ID
1523
* @param state The state

‎src/wrapper/WebhooksClient.ts

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { Webhooks } from "../api/resources/webhooks/client/Client";
2+
import crypto from "crypto";
3+
4+
// Extends the namespace declared in the Fern generated client
5+
declare module "../api/resources/webhooks/client/Client" {
6+
export namespace Webhooks {
7+
interface RequestSignatureDetails {
8+
/** The headers of the incoming webhook request as a record-like object */
9+
headers: Record<string, string>;
10+
/** The body of the incoming webhook request as a string */
11+
body: string;
12+
/** The secret key generated when creating the webhook or the OAuth client secret */
13+
secret: string;
14+
}
15+
}
16+
}
17+
18+
export class Client extends Webhooks {
19+
constructor(protected readonly _options: Webhooks.Options) {
20+
super(_options);
21+
}
22+
23+
/**
24+
* Verify that the signature on the webhook message is from Webflow
25+
* @link https://developers.webflow.com/data/docs/working-with-webhooks#validating-request-signatures
26+
*
27+
* @param {Webhooks.RequestSignatureDetails.headers} requestSignatureDetails - details of the incoming webhook request
28+
* @example
29+
* function incomingWebhookRouteHandler(req, res) {
30+
* const headers = req.headers;
31+
* const body = JSON.stringify(req.body);
32+
* const secret = getWebhookSecret(WEBHOOK_ID);
33+
* const isAuthenticated = await client.webhooks.verifySignature({ headers, body, secret });
34+
*
35+
* if (isAuthenticated) {
36+
* // Process the webhook
37+
* } else {
38+
* // Alert the user that the webhook is not authenticated
39+
* }
40+
* res.sendStatus(200);
41+
* }
42+
*
43+
*/
44+
public async verifySignature({ headers, body, secret }: Webhooks.RequestSignatureDetails): Promise<boolean> {
45+
// Creates a HMAC signature following directions from https://developers.webflow.com/data/docs/working-with-webhooks#steps-to-validate-the-request-signature
46+
const createHmac = async (signingSecret: string, message: string) => {
47+
const encoder = new TextEncoder();
48+
49+
// Encode the signingSecret key
50+
// @ts-expect-error TS2339: Property 'subtle' does not exist on type 'typeof import("crypto")'.
51+
const key = await crypto.subtle.importKey(
52+
"raw",
53+
encoder.encode(signingSecret),
54+
{ name: "HMAC", hash: "SHA-256" },
55+
false,
56+
["sign"]
57+
);
58+
59+
// Encode the message and compute HMAC signature
60+
// @ts-expect-error TS2339: Property 'subtle' does not exist on type 'typeof import("crypto")'.
61+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(message));
62+
63+
// Convert signature to hex string
64+
return Array.from(new Uint8Array(signature))
65+
.map((b) => b.toString(16).padStart(2, "0"))
66+
.join("");
67+
};
68+
69+
const message = `${headers["x-webflow-timestamp"]}:${body}`;
70+
71+
const generatedSignature = await createHmac(secret, message);
72+
return headers["x-webflow-signature"] === generatedSignature;
73+
}
74+
}

‎yarn.lock

+354-5
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.