Webhook
Quando crei un'integrazione con Sibill, potresti essere interessato a ricevere eventi man mano che si verificano nelle entità dei tuoi account Sibill, in modo che i sistemi di backend possano eseguire azioni di conseguenza.
Come si fa
Per iniziare a ricevere eventi nella tua app:
- Crea un webhook endpoint per ricevere gli eventi tramite richieste
HTTPS
inPOST
- Chiedi la registrazione del webhook su Sibill
- Metti in sicurezza l'endpoint
Un webhook endpoint
è una destinazione sul tuo server che riceve richieste da Sibill.
Aggiungi un nuovo endpoint al tuo server e assicurati che sia pubblicamente accessibile, così possiamo inviare richieste in POST non autenticate.
Assicurati inoltre che non ci siano redirect dalla destinazione sul tuo server verso altre destinazioni. Se dovesse succedere, in qualsiasi momento, un redirect, la destinazione verrà identificata come invalida.
1 Creare un endpoint per rispondere al webhook
Crea un servizio HTTPS
che possa ricevere richieste con il metodo POST
.
Il servizio deve restituire una risposta con status code 2xx
il prima possibile e poi eseguire logiche di business per evitare eventuali timeout
.
Esempio
- PHP
- Elixir
// Set the content type to application/json
header('Content-Type: application/json');
// Get the raw POST data
$json = file_get_contents('php://input');
// Decode the JSON data
$data = json_decode($json, true);
// Prepare the response array
$response = [];
// Check for JSON decoding errors
if (json_last_error() !== JSON_ERROR_NONE) {
// If there's an error, include it in the response
$response['error'] = 'Invalid JSON';
} else {
// If the JSON is valid, include the parsed data
$response['received'] = $data;
}
// Always return a 200 status code
http_response_code(200);
echo json_encode($response);
defmodule MyApp.PostHandler do
@moduledoc"""
Make sure to add the `jason` dependency to `mix.exs`.
"""
import Plug.Conn
@behaviour Plug
def init(default), do: default
def call(conn, _opts) do
# Check if the request method is POST
if conn.method == "POST" do
# Read the request body
{:ok, body, conn} = read_body(conn)
# Process the body (for example, parse JSON)
case Jason.decode(body) do
{:ok, params} ->
# Send a response
conn
|> put_resp_content_type("application/json")
|> send_resp(200, "")
{:error, _reason} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(400, Jason.encode!(%{error: "Invalid JSON"}))
end
else
conn |> send_resp(404, Jason.encode!(%{error: "Not found"}))
end
end
end
# Add the plug to the router
# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :api do
plug :accepts, ["json"]
plug MyApp.PostHandler # Add your custom Plug here
end
scope "/api", MyAppWeb do
pipe_through :api
end
end
2 Registrare il tuo endpoint
Al momento la registrazione deve avvenire facendo richiesta all'indirizzo email api@sibill.it. In tale richiesta sarà necessario fornire l'indirizzo HTTPS dell'enpoint che risponderà alle chiamate.
3 Mettere in sicurezza l'endpoint
È fondamentale garantire che l'integrazione avvenga in totale sicurezza, in modo da essere certi che le richieste ricevute siano effettivamente originate da Sibill. Per questo motivo è necessario verificare le firme delle richieste ricevute.
In seguito sono stati portati alcuni esempi
Verifica manualmente la firma dei webhook
Puoi creare una soluzione personalizzata seguendo questa procedura oppure seguire gli esempi che trovi sotto.
L’intestazione X-Sibill-Signature
inclusa in ogni evento firmato contiene un timestamp e una firma che devi verificare.
Il timestamp ha un prefisso t=
e ogni firma ha un prefisso scheme. Attualmente, l’unico schema valido per le firme è v1
.
X-Sibill-Signature: t=1492774577, v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Sibill genera le firme utilizzando una modalità di autenticazione dei messaggi basata su hash (HMAC) con SHA-256. Per impedire gli attacchi basati su downgrade attack, ignora tutti gli schemi diversi da v1.
Per creare una soluzione manuale per la verifica delle firme, è necessario completare i seguenti passaggi:
Fase 1: estrarre il timestamp e la firma dall’intestazione
Per ottenere un elenco degli elementi, suddividi l’intestazione, utilizzando il carattere ,
come separatore. Poi suddividi ogni elemento, utilizzando il carattere = come separatore, per ottenere un prefisso e una coppia di valori. Il valore del prefisso t
corrisponde al timestamp e v1
corrisponde alla firma. Puoi ignorare tutti gli altri elementi.
Fase 2: preparare la stringa signed_payload
La stringa signed_payload
viene creata concatenando:
- Il timestamp (come stringa)
- Il carattere
.
- Il payload JSON effettivo, ovvero il corpo della richiesta
Fase 3: determinare la firma prevista
Calcola un HMAC con la funzione hash SHA256. Utilizza la chiave privata di firma dell’endpoint come chiave e la stringa signed_payload
come messaggio.
Fase 4: confrontare le firme
Confronta la firma nell’intestazione con la firma prevista. Per una corrispondenza di eguaglianza, calcola la differenza tra il timestamp corrente e quello ricevuto, poi decidi se la differenza rientra nella tolleranza. Per proteggerti dagli attacchi temporizzati, utilizza un confronto di stringhe a tempo costante per confrontare la firma prevista con ogni firma ricevuta.
Esempio di validazione della firma
- PHP
- Elixir
abstract class WebhookSignature
{
public static function verifyHeader($payload, $header, $secret)
{
// Extract timestamp and signatures from header
$timestamp = self::getTimestamp($header);
$signatures = self::getSignatures($header, 'v1');
if (-1 === $timestamp || empty($signatures)) {
return false;
}
// Check if expected signature is found in list of signatures from header
$signedPayload = "{$timestamp}.{$payload}";
$expectedSignature = self::computeSignature($signedPayload, $secret);
$signatureFound = false;
foreach ($signatures as $signature) {
if (\hash_equals($expectedSignature, $signature)) {
$signatureFound = true;
break;
}
}
return $signatureFound;
}
/**
* Extracts from the headers a prticular header given a key name.
*
* @param string $headers the headers of the reques
* @param string $header the header to be extracted
*
* @return the string value of the header
*/
private static function getHeader($headers, $header)
{
$items = \explode(',', $headers);
$headerValues = [];
foreach ($items as $item) {
$itemParts = \explode('=', $item, 2);
if (\trim($itemParts[0]) === $header) {
$headerValues[] = $itemParts[1];
}
}
return $headerValues;
}
private static function getTimestamp($header)
{
$res = self::getHeader($header, 't');
if (\count($res) != 1) {
return -1;
}
if (!\is_numeric($res[0])) {
return -1;
}
return (int) $res[0];
}
private static function getSignatures($header, $scheme)
{
return self::getHeader($headers, $scheme);
}
private static function computeSignature($payload, $secret)
{
return \hash_hmac('sha256', $payload, $secret);
}
}
defmodule SibillWebhookVerifier do
@doc """
Verifies the Sibill signature for a webhook call.
Args:
- `payload`: The payload of the webhook call as a string.
- `sig_header`: The value of the `X-Sibill-Signature` header.
- `secret`: Your Sibill secret key.
Returns:
- `:ok` if the signature is valid.
- `:error` if the signature is invalid.
"""
def verify_signature(payload, sig_header, secret) do
# Split the signature header into its components
components = String.split(sig_header, ",")
# Extract the timestamp and signature
{timestamp, signature} =
Enum.find(components, fn component ->
String.starts_with?(component, "t=")
end)
|> String.split("=")
|> then(fn [_, timestamp] -> timestamp end)
{signature, _} =
Enum.find(components, fn component ->
String.starts_with?(component, "v1=")
end)
|> String.split("=")
|> then(fn [_, signature] -> signature end)
# Generate the expected signature
message = "#{timestamp}.#{payload}"
expected_signature =
:crypto.mac(:hmac, :sha256, secret, message)
|> Base.encode16()
|> String.downcase()
# Compare the expected signature with the actual signature
if signature == expected_signature do
:ok
else
:error
end
end
end
Comportamenti di consegna degli eventi
Questa sezione ti aiuta a comprendere i vari comportamenti da aspettarsi per quanto riguarda il modo in cui Sibill invia gli eventi all’endpoint del webhook.
Tentativi automatici
Sibill tenta di consegnare gli eventi alla tua destinazione per un massimo di due giorni con un ritardo di tre ora ad ogni nuovo tentativo.
Ordine degli eventi
Sibill non garantisce la consegna degli eventi nell’ordine in cui sono stati generati nel sistema. Preparati a gestire la consegna in modo appropriato. Puoi anche utilizzare l’API per recuperare eventuali oggetti mancanti o per ricostruire uno stato che sembra non consistente.
Pratiche ottimali di utilizzo dei webhook
Questa sezione elenca alcune pratiche ottimali per proteggere gli endpoint del webhook e assicurarti che funzionino bene con la tua integrazione con Sibill.
Gestire gli eventi duplicati
A volte gli endpoint dei webhook potrebbero ricevere lo stesso evento più di una volta. Per proteggerti dalla ricezione di eventi duplicati, gestisci l'evento e poi non elaborare gli eventi già registrati; puoi affidarti a diversi campi tra cui updated_at
o dedurre la validità dell'evento dallo stato dell'entità.
Ascoltare solo i tipi di evento richiesti dalla tua integrazione
Configura gli endpoint dei webhook per ricevere solo i tipi di eventi richiesti dalla tua integrazione e ingorare gli altri. L'ascolto di tutti gli eventi appesantisce inutilmente il server e pertanto è sconsigliato.
Gestire gli eventi in modo asincrono
Configura il tuo sistema per elaborare i prossimi eventi con una coda asincrona. Se scegli di elaborare gli eventi in modo sincrono, potrebbero verificarsi problemi di scalabilità. Un forte aumento delle consegne di webhook (ad esempio, alla fine del mese, e secondo la periodicità del tuo business) può sovraccaricare gli host degli endpoint.
Ricevere eventi con un server HTTPS
Per ricevere eventi dal webhook di Sibill, il tuo server deve essere configurato correttamente per supportare il protocollo HTTPS e deve disporre di un certificato valido.
Impedire gli attacchi di tipo replay
Un attacco di tipo replay si verifica quando un utente malintenzionato intercetta un paylod valido e la sua firma e poi li ritrasmette. Per mitigare questi attacchi, Sibill include un timestamp nell’intestazione X-Sibill-Signature
. Dato che il timestamp fa parte del payload firmato, viene anch’esso verificato dalla firma: quindi un utente malintenzionato non può modificare il timestamp senza invalidare la firma. Se la firma è valida ma il timestamp è troppo vecchio, puoi fare in modo che la tua applicazione rifiuti il payload.
Restituire rapidamente una risposta 2xx
L’endpoint deve restituire rapidamente un codice di stato riuscito (2xx) prima che l’esecuzione di qualsiasi logica complessa possa causare un timeout. Ad esempio, è necessario restituire una risposta 200 prima che il sistema riceventi effettui qualsiasi operazione.