From 65a6698ba279e547f9312e5e94ef622873b7bacf Mon Sep 17 00:00:00 2001 From: Roach Date: Wed, 21 Sep 2022 23:53:34 -0700 Subject: [PATCH 1/5] Added request signing Added helper and request logic to implement hmac request signing for webhooks --- app/server.js | 35 ++++++++++++++++++++++++----------- app/webflow.js | 24 +++++++++++++++++++++++- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/app/server.js b/app/server.js index e7dc6c4..5a86787 100644 --- a/app/server.js +++ b/app/server.js @@ -1,6 +1,8 @@ import Webflow from "webflow-api"; import App from "./webflow.js"; import Fastify from "fastify"; +import pinoInspector from "pino-inspector"; + // Load environment variables from .env file const { CLIENT_ID, CLIENT_SECRET, SERVER_HOST, PORT } = process.env; @@ -14,21 +16,32 @@ const server = Fastify({ // Response to Webhooks server.post("/webhook", async (req, reply) => { - // Get site ID from webhook payload - const { site } = req.body; + // Get signature and timestamp headers for validation + const request_signature = req.headers["x-webflow-signature"]; + const request_timestamp = req.headers["x-webflow-timestamp"]; + + // Validate the request signature to ensure this request came from Webflow + if (app.validateRequestSignature(request_signature, request_timestamp, req.body)){ + // Get site ID from webhook payload + const { site } = req.body; + + // Get the site's access token + const token = await app.data.get(site); - // Get the site's access token - const token = await app.data.get(site); + // Initialize a new Webflow client + const webflow = new Webflow({ token }); - // Initialize a new Webflow client - const webflow = new Webflow({ token }); + // Make calls to the Webflow API + const user = await webflow.get("/user"); - // Make calls to the Webflow API - const user = await webflow.get("/user"); - // Do other stuff with the API... + // Do other stuff with the API... - // Return a 200 response to Webflow - reply.statusCode = 200; + // Return a 200 response to Webflow + reply.statusCode = 200; + } else { + // Return a 403 response to Webflow if the request doesn't pass validation + reply.statusCode = 403; + } }); // Install the App diff --git a/app/webflow.js b/app/webflow.js index 8e8e11e..5beb71a 100644 --- a/app/webflow.js +++ b/app/webflow.js @@ -1,7 +1,10 @@ -import { AuthorizationCode } from "simple-oauth2"; import Client from "webflow-api"; +import crypto from "crypto"; +import { AuthorizationCode } from "simple-oauth2"; import { Level } from "level"; +const {CLIENT_SECRET} = process.env; + class App { /** * @param {string} clientId The OAuth client ID for the app @@ -26,6 +29,25 @@ class App { authorizeHost: "https://webflow.com", }, }); + + // Webhook Request Validation + this.validateRequestSignature = function(request_signature, request_timestamp, request_body){ + // Concatinate the request timestamp header and request body + const content = Number(request_timestamp) + ":" + JSON.stringify(request_body); + + // Generate an HMAC signature from the timestamp and body + const hmac = crypto + .createHmac('sha256', CLIENT_SECRET) + .update(content) + .digest('hex'); + + // Create a Buffers from the generated signature and signature header + const hmac_buffer = Buffer.from(hmac); + const signature_buffer = Buffer.from(request_signature); + + // Compare generated signature with signature header checksum + return crypto.timingSafeEqual(hmac_buffer, signature_buffer); + } } /** From 6a33d4a02a62bc579df397b4d2520cefbb81f4ca Mon Sep 17 00:00:00 2001 From: Roach Date: Thu, 22 Sep 2022 00:11:40 -0700 Subject: [PATCH 2/5] Shortened param names Shortened param names, added client secret to method imputs --- app/server.js | 2 +- app/webflow.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/server.js b/app/server.js index 5a86787..d1c7748 100644 --- a/app/server.js +++ b/app/server.js @@ -21,7 +21,7 @@ server.post("/webhook", async (req, reply) => { const request_timestamp = req.headers["x-webflow-timestamp"]; // Validate the request signature to ensure this request came from Webflow - if (app.validateRequestSignature(request_signature, request_timestamp, req.body)){ + if (app.validateRequestSignature(request_signature, request_timestamp, req.body, CLIENT_SECRET)){ // Get site ID from webhook payload const { site } = req.body; diff --git a/app/webflow.js b/app/webflow.js index 5beb71a..29d1717 100644 --- a/app/webflow.js +++ b/app/webflow.js @@ -31,19 +31,19 @@ class App { }); // Webhook Request Validation - this.validateRequestSignature = function(request_signature, request_timestamp, request_body){ + this.validateRequestSignature = function(signature, timestamp, body_json, consumer_secret){ // Concatinate the request timestamp header and request body - const content = Number(request_timestamp) + ":" + JSON.stringify(request_body); + const content = Number(timestamp) + ":" + JSON.stringify(body_json); // Generate an HMAC signature from the timestamp and body const hmac = crypto - .createHmac('sha256', CLIENT_SECRET) + .createHmac('sha256', consumer_secret) .update(content) .digest('hex'); // Create a Buffers from the generated signature and signature header const hmac_buffer = Buffer.from(hmac); - const signature_buffer = Buffer.from(request_signature); + const signature_buffer = Buffer.from(signature); // Compare generated signature with signature header checksum return crypto.timingSafeEqual(hmac_buffer, signature_buffer); From 330bd18416984b81467490fe1c40af2b8b663ee3 Mon Sep 17 00:00:00 2001 From: Roach Date: Thu, 22 Sep 2022 00:47:33 -0700 Subject: [PATCH 3/5] Added timestamp validation Added timestamp validation --- app/webflow.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/webflow.js b/app/webflow.js index 29d1717..8060870 100644 --- a/app/webflow.js +++ b/app/webflow.js @@ -32,6 +32,11 @@ class App { // Webhook Request Validation this.validateRequestSignature = function(signature, timestamp, body_json, consumer_secret){ + // Return false if timestamp is more than 5 minutes old + if (((Date.now() - Number(timestamp)) / 60000) > 5){ + return false + }; + // Concatinate the request timestamp header and request body const content = Number(timestamp) + ":" + JSON.stringify(body_json); From 8e485ed08f6d06b80d0fdb2941eb247e61aeafc3 Mon Sep 17 00:00:00 2001 From: Roach Date: Thu, 22 Sep 2022 00:49:00 -0700 Subject: [PATCH 4/5] Removed unused import --- app/server.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/server.js b/app/server.js index d1c7748..1a1f5a8 100644 --- a/app/server.js +++ b/app/server.js @@ -1,8 +1,6 @@ import Webflow from "webflow-api"; import App from "./webflow.js"; import Fastify from "fastify"; -import pinoInspector from "pino-inspector"; - // Load environment variables from .env file const { CLIENT_ID, CLIENT_SECRET, SERVER_HOST, PORT } = process.env; From b2f383d03744d69daa9e7d31868f4b512e84ac40 Mon Sep 17 00:00:00 2001 From: Roach Date: Thu, 22 Sep 2022 00:49:40 -0700 Subject: [PATCH 5/5] Update webflow.js --- app/webflow.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/webflow.js b/app/webflow.js index 8060870..a4a6006 100644 --- a/app/webflow.js +++ b/app/webflow.js @@ -3,8 +3,6 @@ import crypto from "crypto"; import { AuthorizationCode } from "simple-oauth2"; import { Level } from "level"; -const {CLIENT_SECRET} = process.env; - class App { /** * @param {string} clientId The OAuth client ID for the app