Skip to content

Fix HMAC validation for Banking webhooks #1476

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

Merged
merged 3 commits into from
Mar 4, 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
61 changes: 61 additions & 0 deletions src/__mocks__/notification/accountHolderCreated.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"data": {
"balancePlatform": "YOUR_BALANCE_PLATFORM",
"accountHolder": {
"contactDetails": {
"email": "test@adyen.com",
"phone": {
"number": "0612345678",
"type": "mobile"
},
"address": {
"houseNumberOrName": "23",
"city": "Amsterdam",
"country": "NL",
"postalCode": "12345",
"street": "Main Street 1"
}
},
"description": "Shelly Eller",
"legalEntityId": "LE00000000000000000001",
"reference": "YOUR_REFERENCE-2412C",
"capabilities": {
"issueCard": {
"enabled": true,
"requested": true,
"allowed": false,
"verificationStatus": "pending"
},
"receiveFromTransferInstrument": {
"enabled": true,
"requested": true,
"allowed": false,
"verificationStatus": "pending"
},
"sendToTransferInstrument": {
"enabled": true,
"requested": true,
"allowed": false,
"verificationStatus": "pending"
},
"sendToBalanceAccount": {
"enabled": true,
"requested": true,
"allowed": false,
"verificationStatus": "pending"
},
"receiveFromBalanceAccount": {
"enabled": true,
"requested": true,
"allowed": false,
"verificationStatus": "pending"
}
},
"id": "AH00000000000000000001",
"status": "active"
}
},
"environment": "test",
"timestamp": "2024-12-15T15:42:03+01:00",
"type": "balancePlatform.accountHolder.created"
}
18 changes: 18 additions & 0 deletions src/__tests__/hmacValidator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import HmacValidator from "../utils/hmacValidator";
import {NotificationItem, NotificationRequestItem } from "../typings/notification/models";
import { ApiConstants } from "../constants/apiConstants";
import NotificationRequestService from "../notification/notificationRequest";
import { readFileSync } from "fs";

const key = "DFB1EB5485895CFA84146406857104ABB4CBCABDC8AAF103A624C8F6A3EAAB00";
const expectedSign = "ZNBPtI+oDyyRrLyD1XirkKnQgIAlFc07Vj27TeHsDRE=";
Expand Down Expand Up @@ -127,4 +128,21 @@ describe("HMAC Validator", function (): void {
notification.notificationItems![0].additionalData![ApiConstants.HMAC_SIGNATURE] = "notValidSign";
expect(hmacValidator.validateHMAC(notification.notificationItems![0], key)).toBeFalsy();
});

it("should calculate Banking webhook correctly", function (): void {
const data = readFileSync("./src/__mocks__/notification/accountHolderCreated.json", "utf8");
const encrypted = hmacValidator.calculateHmac(data, "11223344D785FBAE710E7F943F307971BB61B21281C98C9129B3D4018A57B2EB");

expect(encrypted).toEqual("UVBzHbDayhfT1XgaRGAkuKvxwoxrLoVCBdfi3WZU8lI=");
});

it("should validate Banking webhook correctly", function (): void {
const hmacKey = "11223344D785FBAE710E7F943F307971BB61B21281C98C9129B3D4018A57B2EB";
const hmacSignature = "UVBzHbDayhfT1XgaRGAkuKvxwoxrLoVCBdfi3WZU8lI=";
const data = readFileSync("./src/__mocks__/notification/accountHolderCreated.json", "utf8");
const isValid = hmacValidator.validateHMACSignature(hmacKey, hmacSignature, data);

expect(isValid).toBeTruthy;
});

});
44 changes: 43 additions & 1 deletion src/utils/hmacValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,26 @@ class HmacValidator {
public static HMAC_SHA256_ALGORITHM = "sha256";
public static DATA_SEPARATOR = ":";

/**
* Calculate HMAC signature of the payload data
* @param data payload as String or as NotificationRequestItem
* @param key HMAC key
* @returns HMAC signature
*/
public calculateHmac(data: string | NotificationRequestItem, key: string): string {
const dataString = typeof data !== "string" ? this.getDataToSign(data) : data;
const rawKey = Buffer.from(key, "hex");
return createHmac(HmacValidator.HMAC_SHA256_ALGORITHM, rawKey).update(dataString, "utf8").digest("base64");
}

/**
* @deprecated use Use validateHMACSignature with correct parameter order instead
* Validate HMAC signature for Banking webhooks
* @param hmacKey
* @param hmacSign
* @param notification
* @returns
*/
public validateBankingHMAC(hmacKey: string, hmacSign: string, notification: string): boolean {
const expectedSign = createHmac(HmacValidator.HMAC_SHA256_ALGORITHM, Buffer.from(hmacSign, "hex")).update(notification, "utf8").digest("base64");
if(hmacKey?.length === expectedSign.length) {
Expand All @@ -44,6 +58,30 @@ class HmacValidator {
return false;
}

/**
* Validate HMAC signature for Banking/Management webhooks
* @param hmacKey HMAC key
* @param hmacSignature HMAC signature to validate
* @param data webhook payload (as string)
* @returns true when HMAC signature is valid
*/
public validateHMACSignature(hmacKey: string, hmacSignature: string, data: string): boolean {
const expectedSign = createHmac(HmacValidator.HMAC_SHA256_ALGORITHM, Buffer.from(hmacKey, "hex")).update(data, "utf8").digest("base64");
if(hmacSignature?.length === expectedSign.length) {
return timingSafeEqual(
Buffer.from(expectedSign, "base64"),
Buffer.from(hmacSignature, "base64")
);
}
return false;
}

/**
* Validate HMAC signature for Payment webhooks
* @param notificationRequestItem webhook payload (as NotificationRequestItem object)
* @param key HMAC key
* @returns true when HMAC signature is valid
*/
public validateHMAC(notificationRequestItem: NotificationRequestItem, key: string): boolean {
if (notificationRequestItem.additionalData?.[ApiConstants.HMAC_SIGNATURE]) {
const expectedSign = this.calculateHmac(notificationRequestItem, key);
Expand All @@ -55,7 +93,6 @@ class HmacValidator {
);
}
return false;

}
throw Error(`Missing ${ApiConstants.HMAC_SIGNATURE}`);
}
Expand All @@ -64,6 +101,11 @@ class HmacValidator {
return !Object.values(item).every((value): boolean => typeof value === "string");
}

/**
* extract fields to be used to calculate the HMAC signature
* @param notificationRequestItem webhook payload
* @returns data to sign (as string)
*/
public getDataToSign(notificationRequestItem: DataToSign): string {
if (this.isNotificationRequestItem(notificationRequestItem)) {
const signedDataList = [];
Expand Down