Verifying Webhook Signatures
Always verify the webhook signature to ensure the payload’s integrity and authenticity.
Fenan Pay signs the webhook payload using a private key. To ensure that the webhook you receive is from Fenan Pay and hasn’t been tampered with, verify the signature using the corresponding public key:
- Test environment:
on dashboard: settings > webhook settings > webhook_pubk_test
- Production environment:
on dashboard: settings > webhook settings > webhook_pubk_prod
How Webhook Signatures Work
Fenan Pay signs the body using private key speified in setting. The signature is sent in the webhook’s signature field. You should verify the integrity of the payload by comparing the provided signature with the one generated using your webhook’s public key.
Make sure to verify the body as a string (unmodified) because any changes could invalidate the signature.
The cryptographic algorithm and signing mechanism used is SHA256withRSA with a key length of 2048 bits.
Webhook Payload Structure
| Field | Type | Description |
|---|
event | string | Specifies the type of webhook notification. |
body | string | Contains either a PaymentIntent or WithdrawalIntent as a JSON string, depending on the event type. |
event | string | Specifies the type of webhook notification. which will be found inside the body object. |
Key Points to Remember
- Signature Field: The
signature field contains the cryptographic signature to verify the authenticity of the webhook body.
- body: Ensure you verify the body as a string, as any modifications can lead to a failed verification.
Example Implementations for Verifying Webhook Signatures
JavaScript (Node.js)
Python
Java
Go
PHP
const crypto = require('crypto');
function verifySignature(payload, signature, publicKey) {
const verify = crypto.createVerify('SHA256');
// Convert the payload body into a string and update the verify object
verify.update(payload.body);
verify.end();
// Verify the signature using the provided public key
return verify.verify(publicKey, signature, 'base64');
}
// Example usage
const payload = { signature: 'example-signature', body: { event: 'payment_intent.succeeded', ...paymentIntentObject } };
const publicKey = '-----BEGIN PUBLIC KEY-----\nyour-public-key-here\n-----END PUBLIC KEY-----'; // should be the public key from the webhook settings including the Header and Footer
const isVerified = verifySignature(payload, signature, publicKey);
console.log('Signature verified:', isVerified);
import base64
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import load_pem_public_key
def verify_signature(payload, signature, public_key):
try:
# Load the public key
key = load_pem_public_key(public_key.encode())
# Verify the signature
key.verify(
base64.b64decode(signature),
payload['body'].encode(),
padding.PKCS1v15(),
hashes.SHA256()
)
return True
except:
return False
# Example usage
payload = {'signature': 'example-signature', 'body': '{"event": "payment_intent.succeeded", ...}'}
public_key = '-----BEGIN PUBLIC KEY-----\nyour-public-key-here\n-----END PUBLIC KEY-----'
is_verified = verify_signature(payload, payload['signature'], public_key)
print('Signature verified:', is_verified)
import java.security.*;
import java.util.Base64;
public class WebhookVerifier {
public static boolean verifySignature(String payload, String signature, String publicKey) {
try {
Signature verifier = Signature.getInstance("SHA256withRSA");
// Load the public key
byte[] publicKeyBytes = publicKey.getBytes();
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyBytes));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey pubKey = keyFactory.generatePublic(keySpec);
verifier.initVerify(pubKey);
verifier.update(payload.getBytes());
return verifier.verify(Base64.getDecoder().decode(signature));
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public static void main(String[] args) {
String payload = "{\"event\": \"payment_intent.succeeded\", ...}";
String signature = "example-signature";
String publicKey = "-----BEGIN PUBLIC KEY-----\nyour-public-key-here\n-----END PUBLIC KEY-----";
boolean isVerified = verifySignature(payload, signature, publicKey);
System.out.println("Signature verified: " + isVerified);
}
}
package main
import (
"crypto"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
)
func verifySignature(payload, signature, publicKey string) bool {
// Decode the base64 signature
sig, _ := base64.StdEncoding.DecodeString(signature)
// Parse the public key
block, _ := pem.Decode([]byte(publicKey))
if block == nil {
return false
}
pubKey, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return false
}
rsaPublicKey, ok := pubKey.(*rsa.PublicKey)
if !ok {
return false
}
// Create a SHA256 hash of the payload
hashed := sha256.Sum256([]byte(payload))
// Verify the signature
err = rsa.VerifyPKCS1v15(rsaPublicKey, crypto.SHA256, hashed[:], sig)
return err == nil
}
func main() {
payload := `{"event": "payment_intent.succeeded", ...}`
signature := "example-signature"
publicKey := `-----BEGIN PUBLIC KEY-----
your-public-key-here
-----END PUBLIC KEY-----`
isVerified := verifySignature(payload, signature, publicKey)
fmt.Printf("Signature verified: %v\n", isVerified)
}
<?php
function verifySignature($payload, $signature, $publicKey) {
$publicKeyResource = openssl_pkey_get_public($publicKey);
if (!$publicKeyResource) {
return false;
}
$verify = openssl_verify($payload, base64_decode($signature), $publicKeyResource, OPENSSL_ALGO_SHA256);
openssl_free_key($publicKeyResource);
return $verify === 1;
}
// Example usage
$payload = json_encode(['event' => 'payment_intent.succeeded', /* ... */]);
$signature = 'example-signature';
$publicKey = "-----BEGIN PUBLIC KEY-----\nyour-public-key-here\n-----END PUBLIC KEY-----";
$isVerified = verifySignature($payload, $signature, $publicKey);
echo "Signature verified: " . ($isVerified ? 'true' : 'false');
?>