Validate the signature of your Webhook Endpoint

By the end of this guide you can verify that every webhook event came from Fintoc by checking the Fintoc-Signature header.

Fintoc signs every event sent to your webhook endpoints with the Fintoc-Signature header. This header lets you verify that Fintoc, not a third party, sent each event.

Fintoc generates a secret when you register a webhook endpoint. This secret is necessary to validate the Fintoc signature of every event.

Verify that Fintoc sent the event

Each event sent by Fintoc includes the Fintoc-Signature header. The header contains a t timestamp and a v1 signature. For example:

t=1620870928,v1=4df951e02db34a3f333bccad26d207993e9b14d78ac77cec026091991f567f6d

Each signature is a hash-based message authentication code (HMAC). Fintoc generates it from the raw request body and the timestamp, using SHA-256 and your webhook endpoint secret.

Validate the signature with the Fintoc SDK

If you use Node or Python, verify webhook signatures with the Fintoc Node SDK or Fintoc Python SDK.

const { WebhookSignature, WebhookSignatureError } = require('fintoc');

// Use the secret returned when you created the endpoint
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

// Rest of your code here
// ...

// express.raw keeps req.body as the raw Buffer, which the signature covers.
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const payload = req.body;
  
  // Get the signature header
  const signature = req.headers['fintoc-signature'];

  try {
    // Verify the webhook signature
    WebhookSignature.verifyHeader(
      payload,
      signature,
      WEBHOOK_SECRET
    );

    // If verification passes, process the webhook
    const event = JSON.parse(payload.toString());
    
    // Rest of your code...

    // Acknowledge receipt of the event
    res.status(200).json({ received: true });
  } catch (error) {
    if (error instanceof WebhookSignatureError) {
      console.error('Webhook signature verification failed');
      res.status(400).json({
        error: 'Invalid signature',
        message: error.message
      });
    } else {
      res.status(500).json({ error: 'Webhook handler failed' });
    }
  }
});
 
import os

from fintoc.webhook import WebhookSignature
from fintoc.errors import WebhookSignatureError

WEBHOOK_SECRET = os.getenv('FINTOC_WEBHOOK_SECRET')

# Rest of your code 
# ...

@app.route('/webhook', methods=['POST'])
def handle_webhook():
  # Get the signature header
  signature = request.headers.get('Fintoc-Signature')

  # Get the raw request payload
  payload = request.get_data().decode('utf-8')

  # Verify the webhook signature
  try:
    WebhookSignature.verify_header(
      payload=payload,
      header=signature,
      secret=WEBHOOK_SECRET
    )

  except WebhookSignatureError as e:
    print('Invalid signature!')
    return str(e), 400

  # Acknowledge receipt of the event
  return '', 200

For a complete working example, check the full code here.

Validate the signature with your own code

If you want to write your own implementation or use a different programming language, follow these steps:

Extract the timestamp and the signature

Split the Fintoc-Signature header into an array by the , character. Then, split each element of the generated array by the = character to obtain a key-value pair. Finally, get the corresponding values for each key.

# using flask request
header_value = request.headers.get('Fintoc-Signature')
timestamp, event_signature = [x.split('=')[1] for x in header_value.split(',')]

Rebuild the signed message

The signed message is the timestamp, a . character, and the raw body of the request. Rebuild the message with the timestamp you extracted from the Fintoc-Signature header.

import json

# using flask request
message = f"{timestamp}.{request.get_data().decode('utf-8')}"

Use the raw, unparsed request body directly from the request. Different libraries can represent the parsed JSON differently. For example, message should look like this:

'1626102791.{"id":"evt_DyzYBwdC07ao5MqG","type":"link.credentials_changed","mode":"test","created_at":"2021-07-12T15:11:09.875Z","data":{"id":"link_00000000","mode":"test","active":true,"object":"link","status":"active","accounts":null,"username":"111111111","holder_id":"111111111","created_at":"2021-06-24T00:00:00.000Z","link_token":null,"holder_type":"individual","institution":{"id":"cl_banco_bbva","name":"Banco BBVA","country":"cl"}},"object":"event"}'

Generate the signature

Now that you have the message to sign, generate the signature using the SHA-256 hash function and the secret generated when creating the webhook endpoint.

import hmac
from hashlib import sha256

# The secret returned when you created the webhook endpoint
YOUR_WEBHOOK_SECRET = 'YOUR_WEBHOOK_SECRET'
encoded_secret = YOUR_WEBHOOK_SECRET.encode('utf-8')
encoded_message = message.encode('utf-8')
hmac_object = hmac.new(encoded_secret, msg=encoded_message, digestmod=sha256)
signature = hmac_object.hexdigest()

Compare the signatures

Compare the signature you extracted from the Fintoc-Signature header with the one you just generated. If both signatures match, the event came from Fintoc.

import hmac

valid_signature = hmac.compare_digest(signature, event_signature)

Prevent a replay attack

To prevent a replay attack, define a tolerance range for how old an event can be. When you receive an event, read the timestamp from the Fintoc-Signature header. Compare the timestamp with the current time. A tolerance of five minutes is a reasonable default. Accept events within five minutes of the current time, and discard older events.

Test the integration

Confirm that signature validation works before you rely on it in production. Trigger a test event for your webhook endpoint from the Fintoc Dashboard, then check how your handler responds:

  • With a valid signature, your handler validates the header and processes the event.
  • With an invalid signature, for example after you change one character in the v1 value or use the wrong secret, your handler rejects the event and returns a 400 status.