Webhook
1. Introdução e Preparação
Visão Geral e Importância
Esta seção aborda como a QI Tech envia webhooks com headers assinados, destacando a importância de descriptografar e validar esses headers para garantir segurança nas comunicações.
Formato das Requisições
As requisições de webhook serão enviadas para a URL configurada para recebimento dos webhooks. Elas possuem um formato específico de headers e body, detalhado a seguir.
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. Configuração e Descriptografia
Importar bibliotecas
Antes de começar a descriptografia e validação dos webhooks, é essencial importar as bibliotecas necessárias em sua linguagem de programação preferida. Estas bibliotecas facilitarão o trabalho com JWTs, criptografia e outros aspectos relacionados.
- 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;
Definir variáveis
Defina as variáveis necessárias para manipular os headers e o corpo do webhook. Isso inclui a chave pública fornecida pela QI Tech, utilizada para descriptografar e validar o 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. Inserção de Dados de Criptografia e Realização da Descriptografia
Inserimos a chave pública fornecida pela QI Tech e realizamos a descriptografia do header do webhook. Essa chave é crucial para a descriptografia dos headers do webhook.
- 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-----";
Realizar descriptografia do header
O processo de descriptografia é essencial para verificar a autenticidade e integridade do webhook recebido.
- 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. Validação e Conclusão
Realização de Validações
Após descriptografar o header, é importante realizar várias validações para garantir que o webhook é válido e seguro.
- 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();
}