Request authentication
An API request from the Webhooks API can be verified by using the Host
, X-ms-date
, X-ms-content-sha256
and Authorization
headers with the unique secret
specified when the webhook was registered with
POST:/webhooks/v1/webhooks
. For more info, see POST:/webhooks/v1/webhooks
.
The authorization is a SHA-256 HMAC hash of:
- Request date
- Content of the webhook payload
- Webook URI
that is created using the
secret
specified for the webhook.
Sample Data
Receiving the secret when registering webhook
POST https://api.vipps.no/webhooks/v1/webhooks
{
"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=="
}
Sample request sent to you with valid authorization and content
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"
}
How to verify a received 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) like so:
POST\n\<pathAndQuery\>\n\<date\>;\<host\>;\<hash\>
and sign it with the hmac-sha256 and your secret.
This ought to match the Signature part of the 'authorization' header in the request you received.
Please note the use of \n
not \r\n
.
Authorization: HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature=agAiSyogQbDHpeucoNwYz+yAr5nJ+v+zasdkSbqzv+U=
Sample Code
- 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"]);
}
}