Webhook
1. Introduction and Preparation
Overview and Importance
This section covers how QI Tech sends webhooks with signed headers, highlighting the importance of decrypting and validating these headers to ensure secure communication.
Request Format
Webhook requests will be sent to the URL configured for receiving webhooks. . They have a specific format for headers and body, which is detailed below.
Request Headers
{
"AUTHORIZATION": "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJwYXlsb2FkX21kNSI6IjRhNjAzZjBmMGU3ZGRkZTlkYTJhMGFkM2QzNDFmNzRiIiwidGltZXN0YW1wIjoiMjAyMy0wNi0zMFQxODo1MjoyNy44ODU3MzFaIiwibWV0aG9kIjoiUE9TVCIsInVyaSI6Ii90ZXN0In0.AcNiJqXDdVmlXSbPI6bH41n0KXz9JwVVMgo4Ivqsq5UZjM2WBOTWw3aAvIMAAhjK5OdrURD4cX3dbbnRgzxspUckANRt0hVHRKSkhROHBfZxuTXVfv8oYzwghwiO2MatPBsroC9Vxbh-DEVQJIBigtN9_D5bg8p2-mlVvoxou2I-EwZs",
"API-CLIENT-KEY": "20d6a816-9d21-4e29-bbe5-2ffb3baacfe9"
}
Request Body
{
"body_sample": "Exemplo de webhook"
}
2. Configuration and Decryption
Import libraries
Before starting the decryption and validation of webhooks, it is essential to import the necessary libraries in your preferred programming language. These libraries will facilitate working with JWTs, encryption, and other related aspects.
- Python
- PHP
- Node.js
- Java
- C#
import json
from datetime import datetime, timedelta
from hashlib import md5
from jose import jwt
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Signature\Algorithm\ES512;
use Jose\Component\Signature\JWSVerifier;
use Jose\Component\KeyManagement\JWKFactory;
use Jose\Component\Signature\Serializer\JWSSerializerManager;
use Jose\Component\Signature\Serializer\CompactSerializer;
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.io.pem.PemReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Security;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using Newtonsoft.Json;
using Jose;
Define variables
Define the necessary variables to handle the headers and body of the webhook. This includes the public key provided by QI Tech, used to decrypt and validate the webhook.
- Python
- PHP
- Node.js
- Java
- C#
headers = {
"AUTHORIZATION": "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJwYXlsb2FkX21kNSI6IjRhNjAzZjBmMGU3ZGRkZTlkYTJhMGFkM2QzNDFmNzRiIiwidGltZXN0YW1wIjoiMjAyMy0wNi0zMFQxODo1MjoyNy44ODU3MzFaIiwibWV0aG9kIjoiUE9TVCIsInVyaSI6Ii90ZXN0In0.AcNiJqXDdVmlXSbPI6bH41n0KXz9JwVVMgo4Ivqsq5UZjM2WBOTWw3aAvIMAAhjK5OdrURD4cX3dbbnRgzxspUckANRt0hVHRKSkhROHBfZxuTXVfv8oYzwghwiO2MatPBsroC9Vxbh-DEVQJIBigtN9_D5bg8p2-mlVvoxou2I-EwZs",
"API-CLIENT-KEY": "20d6a816-9d21-4e29-bbe5-2ffb3baacfe9",
}
body = {"body_sample": "Exemplo de webhook"}
authorization = headers.get("AUTHORIZATION")
$headers = [
"AUTHORIZATION" => "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJwYXlsb2FkX21kNSI6IjRhNjAzZjBmMGU3ZGRkZTlkYTJhMGFkM2QzNDFmNzRiIiwidGltZXN0YW1wIjoiMjAyMy0wNi0zMFQxODo1MjoyNy44ODU3MzFaIiwibWV0aG9kIjoiUE9TVCIsInVyaSI6Ii90ZXN0In0.AcNiJqXDdVmlXSbPI6bH41n0KXz9JwVVMgo4Ivqsq5UZjM2WBOTWw3aAvIMAAhjK5OdrURD4cX3dbbnRgzxspUckANRt0hVHRKSkhROHBfZxuTXVfv8oYzwghwiO2MatPBsroC9Vxbh-DEVQJIBigtN9_D5bg8p2-mlVvoxou2I-EwZs",
"API-CLIENT-KEY" => "20d6a816-9d21-4e29-bbe5-2ffb3baacfe9",
];
$body = ["body_sample" => "Exemplo de webhook"];
$authorization = $headers["AUTHORIZATION"];
const headers = {
AUTHORIZATION: 'eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJwYXlsb2FkX21kNSI6IjRhNjAzZjBmMGU3ZGRkZTlkYTJhMGFkM2QzNDFmNzRiIiwidGltZXN0YW1wIjoiMjAyMy0wNi0zMFQxODo1MjoyNy44ODU3MzFaIiwibWV0aG9kIjoiUE9TVCIsInVyaSI6Ii90ZXN0In0.AcNiJqXDdVmlXSbPI6bH41n0KXz9JwVVMgo4Ivqsq5UZjM2WBOTWw3aAvIMAAhjK5OdrURD4cX3dbbnRgzxspUckANRt0hVHRKSkhROHBfZxuTXVfv8oYzwghwiO2MatPBsroC9Vxbh-DEVQJIBigtN9_D5bg8p2-mlVvoxou2I-EwZs',
'API-CLIENT-KEY': '20d6a816-9d21-4e29-bbe5-2ffb3baacfe9'
};
const body = { body_sample: 'Exemplo de webhook' };
String authorization = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJwYXlsb2FkX21kNSI6IjRhNjAzZjBmMGU3ZGRkZTlkYTJhMGFkM2QzNDFmNzRiIiwidGltZXN0YW1wIjoiMjAyMy0wNi0zMFQxODo1MjoyNy44ODU3MzFaIiwibWV0aG9kIjoiUE9TVCIsInVyaSI6Ii90ZXN0In0.AcNiJqXDdVmlXSbPI6bH41n0KXz9JwVVMgo4Ivqsq5UZjM2WBOTWw3aAvIMAAhjK5OdrURD4cX3dbbnRgzxspUckANRt0hVHRKSkhROHBfZxuTXVfv8oYzwghwiO2MatPBsroC9Vxbh-DEVQJIBigtN9_D5bg8p2-mlVvoxou2I-EwZs";
var headers = new Dictionary<string, string>()
{
{ "AUTHORIZATION", "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJwYXlsb2FkX21kNSI6IjRhNjAzZjBmMGU3ZGRkZTlkYTJhMGFkM2QzNDFmNzRiIiwidGltZXN0YW1wIjoiMjAyMy0wNi0zMFQxODo1MjoyNy44ODU3MzFaIiwibWV0aG9kIjoiUE9TVCIsInVyaSI6Ii90ZXN0In0.AcNiJqXDdVmlXSbPI6bH41n0KXz9JwVVMgo4Ivqsq5UZjM2WBOTWw3aAvIMAAhjK5OdrURD4cX3dbbnRgzxspUckANRt0hVHRKSkhROHBfZxuTXVfv8oYzwghwiO2MatPBsroC9Vxbh-DEVQJIBigtN9_D5bg8p2-mlVvoxou2I-EwZs" },
{ "API-CLIENT-KEY", "20d6a816-9d21-4e29-bbe5-2ffb3baacfe9" }
};
var body = new Dictionary<string, string>()
{
{ "body_sample", "Exemplo de webhook" }
};
2. Insertion of Encryption Data and Performing Decryption
We insert the public key provided by QI Tech and perform the decryption of the webhook header. This key is crucial for decrypting the webhook headers.
- Python
- PHP
- Node.js
- Java
- C#
qi_public_key = """-----BEGIN PUBLIC KEY-----
{QI_PUBLIC_KEY}
-----END PUBLIC KEY-----"""
$qiPublicKey = "-----BEGIN PUBLIC KEY-----
{QI_PUBLIC_KEY}
-----END PUBLIC KEY-----";
const qiPublicKey = `-----BEGIN PUBLIC KEY-----
{QI_PUBLIC_KEY}
-----END PUBLIC KEY-----`;
String publicKeyStr = "{QI_PUBLIC_KEY}";
var authorization = headers["AUTHORIZATION"];
var qiPublicKey = @"-----BEGIN PUBLIC KEY-----
{QI_PUBLIC_KEY}
-----END PUBLIC KEY-----";
Decrypt the header
The decryption process is essential to verify the authenticity and integrity of the received webhook.
- Python
- PHP
- Node.js
- Java
- C#
try:
decoded_header = jwt.decode(token=authorization, key=qi_public_key)
except:
raise Exception("Decodification failed.")
$algorithmManager = new AlgorithmManager([new ES512()]);
$jwsVerifier = new JWSVerifier($algorithmManager);
$publicKey = JWKFactory::createFromKey($qiPublicKey, null, ['use' => 'sig']);
$serializerManager = new JWSSerializerManager([new CompactSerializer]);
$jws = $serializerManager->unserialize($authorization);
$decodedHeader = json_decode($jws->getPayload(), true);
const decodedHeader = jwt.verify(authorization, qiPublicKey);
private static Claims validate(final String encodedBody, String publicKeyStr){
try {
Security.addProvider(new BouncyCastleProvider());
final String pbKey = new String(Base64.getDecoder().decode(publicKeyStr));
Reader rdr = new StringReader(pbKey);
PemReader pemParser = new PemReader(rdr);
X509EncodedKeySpec spec = new X509EncodedKeySpec(pemParser.readPemObject().getContent());
KeyFactory kf = KeyFactory.getInstance("EC");
PublicKey publicKey = kf.generatePublic(spec);
return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(encodedBody).getBody();
} catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new IllegalStateException(e);
}
}
var key = ECDsa.Create();
key.ImportFromPem(qiPublicKey);
var decodedHeader = JWT.Decode<IDictionary<string, string>>(authorization, key);
3. Validation and Conclusion
Performing Validations
After decrypting the header, it is important to perform various validations to ensure that the webhook is valid and secure.
- Python
- PHP
- Node.js
- Java
- C#
assert decoded_header.get("method") == "POST"
assert decoded_header.get("uri") == "/client_webhook_endpoint"
assert (
decoded_header.get("payload_md5")
== md5(json.dumps(body).encode()).hexdigest()
)
assert (
(datetime.now() - timedelta(minutes=5))
< datetime.strptime(decoded_header.get("timestamp"), "%Y-%m-%dT%H:%M:%S.%fZ")
< (datetime.now() + timedelta(minutes=5))
)
$method = $decodedHeader["method"];
$uri = $decodedHeader["uri"];
$payloadMd5 = $decodedHeader["payload_md5"];
$timestamp = $decodedHeader["timestamp"];
assert($method === "POST");
assert($uri === "/client_webhook_endpoint");
assert($payloadMd5 === md5(json_encode($body, JSON_UNESCAPED_SLASHES)));
assert(
(new DateTime("now", new DateTimeZone("UTC")))->sub(new DateInterval("PT5M")) < DateTime::createFromFormat("Y-m-d\TH:i:s.u\Z", $timestamp) &&
DateTime::createFromFormat("Y-m-d\TH:i:s.u\Z", $timestamp) < (new DateTime("now", new DateTimeZone("UTC")))->add(new DateInterval("PT5M"))
);
if (decodedHeader.method !== 'POST') {
throw new Error('Invalid method');
}
if (decodedHeader.uri !== '/client_webhook_endpoint') {
throw new Error('Invalid URI');
}
const payloadMd5 = crypto
.createHash('md5')
.update(JSON.stringify(body))
.digest('hex');
if (decodedHeader.payload_md5 !== payloadMd5) {
throw new Error('Invalid payload MD5');
}
const timestamp = new Date(decodedHeader.timestamp);
const currentDateTime = new Date();
const fiveMinutesAgo = new Date(currentDateTime.getTime() - 5 * 60000);
const fiveMinutesAhead = new Date(currentDateTime.getTime() + 5 * 60000);
if (!(timestamp > fiveMinutesAgo && timestamp < fiveMinutesAhead)) {
throw new Error('Invalid timestamp');
}
Claims result = validate(authorization, publicKeyStr);
System.out.println(result);
System.out.println(result.get("method").equals("POST"));
System.out.println(result.get("uri").equals("/test"));
var method = decodedHeader["method"];
var uri = decodedHeader["uri"];
var payloadMd5 = decodedHeader["payload_md5"];
var timestamp = DateTime.Parse(decodedHeader["timestamp"]);
timestamp = timestamp.ToUniversalTime();
var bodyJson = JsonConvert.SerializeObject(body, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
Formatting = Formatting.None
});
using (var md5Hash = MD5.Create())
{
var calculatedMd5 = GetMd5Hash(md5Hash, bodyJson);
if (payloadMd5 != calculatedMd5)
{
throw new Exception("Payload MD5 verification failed.");
}
}
var currentTime = datetime.now;
var validTimeStart = currentTime.AddMinutes(-5);
var validTimeEnd = currentTime.AddMinutes(5);
if (timestamp < validTimeStart || timestamp > validTimeEnd)
{
throw new Exception("Timestamp verification failed.");
}
...
static string GetMd5Hash(MD5 md5Hash, string input)
{
byte[] data = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(input));
StringBuilder builder = new StringBuilder();
for (int i = 0; i < data.Length; i++)
{
builder.Append(data[i].ToString("x2"));
}
return builder.ToString();
}