Webhook authentication Copy section link Copied!

Since your notification server will be publicly accessible, AgoraPay includes an authentication mechanism so you can verify that incoming events have been genuinely created by AgoraPay.

HMAC control Copy section link Copied!

Each webhook notification event contains, in addition to the JSON body, an "Authorization" HTTP header that includes a computed HMAC for the event sent.


The computed HMAC contains the following fields, separated by "/".

Fields

Description

Version

The current version of the authorisation header is 1.0.

Nonce

This is a 36-character random data type uuid v4.

Timestamp

A timestamp in seconds since 1/1/1970 UTC (linux time).

Key Id

This is the identifier for the key generated when the notification account was created.

HMAC

The HMAC is in hexadecimal. It is calculated with the generated HMAC key. This key is shared with the Marketplace. The string used to perform this calculation contains the following fields separated by a ';' :

  • The HTTP POST method (notifications);
  • The full URL used (e.g. https://myserver.com/path/function?param=value);
  • The SHA256, in hexadecimal, of the body transformed into a string (stringify);
  • The nonce;
  • The timestamp.

HMAC control example Copy section link Copied!

Let's take the following event received from a POST request on the webhook URL "https://yourserver.com/webhook", containing the following HTTP header and JSON payload body:

Http Headers
Copy
Copied
1
2
3
4
5
6
header: { 
authorization: 'hmac 1.0/2add0756-5a6b-4fe5-97a4-13363434a127/1620740102268/a167b5f6-f797-40b7-b743-e02e4eef4cc1/3734A7C5B6469496C362FF605D61A9C3F644B20D6B8F3334AE01BE575032AEE1',
'content-length': '118',
'content-type': 'application/json',
host: 'yourserver.com'
}
Copy
Copied
Request Body
Copy
Copied
1
2
3
4
5
6
7
8
{
"eventCode": "IPN",
"orderId": "3529421",
"amount": "1003.28",
"currency": "EUR",
"transactionId": "1948921",
"resultCode": "0"
}
Copy
Copied

The following fields would be extracted from the authorisation header:

Field name

Value

version

1.0

nonce

2add0756-5a6b-4fe5-97a4-13363434a127

timestamp

1620740102268

keyid

a167b5f6-f797-40b7-b743-e02e4eef4cc1

hmac

3734A7C5B6469496C362FF605D61A9C3F644B20D6B8F3334AE01BE5

75032AEE1

The marketplace must perform the following verifications on the extracted information:

  • Check the HMAC control version is the same as the one you implemented
  • Check that the key ID has the same value as your marketplace key ID
  • The HMAC you compute corresponds to the computed HMAC passed in the authorisation header,

To compute the HMAC, you need to:

  • Compute the SHA256 hash of the stringified and capitalised body: "3734A7C5B6469496C362FF605D61A9C3F644B20D6B8F3334AE01BE575032AEE1"
  • Build the request string: "POST;https://yourserver.com/webhook;3734A7C5B6469496C362FF605D61A9C3F644B20D6B8F3334AE01BE575032AEE1;2add0756-5a6b-4fe5-97a4-13363434a127;1620740102268"
  • Compute the HMAC of the string with your marketplace Hook HMAC Key and capitalise it.

HMAC calculation implementation Copy section link Copied!

As part of the developer tools, AgoraPay provides a reference implementation of the webhook server that you can use for building your marketplace.

Here are some code examples of the HMAC control algorithm:

Python
Node.js
Copy
Copied
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import hashlib
import hmac
import json


def verify_hmac(
headers: str,
header_auth_version: str,
server_url: str,
body: bytes,
method: str,
path: str,
key_id: str,
hmac_key: str,
) -> bool:
"""
Method verifying the HMAC
:param headers: request headers
:param header_auth_version: header authentication version
:param server_url: Actual URL of the server
:param body: request payload
:param method: request method (GET, POST...)
:param path: request path
:param key_id: specific key id of the hook
:param hmac_key: specific hmac key of the hook
:return: Success (or not) of auth
"""

authorization = headers["authorization"] if "authorization" in headers else None
if not authorization:
return False

auth = authorization.split(" ")
if auth[0] != "hmac":
return False

fields = auth[1].split("/")
if len(fields) != 5:
return False

if fields[0] != header_auth_version:
return False

if fields[3] != key_id:
return False

sha256 = hashlib.sha256(body).hexdigest().upper()
hmac_data = (
method
+ ";"
+ server_url
+ path
+ ";"
+ sha256
+ ";"
+ fields[1]
+ ";"
+ fields[2]
)
hmac_hash = (
hmac.new(str.encode(hmac_key), str.encode(hmac_data), hashlib.sha256)
.hexdigest()
.upper()
)

if not hmac.compare_digest(hmac_hash, fields[4]):
return False

return True
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
const crypto = require("crypto");

function verify_hmac(
headers,
headerAuthVersion,
serverUrl,
body,
method,
path,
keyId,
hmacKey
) {
const authorization = headers["authorization"];

if (!authorization) {
return false;
}

const auth = authorization.split(" ");
if (auth[0] !== "hmac") {
return false;
}

const fields = auth[1].split("/");

if (fields.length != 5) {
return False;
}

if (fields[0] != headerAuthVersion) {
return False;
}

if (fields[3] != keyId) {
return False;
}

const sha256Value = crypto
.createHash("sha256")
.update(JSON.stringify(body))
.digest("hex")
.toUpperCase();


const hmacData =
method +
";" +
serverUrl +
path +
";" +
sha256_value +
";" +
fields[1] +
";" +
fields[2];

const hmac = crypto
.createHmac("sha256", Buffer.from(hmacKey, "hex"))
.update(hmacData)
.digest("hex")
.toUpperCase();

return crypto.timingSafeEqual(hmac, fields[4]);
}
Copy
Copied