HMAC Security
Customers Bank uses HMAC security to sign all webhook events sent to the callbackURL
specified in the webhook subscription. The HMAC signature is also hashed using HMAC-SHA256 with the secretText
specified in the subscription. The callback URL needs to implement HMAC signature verification to ensure the authenticity of a webhook invocation.
The secretText
should be Base64 encoded when subscribing. In the example below, the secret text my-secret
has been Base64 encoded as bXktc2VjcmV0
Example Subscription:
POST /webhooks/v1/ HTTP/1.1
X-Idempotency-Key: 628bab13-6177-4ba5-b604-2fc8b9a0178f
Authorization: ****
Host: cubi-sandbox-api.customersbank.com
{
"eventTypeName":"transfers.book-completed",
"callbackUrl":"https://webhook.site/f57f777c-1274-41c4-aa97-af9e25782d6c",
"secretText":"bXktc2VjcmV0",
"description":"Transfers completed"
}
Example Callback (using the subscription example above):
POST /api/cubix/webhooks HTTP/1.1
X-Idempotency-Key: f553bece-a863-4061-b5f0-92f850c6b08a
X-Event-Type: transfers.book-completed
Authorization-Timestamp: Tue, 10 Sep 2024 13:10:32 GMT
Authorization: HMAC-SHA256 Signature=4OOstBbS4iOHeWEqnIF2nSOrG+9MKWsBVWCGDgU7CJk=
Host: webhook.site
{"Id":"4c1d8cc1-1ef6-411f-8078-b1e10139e992"}
HMAC Signature Verification
In order to verify the authenticity of an invocation on the callback URL, clients must reconstruct the HMAC signature and confirm it matches the signature value specified in the Authorization
header of the callback. If the callback URL is not properly verifying the HMAC signature, the callback URL may be susceptible to malicious request attempts from third parties.
Constructing a valid HMAC signature
-
Create a SHA256 hash of the request body content and convert the hash to a Base64 encoded string
-
Construct a signature to be signed in the format:
<callbackURL path and query> <Authorization-Timestamp>;<callbackUrl host>;<request body hash>
<callBackUrl path and query>
should be the absolute path and query properties specified in thecallbackUrl
in the webhook subscription<Authorization-Timestamp>
should be the value of the request headerAuthorization-Timestamp
<callbackUrl host>
should be the host specified in thecallbackUrl
in the webhook subscription<request body hash>
should be the BASE64 encoded SHA256 hash created in the first step
Using the example callback above, the signature before signing would be:
/f57f777c-1274-41c4-aa97-af9e25782d6c Tue, 10 Sep 2024 13:10:32 GMT;webhook.site;71MyZ3d9CKN7W9gnIXskBMB2zIWLAmMEM/j2qN3odnU=
-
Hash the constructed signature by creating an HMAC-SHA256 hash using the same
secretKey
specified in the webhook subscription. -
The reconstructed hashed signature should match the value of the signature provided in the request
Authorization
header, if they do not match, the webhook callback should be rejected.
Example HMAC Verification
This is a naive verification example (hardcoded secret, etc.) meant for demonstration only. Using the example webhook callback payload above, you should be able to derive the same values in the example below.
[HttpPost]
public async Task<IActionResult> CubiWebhookPost()
{
// uri = https://webhook.site/f57f777c-1274-41c4-aa97-af9e25782d6c
var uri = new Uri(Request.GetEncodedUrl());
// content = {"Id":"4c1d8cc1-1ef6-411f-8078-b1e10139e992"}
var content = await Request.GetRawBodyAsync();
// authorizationHeader = HMAC-SHA256 Signature=4OOstBbS4iOHeWEqnIF2nSOrG+9MKWsBVWCGDgU7CJk=
var authorizationHeader = Request.Headers.Authorization[0];
// authorizationTimestamp = Tue, 10 Sep 2024 13:10:32 GMT
var authorizationTimestamp = Request.Headers["Authorization-Timestamp"][0];
// authorizationHeader = HMAC-SHA256 Signature=4OOstBbS4iOHeWEqnIF2nSOrG+9MKWsBVWCGDgU7CJk=
// signatureToVerify = 4OOstBbS4iOHeWEqnIF2nSOrG+9MKWsBVWCGDgU7CJk=
var signatureToVerify = authorizationHeader.Remove(0, "HMAC-SHA256 Signature=".Length);
// 1. Create a SHA256 hash of the request body content and convert the hash to a Base64 encoded string
// content = {"Id":"4c1d8cc1-1ef6-411f-8078-b1e10139e992"}
// contentHash = 71MyZ3d9CKN7W9gnIXskBMB2zIWLAmMEM/j2qN3odnU=
var contentHashAlgorithm = SHA256.Create();
var contentHash = Convert.ToBase64String(contentHashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(content)));
// 2. Construct a signature to be signed in the format:
// /f57f777c-1274-41c4-aa97-af9e25782d6c
// Tue, 10 Sep 2024 13:10:32 GMT;webhook.site;71MyZ3d9CKN7W9gnIXskBMB2zIWLAmMEM/j2qN3odnU=
var signatureToSign = $"{uri.PathAndQuery}\n{authorizationTimestamp};{uri.Authority};{contentHash}";
// 3. Hash the constructed signature by creating an HMAC-SHA256 hash using the same `secretKey` specified in the webhook subscription.
// signature = 4OOstBbS4iOHeWEqnIF2nSOrG+9MKWsBVWCGDgU7CJk=
var hmacSha256 = new HMACSHA256(Encoding.UTF8.GetBytes("my-secret"));
var signature = Convert.ToBase64String(hmacSha256.ComputeHash(Encoding.UTF8.GetBytes(signatureToSign)));
// 4. The reconstructed hashed signature should match the value of the signature provided in the request `Authorization` header, if they do not match, the webhook callback should be rejected.
if (signature != signatureToVerify)
{
return Unauthorized();
}
// Do work
return Ok();
}
Updated 5 days ago