Skip to content

Commit 18d7461

Browse files
authored
Merge pull request #1476 from Adyen/hmac-validation-fix
Fix HMAC validation for Banking webhooks
2 parents 082578b + f82d250 commit 18d7461

File tree

3 files changed

+122
-1
lines changed

3 files changed

+122
-1
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"data": {
3+
"balancePlatform": "YOUR_BALANCE_PLATFORM",
4+
"accountHolder": {
5+
"contactDetails": {
6+
"email": "test@adyen.com",
7+
"phone": {
8+
"number": "0612345678",
9+
"type": "mobile"
10+
},
11+
"address": {
12+
"houseNumberOrName": "23",
13+
"city": "Amsterdam",
14+
"country": "NL",
15+
"postalCode": "12345",
16+
"street": "Main Street 1"
17+
}
18+
},
19+
"description": "Shelly Eller",
20+
"legalEntityId": "LE00000000000000000001",
21+
"reference": "YOUR_REFERENCE-2412C",
22+
"capabilities": {
23+
"issueCard": {
24+
"enabled": true,
25+
"requested": true,
26+
"allowed": false,
27+
"verificationStatus": "pending"
28+
},
29+
"receiveFromTransferInstrument": {
30+
"enabled": true,
31+
"requested": true,
32+
"allowed": false,
33+
"verificationStatus": "pending"
34+
},
35+
"sendToTransferInstrument": {
36+
"enabled": true,
37+
"requested": true,
38+
"allowed": false,
39+
"verificationStatus": "pending"
40+
},
41+
"sendToBalanceAccount": {
42+
"enabled": true,
43+
"requested": true,
44+
"allowed": false,
45+
"verificationStatus": "pending"
46+
},
47+
"receiveFromBalanceAccount": {
48+
"enabled": true,
49+
"requested": true,
50+
"allowed": false,
51+
"verificationStatus": "pending"
52+
}
53+
},
54+
"id": "AH00000000000000000001",
55+
"status": "active"
56+
}
57+
},
58+
"environment": "test",
59+
"timestamp": "2024-12-15T15:42:03+01:00",
60+
"type": "balancePlatform.accountHolder.created"
61+
}

src/__tests__/hmacValidator.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import HmacValidator from "../utils/hmacValidator";
22
import {NotificationItem, NotificationRequestItem } from "../typings/notification/models";
33
import { ApiConstants } from "../constants/apiConstants";
44
import NotificationRequestService from "../notification/notificationRequest";
5+
import { readFileSync } from "fs";
56

67
const key = "DFB1EB5485895CFA84146406857104ABB4CBCABDC8AAF103A624C8F6A3EAAB00";
78
const expectedSign = "ZNBPtI+oDyyRrLyD1XirkKnQgIAlFc07Vj27TeHsDRE=";
@@ -127,4 +128,21 @@ describe("HMAC Validator", function (): void {
127128
notification.notificationItems![0].additionalData![ApiConstants.HMAC_SIGNATURE] = "notValidSign";
128129
expect(hmacValidator.validateHMAC(notification.notificationItems![0], key)).toBeFalsy();
129130
});
131+
132+
it("should calculate Banking webhook correctly", function (): void {
133+
const data = readFileSync("./src/__mocks__/notification/accountHolderCreated.json", "utf8");
134+
const encrypted = hmacValidator.calculateHmac(data, "11223344D785FBAE710E7F943F307971BB61B21281C98C9129B3D4018A57B2EB");
135+
136+
expect(encrypted).toEqual("UVBzHbDayhfT1XgaRGAkuKvxwoxrLoVCBdfi3WZU8lI=");
137+
});
138+
139+
it("should validate Banking webhook correctly", function (): void {
140+
const hmacKey = "11223344D785FBAE710E7F943F307971BB61B21281C98C9129B3D4018A57B2EB";
141+
const hmacSignature = "UVBzHbDayhfT1XgaRGAkuKvxwoxrLoVCBdfi3WZU8lI=";
142+
const data = readFileSync("./src/__mocks__/notification/accountHolderCreated.json", "utf8");
143+
const isValid = hmacValidator.validateHMACSignature(hmacKey, hmacSignature, data);
144+
145+
expect(isValid).toBeTruthy;
146+
});
147+
130148
});

src/utils/hmacValidator.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,26 @@ class HmacValidator {
2727
public static HMAC_SHA256_ALGORITHM = "sha256";
2828
public static DATA_SEPARATOR = ":";
2929

30+
/**
31+
* Calculate HMAC signature of the payload data
32+
* @param data payload as String or as NotificationRequestItem
33+
* @param key HMAC key
34+
* @returns HMAC signature
35+
*/
3036
public calculateHmac(data: string | NotificationRequestItem, key: string): string {
3137
const dataString = typeof data !== "string" ? this.getDataToSign(data) : data;
3238
const rawKey = Buffer.from(key, "hex");
3339
return createHmac(HmacValidator.HMAC_SHA256_ALGORITHM, rawKey).update(dataString, "utf8").digest("base64");
3440
}
3541

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

61+
/**
62+
* Validate HMAC signature for Banking/Management webhooks
63+
* @param hmacKey HMAC key
64+
* @param hmacSignature HMAC signature to validate
65+
* @param data webhook payload (as string)
66+
* @returns true when HMAC signature is valid
67+
*/
68+
public validateHMACSignature(hmacKey: string, hmacSignature: string, data: string): boolean {
69+
const expectedSign = createHmac(HmacValidator.HMAC_SHA256_ALGORITHM, Buffer.from(hmacKey, "hex")).update(data, "utf8").digest("base64");
70+
if(hmacSignature?.length === expectedSign.length) {
71+
return timingSafeEqual(
72+
Buffer.from(expectedSign, "base64"),
73+
Buffer.from(hmacSignature, "base64")
74+
);
75+
}
76+
return false;
77+
}
78+
79+
/**
80+
* Validate HMAC signature for Payment webhooks
81+
* @param notificationRequestItem webhook payload (as NotificationRequestItem object)
82+
* @param key HMAC key
83+
* @returns true when HMAC signature is valid
84+
*/
4785
public validateHMAC(notificationRequestItem: NotificationRequestItem, key: string): boolean {
4886
if (notificationRequestItem.additionalData?.[ApiConstants.HMAC_SIGNATURE]) {
4987
const expectedSign = this.calculateHmac(notificationRequestItem, key);
@@ -55,7 +93,6 @@ class HmacValidator {
5593
);
5694
}
5795
return false;
58-
5996
}
6097
throw Error(`Missing ${ApiConstants.HMAC_SIGNATURE}`);
6198
}
@@ -64,6 +101,11 @@ class HmacValidator {
64101
return !Object.values(item).every((value): boolean => typeof value === "string");
65102
}
66103

104+
/**
105+
* extract fields to be used to calculate the HMAC signature
106+
* @param notificationRequestItem webhook payload
107+
* @returns data to sign (as string)
108+
*/
67109
public getDataToSign(notificationRequestItem: DataToSign): string {
68110
if (this.isNotificationRequestItem(notificationRequestItem)) {
69111
const signedDataList = [];

0 commit comments

Comments
 (0)