import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
public class WebhookTest {
/**
* Verify the signature of a webhook payload.
*
* @param payload The raw request body as a string
* @param signatureHeader The Exa-Signature header value
* @param webhookSecret Your webhook secret
* @return true if signature is valid, false otherwise
*/
public static boolean verifyWebhookSignature(String payload, String signatureHeader, String webhookSecret) {
try {
// Parse the signature header
String[] pairs = signatureHeader.split(",");
String timestamp = null;
List<String> signatures = new ArrayList<>();
for (String pair : pairs) {
String[] keyValue = pair.split("=", 2);
if (keyValue.length == 2) {
String key = keyValue[0];
String value = keyValue[1];
if ("t".equals(key)) {
timestamp = value;
} else if ("v1".equals(key)) {
signatures.add(value);
}
}
}
if (timestamp == null || signatures.isEmpty()) {
return false;
}
// Optional: Check if timestamp is recent (within 5 minutes)
long currentTime = Instant.now().getEpochSecond();
long webhookTime = Long.parseLong(timestamp);
if (Math.abs(currentTime - webhookTime) > 300) {
System.out.println("Warning: Webhook timestamp is more than 5 minutes old");
}
// Create the signed payload
String signedPayload = timestamp + "." + payload;
// Compute the expected signature
String expectedSignature = computeHmacSha256(signedPayload, webhookSecret);
// Compare with provided signatures using timing-safe comparison
return signatures.stream().anyMatch(sig -> timingSafeEquals(expectedSignature, sig));
} catch (Exception e) {
System.err.println("Error verifying signature: " + e.getMessage());
return false;
}
}
/**
* Compute HMAC SHA256 signature.
*/
private static String computeHmacSha256(String data, String key)
throws NoSuchAlgorithmException, InvalidKeyException {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(secretKeySpec);
byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
return bytesToHex(hash);
}
/**
* Convert byte array to hexadecimal string.
*/
private static String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
/**
* Timing-safe string comparison to prevent timing attacks.
*/
private static boolean timingSafeEquals(String a, String b) {
if (a.length() != b.length()) {
return false;
}
int result = 0;
for (int i = 0; i < a.length(); i++) {
result |= a.charAt(i) ^ b.charAt(i);
}
return result == 0;
}
// Example usage and test
public static void main(String[] args) {
System.out.println("🚀 === Exa Webhook Signature Verification Test ===\n");
// Test with a known payload and signature
String testPayload = "{\"type\":\"webset.created\",\"data\":{\"id\":\"ws_test\"}}";
String testSecret = "test_webhook_secret";
String testTimestamp = String.valueOf(Instant.now().getEpochSecond());
try {
// Create test signature
String signedPayload = testTimestamp + "." + testPayload;
String testSignature = computeHmacSha256(signedPayload, testSecret);
String testHeader = "t=" + testTimestamp + ",v1=" + testSignature;
System.out.println("📋 Test Data:");
System.out.println(" • Payload: " + testPayload);
System.out.println(" • Secret: " + testSecret);
System.out.println(" • Timestamp: " + testTimestamp);
System.out.println(" • Generated Signature: " + testSignature);
System.out.println(" • Header: " + testHeader);
System.out.println();
System.out.println("🧪 Running Tests...");
// Test verification
boolean isValid = verifyWebhookSignature(testPayload, testHeader, testSecret);
System.out.println(" ✓ Valid signature verification: " + (isValid ? "✅ PASSED" : "❌ FAILED"));
// Test with invalid signature
String invalidHeader = "t=" + testTimestamp + ",v1=invalid_signature";
boolean isInvalid = verifyWebhookSignature(testPayload, invalidHeader, testSecret);
System.out.println(" ✓ Invalid signature rejection: " + (!isInvalid ? "✅ PASSED" : "❌ FAILED"));
// Test with missing timestamp
String noTimestampHeader = "v1=" + testSignature;
boolean noTimestamp = verifyWebhookSignature(testPayload, noTimestampHeader, testSecret);
System.out.println(" ✓ Missing timestamp rejection: " + (!noTimestamp ? "✅ PASSED" : "❌ FAILED"));
// Test with empty header
boolean emptyHeader = verifyWebhookSignature(testPayload, "", testSecret);
System.out.println(" ✓ Empty header rejection: " + (!emptyHeader ? "✅ PASSED" : "❌ FAILED"));
// Test with malformed header
boolean malformedHeader = verifyWebhookSignature(testPayload, "invalid-header-format", testSecret);
System.out.println(" ✓ Malformed header rejection: " + (!malformedHeader ? "✅ PASSED" : "❌ FAILED"));
System.out.println();
// Example webhook processing
if (isValid) {
System.out.println("🎉 === Processing Valid Webhook ===");
System.out.println(" Processing webhook payload: " + testPayload);
// Here you would parse the JSON and handle the webhook event
System.out.println(" Webhook processed successfully!");
System.out.println();
System.out.println("🔒 Security verification complete! Your webhook signature verification is working correctly.");
}
} catch (Exception e) {
System.err.println("❌ Test failed with error: " + e.getMessage());
e.printStackTrace();
}
}
}