Check the webhook signatures
Verify the events that Bloock sends to your webhook endpoints.
Bloock signs the webhook events it sends to your endpoints by including a signature in each event’s Bloock-Signature header. This allows you to verify that the events were sent by Bloock, not by a third party.
Before you can verify signatures, you need to retrieve your endpoint’s signing secret from your Dashboard’s Webhook's settings. Select an endpoint that you want to obtain the secret for, then click the Click to reveal button.

Bloock generates a unique secret key for each endpoint. If you use multiple endpoints, you must obtain a secret for each one you want to verify signatures on. After this setup, Bloock starts to sign each webhook it sends to the endpoint.
A replay attack is when an attacker intercepts a valid payload and its signature, then re-transmits them. To mitigate such attacks, Bloock includes a timestamp in the Bloock-Signature header. Because this timestamp is part of the signed payload, it is also verified by the signature, so an attacker can’t change the timestamp without invalidating the signature. If the signature is valid but the timestamp is too old, you can have your application reject the payload.
Bloock defines a default tolerance of ten minutes between the timestamp and the current time. You can enable and disable the tolerance control and change this tolerance by changing the number of minutes when verifying signatures.
Bloock requires the raw body of the request to perform signature verification. If you’re using a framework, make sure it doesn’t manipulate the raw body. Any manipulation to the raw body of the request causes the verification to fail.
Javascript
Java
Python
Golang
npm install @bloock/sdk --save
const { WebhookClient } = require("@bloock/sdk");
const express = require("express");
const app = express();
const port = 3000;
var bodyParser = require("body-parser");
const secretKey = "NHJTAE6ikKBccSaeCSBSWGdp7NmixXy7";
var options = {
inflate: true,
limit: "100kb",
type: "application/*",
};
app.use(bodyParser.raw(options));
app.post("/verify", async (req, res) => {
let enforceTolerance = false; // decide if you want to set tolerance when verifying
let body = req.body;
let header = req.get("Bloock-Signature");
let webhookClient = new WebhookClient();
const ok = await webhookClient.verifyWebhookSignature(body, header, secretKey, enforceTolerance);
if (!ok) {
console.error("Invalid Signature!");
} else {
console.log("Valid Signature!");
}
return res;
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
pip install bloock==SDK_VERSION
from flask import Flask
from flask import request
import bloock
from bloock.client.webhook import WebhookClient
app = Flask(__name__)
SECRET_KEY = "NHJTAE6ikKBccSaeCSBSWGdp7NmixXy7"
@app.route('/verify', methods=['POST'])
def index():
enforce_tolerance = False # decide if you want to set tolerance when verifying
body = request.data
bloock_signature = request.headers['Bloock-Signature']
webhook_client = WebhookClient()
ok = webhook_client.verify_webhook_signature(
body, bloock_signature, SECRET_KEY, enforce_tolerance)
if ok == False:
raise ValueError('Invalid Signature!')
else:
print('Valid Signature!')
return 'Finish'
app.run(host='0.0.0.0', port=81)
go get github.com/bloock/bloock-sdk-go/v2
package main
import (
"log"
"net/http"
"github.com/bloock/bloock-sdk-go/v2/client"
)
// SecretKey represents the client secret key associate to your webhook endpoint
const SecretKey = "NHJTAE6ikKBccSaeCSBSWGdp7NmixXy7"
func main() {
verifyHandler := func(w http.ResponseWriter, req *http.Request) {
enforceTolerance := false // decide if you want to set tolerance when verifying
body, err := io.ReadAll(req.Body)
if err != nil {
log.Fatalf("Cannot read body request: %v", err)
}
bloockSignature := req.Header.Get("Bloock-Signature")
webhookClient := client.NewWebhookClient()
ok, err := webhookClient.VerifyWebhookSignature(body, bloockSignature, SecretKey, false)
if err != nil {
log.Fatal(err)
}
if !ok {
log.Fatal("Invalid Signature!")
}
log.Println("Valid Signature!")
}
http.HandleFunc("/verify", verifyHandler)
log.Println("Listing for requests at http://localhost:8000/verify")
log.Fatal(http.ListenAndServe(":8000", nil))
}
The Bloock-Signature header included in each signed event contains a timestamp and one signature. The timestamp is prefixed by t=, signature is prefixed by v1=.
Bloock-Signature:
t=1492774577,
v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
This code shows how to verify your webhook events signatures.
Golang
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"strconv"
"strings"
"time"
)
const (
// SecretKey represents the client secret key associate to your webhook endpoint
SecretKey = "secret"
// DefaultTolerance indicates that signatures older than this will be rejected.
DefaultTolerance = 600 * time.Second
// signingVersion represents the version of the signature we currently use.
signingVersion = "v1"
)
var (
ErrInvalidHeader = errors.New("webhook has invalid Bloock-Signature header")
ErrNotSigned = errors.New("webhook has no Bloock-Signature header")
ErrNoValidSignature = errors.New("webhook had no valid signature")
ErrTooOld = errors.New("timestamp wasn't within tolerance")
)
type signedHeader struct {
timestamp time.Time
signature []byte
}
func main() {
verifyHandler := func(w http.ResponseWriter, req *http.Request) {
enforceTolerance := false // decide if you want to set tolerance when verifying
body, err := io.ReadAll(req.Body)
if err != nil {
log.Fatalf("Cannot read body request: %v", err)
}
bloockSignature := req.Header.Get("Bloock-Signature")
err = verifySignature(body, bloockSignature, SecretKey, enforceTolerance)
if err != nil {
log.Fatal(err)
}
log.Println("Valid Signature!")
}
http.HandleFunc("/verify", verifyHandler)
log.Println("Listing for requests at http://localhost:8000/verify")
log.Fatal(http.ListenAndServe(":8000", nil))
}
func verifySignature(payload []byte, sigHeader string, secretKey string, enforceTolerance bool) error {
header, err := parseSignatureHeader(sigHeader)
if err != nil {
return err
}
expectedSignature, err := computeSignature(header.timestamp, payload, secretKey)
if err != nil {
return err
}
expiredTimestamp := time.Since(header.timestamp) > DefaultTolerance
if enforceTolerance && expiredTimestamp {
return ErrTooOld
}
if hmac.Equal(expectedSignature, header.signature) {
return nil
}
return ErrNoValidSignature
}
Split the header, using the , character as the separator, to get a list of elements. Then split each element, using the = character as the separator, to get a prefix and value pair.
The value for the prefix t corresponds to the timestamp, and v1 corresponds to the signature.
Golang
func parseSignatureHeader(header string) (*signedHeader, error) {
sh := &signedHeader{}
if header == "" {
return sh, ErrNotSigned
}
// Signed header looks like "t=1495999758,v1=ABC"
pairs := strings.Split(header, ",")
for _, pair := range pairs {
parts := strings.Split(pair, "=")
if len(parts) != 2 {
return sh, ErrInvalidHeader
}
switch parts[0] {
case "t":
timestamp, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return sh, ErrInvalidHeader
}
sh.timestamp = time.Unix(timestamp, 0)
case signingVersion:
sig, err := hex.DecodeString(parts[1])
if err != nil {
continue
}
sh.signature = sig
default:
continue
}
}
if len(sh.signature) == 0 {
return sh, ErrNoValidSignature
}
return sh, nil
}
Compute an HMAC with the SHA256 hash function. Use the endpoint’s signing secret as the key, and use the concatenation of the timestamp with the payload.
Golang
func computeSignature(t time.Time, payload []byte, secretKey string) ([]byte, error) {
buffer := new(bytes.Buffer)
if err := json.Compact(buffer, payload); err != nil {
return nil, err
}
mac := hmac.New(sha256.New, []byte(secretKey))
mac.Write([]byte(fmt.Sprintf("%d", t.Unix())))
mac.Write([]byte("."))
mac.Write(buffer.Bytes())
return mac.Sum(nil), nil
}
Compare the signature in the header to the expected signature. For an equality match, compute the difference between the current timestamp and the received timestamp, then decide if the difference is within your tolerance.
Golang
expiredTimestamp := time.Since(header.timestamp) > DefaultTolerance
if enforceTolerance && expiredTimestamp {
return ErrTooOld
}
if hmac.Equal(expectedSignature, header.signature) {
return nil
}
All the complete code snippet.
Golang
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"strconv"
"strings"
"time"
)
const (
// SecretKey represents the client secret key associate to your webhook endpoint
SecretKey = "secret"
// DefaultTolerance indicates that signatures older than this will be rejected.
DefaultTolerance = 600 * time.Second
// signingVersion represents the version of the signature we currently use.
signingVersion = "v1"
)
var (
ErrInvalidHeader = errors.New("webhook has invalid Bloock-Signature header")
ErrNotSigned = errors.New("webhook has no Bloock-Signature header")
ErrNoValidSignature = errors.New("webhook had no valid signature")
ErrTooOld = errors.New("timestamp wasn't within tolerance")
)
type signedHeader struct {
timestamp time.Time
signature []byte
}
func main() {
verifyHandler := func(w http.ResponseWriter, req *http.Request) {
enforceTolerance := false // decide if you want to set tolerance when verifying
body, err := io.ReadAll(req.Body)
if err != nil {
log.Fatalf("Cannot read body request: %v", err)
}
bloockSignature := req.Header.Get("Bloock-Signature")
err = verifySignature(body, bloockSignature, SecretKey, enforceTolerance)
if err != nil {
log.Fatal(err)
}
log.Println("Valid Signature!")
}
http.HandleFunc("/verify", verifyHandler)
log.Println("Listing for requests at http://localhost:8000/verify")
log.Fatal(http.ListenAndServe(":8000", nil))
}
func verifySignature(payload []byte, sigHeader string, secretKey string, enforceTolerance bool) error {
header, err := parseSignatureHeader(sigHeader)
if err != nil {
return err
}
expectedSignature, err := computeSignature(header.timestamp, payload, secretKey)
if err != nil {
return err
}
expiredTimestamp := time.Since(header.timestamp) > DefaultTolerance
if enforceTolerance && expiredTimestamp {
return ErrTooOld
}
if hmac.Equal(expectedSignature, header.signature) {
return nil
}
return ErrNoValidSignature
}
func parseSignatureHeader(header string) (*signedHeader, error) {
sh := &signedHeader{}
if header == "" {
return sh, ErrNotSigned
}
// Signed header looks like "t=1495999758,v1=ABC"
pairs := strings.Split(header, ",")
for _, pair := range pairs {
parts := strings.Split(pair, "=")
if len(parts) != 2 {
return sh, ErrInvalidHeader
}
switch parts[0] {
case "t":
timestamp, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return sh, ErrInvalidHeader
}
sh.timestamp = time.Unix(timestamp, 0)
case signingVersion:
sig, err := hex.DecodeString(parts[1])
if err != nil {
continue
}
sh.signature = sig
default:
continue
}
}
if len(sh.signature) == 0 {
return sh, ErrNoValidSignature
}
return sh, nil
}
func computeSignature(t time.Time, payload []byte, secretKey string) ([]byte, error) {
buffer := new(bytes.Buffer)
if err := json.Compact(buffer, payload); err != nil {
return nil, err
}
mac := hmac.New(sha256.New, []byte(secretKey))
mac.Write([]byte(fmt.Sprintf("%d", t.Unix())))
mac.Write([]byte("."))
mac.Write(buffer.Bytes())
return mac.Sum(nil), nil
}
Last modified 1mo ago