How to authenticate the webhook event
The webhook notification is an HTTP POST request sent to your web server. Your server must listen for it and then authenticate it with HMAC before you can process the message.
Verify the request by using the unique secret
provided when the webhook was registered
along with the Host
, X-ms-date
, X-ms-content-sha256
and Authorization
headers of the request.
The authorization is an SHA-256 HMAC hash of the following and is created using the secret
specified for the webhook:
- Request date
- Content of the webhook payload
- Webhook URI
How HMAC works
- Secret Key: A secret key is shared between the sender and the receiver.
- Message: The message to be authenticated.
- Hash Function: A cryptographic hash function (e.g., SHA-256) is used.
- HMAC Generation:
- The message and the secret key are combined in a specific way.
- The combined data is hashed using the cryptographic hash function.
- The result is the HMAC value.
- Verification:
- The receiver uses the same secret key and hash function to generate an HMAC value for the received message.
- The receiver compares the generated HMAC value with the HMAC value sent with the message.
- If they match, the message is verified as authentic and unaltered.
How to get the secret
When you first register for a webhook, you will receive a response including a secret
.
For example:
{
"url": "https://webhook.site/e2cee29b-012e-4f1d-8ef4-e95fd74a7a63",
"events": ["epayments.payment.created.v1", ...]
}
{
"id":"0b52e5bd-be1e-42a1-acf2-e331f52c68ac",
"//": "👇 This is the important bit"
"secret":"A0+AeKBRG2KRGvnNwJpQlb6IJFk48CKXCIcrLoHncVJKDILsQSxS6NWCccwWm6r6FhGKhiHTBsG2wo/xU6FY/A=="
}
If you have lost the secret, see How can I replace a webhook?.
How to verify a received request
You will receive an HTTP post with this format:
POST https://webhook.site/e2cee29b-012e-4f1d-8ef4-e95fd74a7a63
x-ms-date = Thu, 30 Mar 2023 08:38:32 GMT
x-ms-content-sha256 = lNlsp1XA03N34HrQsVzPgJKtC+r7l/RBF4V3JQUWMj4=
Authorization = HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature=agAiSyogQbDHpeucoNwYz+yAr5nJ+v+zasdkSbqzv+U=
{
"some-unique-content":"ee6e441b-cc4a-46f8-895d-a5af79bcc233/hello-world"
}
To verify the request:
-
Check that the content has not been modified
Hash the serialized request content using SHA256 encryption then base64 encode it. This hash should match the header value 'x-ms-content-sha256'.
'X-Ms-Content-Sha256': 'lNlsp1XA03N34HrQsVzPgJKtC+r7l/RBF4V3JQUWMj4='
-
Verify the authentication header
Concatenate request method, path and query, date, host, and content hash (from previous step) in this format:
POST\n\<pathAndQuery\>\n\<date\>;\<host\>;\<hash\>
Please note the use of
\n
not\r\n
.Sign it with the hmac-sha256 and your secret. This should match the Signature part of the 'authorization' header in the request you received.
Authorization: HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature=agAiSyogQbDHpeucoNwYz+yAr5nJ+v+zasdkSbqzv+U=
Sample Code
The following are examples of authenticating the HTTP requests.
- JavaScript
- .Net C#
'use strict';
const assert = require('node:assert');
const { describe, it } = require('node:test');
const crypto = require('crypto');
describe('Sample Code', () => {
it('Verifying Hmac Headers', () => {
const secret = 'A0+AeKBRG2KRGvnNwJpQlb6IJFk48CKXCIcrLoHncVJKDILsQSxS6NWCccwWm6r6FhGKhiHTBsG2wo/xU6FY/A==';
const request = {
method: 'POST',
url: 'https://webhook.site/e2cee29b-012e-4f1d-8ef4-e95fd74a7a63',
pathAndQuery: '/e2cee29b-012e-4f1d-8ef4-e95fd74a7a63', // this is the path and query part of the target URL
headers: {
'X-Ms-Date': 'Thu, 30 Mar 2023 08:38:32 GMT' ,
'X-Ms-Content-Sha256': 'lNlsp1XA03N34HrQsVzPgJKtC+r7l/RBF4V3JQUWMj4=' ,
'Host': 'webhook.site', // this is the host part of the target URL
'Authorization': 'HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature=agAiSyogQbDHpeucoNwYz+yAr5nJ+v+zasdkSbqzv+U='
},
content: '{"some-unique-content":"ee6e441b-cc4a-46f8-895d-a5af79bcc233/hello-world"}'
};
// Verify content
const expectedContentHash = crypto
.createHash('sha256')
.update(request.content)
.digest('base64')
assert.equal(request.headers['X-Ms-Content-Sha256'], expectedContentHash, 'Content hash was not valid');
// Verify signature
const expectedSignedString =
`${request.method}\n` +
`${request.pathAndQuery}\n` +
`${request.headers['X-Ms-Date']};${request.headers['Host']};${request.headers['X-Ms-Content-Sha256']}`
const expectedSignature = crypto.createHmac('sha256', secret)
.update(expectedSignedString)
.digest('base64')
const expectedAuth = `HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature=${expectedSignature}`
assert.equal(expectedAuth, request.headers.Authorization, "Authorization was not valid");
});
});
using System.Security.Cryptography;
using System.Text;
public class SampleCode
{
public void Verifying_Hmac_Headers()
{
var secret = "A0+AeKBRG2KRGvnNwJpQlb6IJFk48CKXCIcrLoHncVJKDILsQSxS6NWCccwWm6r6FhGKhiHTBsG2wo/xU6FY/A==";
var request = new
{
Method = "POST",
Url = "https://webhook.site/e2cee29b-012e-4f1d-8ef4-e95fd74a7a63",
PathAndQuery = "/e2cee29b-012e-4f1d-8ef4-e95fd74a7a63", // this is the path and query part of the target URL
Headers = new Dictionary<string, string>
{
{ "X-Ms-Date", "Thu, 30 Mar 2023 08:38:32 GMT" },
{ "X-Ms-Content-Sha256", "lNlsp1XA03N34HrQsVzPgJKtC+r7l/RBF4V3JQUWMj4=" },
{ "Host", "webhook.site" }, // this is the host part of the target URL
{
"Authorization",
"HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature=agAiSyogQbDHpeucoNwYz+yAr5nJ+v+zasdkSbqzv+U="
}
},
Content = """{"some-unique-content":"ee6e441b-cc4a-46f8-895d-a5af79bcc233/hello-world"}"""
};
// Verify content
var contentHashInBytes = SHA256.HashData(Encoding.UTF8.GetBytes(request.Content));
var expectedContentHash = Convert.ToBase64String(contentHashInBytes);
Assert.Equal(expected: expectedContentHash, actual: request.Headers["X-Ms-Content-Sha256"]);
// Verify signature
var expectedSignedString =
$"{request.Method}\n" +
$"{request.PathAndQuery}\n" +
$"{request.Headers["X-Ms-Date"]};{request.Headers["Host"]};{request.Headers["X-Ms-Content-Sha256"]}";
using var hmacSha256 = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hmacSha256Bytes = Encoding.UTF8.GetBytes(expectedSignedString);
var hmacSha256Hash = hmacSha256.ComputeHash(hmacSha256Bytes);
var expectedSignature = Convert.ToBase64String(hmacSha256Hash);
var expectedAuthorization = $"HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature={expectedSignature}";
Assert.Equal(expected: expectedAuthorization, actual: request.Headers["Authorization"]);
}
}