Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DEVPL-2717] add hmac verification method to JS SDK #214

Merged
merged 10 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules
.DS_Store
/dist
/dist
.idea
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Webstorm workspace setup

24 changes: 13 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,34 @@
"test": "jest"
},
"dependencies": {
"url-join": "4.0.1",
"crypto-browserify": "^3.12.1",
"form-data": "^4.0.0",
"formdata-node": "^6.0.3",
"js-base64": "3.7.2",
"node-fetch": "2.7.0",
"qs": "6.11.2",
"readable-stream": "^4.5.2",
"js-base64": "3.7.2"
"url-join": "4.0.1"
},
"devDependencies": {
"@types/url-join": "4.0.1",
"@types/qs": "6.9.8",
"@types/jest": "29.5.5",
"@types/node": "17.0.33",
"@types/node-fetch": "2.6.9",
"@types/qs": "6.9.8",
"@types/readable-stream": "^4.0.15",
"webpack": "^5.94.0",
"ts-loader": "^9.3.1",
"@types/url-join": "4.0.1",
"jest": "29.7.0",
"@types/jest": "29.5.5",
"ts-jest": "29.1.1",
"jest-environment-jsdom": "29.7.0",
"@types/node": "17.0.33",
"prettier": "2.7.1",
"typescript": "4.6.4"
"ts-jest": "29.1.1",
"ts-loader": "^9.3.1",
"typescript": "4.6.4",
"webpack": "^5.94.0"
},
"browser": {
"fs": false,
"os": false,
"path": false
"path": false,
"crypto": false
}
}
8 changes: 8 additions & 0 deletions src/wrapper/WebflowClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,20 @@ import { OauthScope } from "../api/types/OAuthScope";
import * as core from "../core";
import * as errors from "../errors";
import { SDK_VERSION } from "../version";
import { Client as Webhooks } from "./WebhooksClient";

export class WebflowClient extends FernClient {
constructor(protected readonly _options: FernClient.Options) {
super(_options);
}

protected _webhooks: Webhooks | undefined;

public get webhooks(): Webhooks {
return (this._webhooks ??= new Webhooks(this._options));
}


/**
* @param clientId The OAuth client ID
* @param state The state
Expand Down
74 changes: 74 additions & 0 deletions src/wrapper/WebhooksClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Webhooks } from "../api/resources/webhooks/client/Client";
import crypto from "crypto";

// Extends the namespace declared in the Fern generated client
declare module "../api/resources/webhooks/client/Client" {
export namespace Webhooks {
interface RequestSignatureDetails {
/** The headers of the incoming webhook request as a record-like object */
headers: Record<string, string>;
/** The body of the incoming webhook request as a string */
body: string;
/** The secret key generated when creating the webhook or the OAuth client secret */
secret: string;
}
}
}

export class Client extends Webhooks {
constructor(protected readonly _options: Webhooks.Options) {
super(_options);
}

/**
* Verify that the signature on the webhook message is from Webflow
* @link https://developers.webflow.com/data/docs/working-with-webhooks#validating-request-signatures
*
* @param {Webhooks.RequestSignatureDetails.headers} requestSignatureDetails - details of the incoming webhook request
* @example
* function incomingWebhookRouteHandler(req, res) {
* const headers = req.headers;
* const body = JSON.stringify(req.body);
* const secret = getWebhookSecret(WEBHOOK_ID);
* const isAuthenticated = await client.webhooks.verifySignature({ headers, body, secret });
*
* if (isAuthenticated) {
* // Process the webhook
* } else {
* // Alert the user that the webhook is not authenticated
* }
* res.sendStatus(200);
* }
*
*/
public async verifySignature({ headers, body, secret }: Webhooks.RequestSignatureDetails): Promise<boolean> {
// Creates a HMAC signature following directions from https://developers.webflow.com/data/docs/working-with-webhooks#steps-to-validate-the-request-signature
const createHmac = async (signingSecret: string, message: string) => {
const encoder = new TextEncoder();

// Encode the signingSecret key
// @ts-expect-error TS2339: Property 'subtle' does not exist on type 'typeof import("crypto")'.
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(signingSecret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);

// Encode the message and compute HMAC signature
// @ts-expect-error TS2339: Property 'subtle' does not exist on type 'typeof import("crypto")'.
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(message));

// Convert signature to hex string
return Array.from(new Uint8Array(signature))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
};

const message = `${headers["x-webflow-timestamp"]}:${body}`;

const generatedSignature = await createHmac(secret, message);
return headers["x-webflow-signature"] === generatedSignature;
}
}
Loading