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=4df951e02db34a3f333bccad26d207993e9b14d78ac77cec026091991f567f6dEach 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 '', 200For 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
v1value or use the wrong secret, your handler rejects the event and returns a400status.