Skip to main content

Webhook Signature Verification

IDaaS webhook requests use RFC 9421 HTTP Message Signatures.

This means each webhook request includes standard HTTP signature metadata that lets you verify both:

  • The request body was not changed in transit
  • The signed HTTP request metadata matches what IDaaS sent
NOTE

Since outbound webhook connections are not necessarily sent from a dedicated IP address, signature verification is the primary way to prove that the webhook request really came from IDaaS and was not spoofed or modified by an attacker.

All customers using webhooks should verify signatures before processing the request.

Headers included in each webhook request

Each webhook request includes these headers:

  • Content-Digest
  • Signature-Input
  • Signature
  • Content-Type: application/json

Content-Type is included on the request but is not part of the current signed component set.

The current IDaaS webhook signing profile is:

  • Signature algorithm: hmac-sha256
  • Covered components: @method, @target-uri, content-digest
  • Signature label: sig
  • Request method: POST
  • Content type: application/json

The algorithm is fixed by the webhook profile and is sent in the Signature-Input header as alg="hmac-sha256".

Verification overview

To verify a webhook request:

  1. Read the raw request body exactly as received.
  2. Read the Content-Digest, Signature-Input, and Signature headers.
  3. Validate the Content-Digest header against the raw request body.
  4. Validate that the Signature-Input member matches the current IDaaS signing profile and extract the signature parameters.
  5. Rebuild the RFC 9421 signature base from the actual HTTP request.
  6. Compute an HMAC-SHA256 over that signature base using the webhook secret token.
  7. Base64-encode the result and compare it to the value in the Signature header using a constant-time comparison.

Important requirements

  • Use the raw request body, not a JSON object that was serialized again.
  • Verify Content-Digest before trusting the payload.
  • Use the actual request URL received by your webhook endpoint when rebuilding @target-uri.
  • Validate that Signature-Input is exactly sig=("@method" "@target-uri" "content-digest");alg="hmac-sha256" for the current webhook profile.
  • Compare signatures using a constant-time comparison function.
  • Configure your verifier to use hmac-sha256 for this webhook profile.
warning

You should use a constant-time equality function from a cryptographic library to prevent timing attacks.

Also note that verifying the Signature header alone is not enough. You must validate the Content-Digest header against the raw request body as well.

Example request headers

An IDaaS webhook request will look conceptually like this:

POST /webhooks/events HTTP/1.1
Content-Type: application/json
Content-Digest: sha-256=:Base64DigestHere=:
Signature-Input: sig=("@method" "@target-uri" "content-digest");alg="hmac-sha256"
Signature: sig=:Base64SignatureHere=:

Rebuild the signature base

For the current IDaaS webhook profile, the verifier rebuilds the signature base in this exact form:

"@method": POST
"@target-uri": https://example.com/webhooks/events
"content-digest": sha-256=:Base64DigestHere=:
"@signature-params": ("@method" "@target-uri" "content-digest");alg="hmac-sha256"

The exact value of @target-uri must match the URL that received the webhook.

Signature verification reference implementation examples

package com.entrust.idaas.webhooks;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;

public class WebhookSignatureVerifier {
private static final String SIGNATURE_LABEL = "sig";
private static final String EXPECTED_SIGNATURE_INPUT =
"sig=(\"@method\" \"@target-uri\" \"content-digest\");alg=\"hmac-sha256\"";

public void verifyPayload(
String rawEventBody,
String contentDigestHeader,
String signatureInputHeader,
String signatureHeader,
String targetUri,
String webhookToken
) throws Exception {
String signatureParameters = extractAndValidateSignatureInputValue(signatureInputHeader);
String signatureValue = extractSignatureValue(signatureHeader, SIGNATURE_LABEL);

validateContentDigest(rawEventBody, contentDigestHeader);

String signatureBase = String.join("\n",
"\"@method\": POST",
"\"@target-uri\": " + targetUri,
"\"content-digest\": " + contentDigestHeader,
"\"@signature-params\": " + signatureParameters
);

Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(webhookToken.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String expectedSignature = Base64.getEncoder().encodeToString(
mac.doFinal(signatureBase.getBytes(StandardCharsets.UTF_8))
);

if (!MessageDigest.isEqual(
expectedSignature.getBytes(StandardCharsets.UTF_8),
signatureValue.getBytes(StandardCharsets.UTF_8)
)) {
throw new IllegalArgumentException("Invalid webhook signature");
}
}

private void validateContentDigest(String rawEventBody, String contentDigestHeader) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
String expectedDigest = "sha-256=:" + Base64.getEncoder().encodeToString(
digest.digest(rawEventBody.getBytes(StandardCharsets.UTF_8))
) + ":";

if (!MessageDigest.isEqual(
expectedDigest.getBytes(StandardCharsets.UTF_8),
contentDigestHeader.getBytes(StandardCharsets.UTF_8)
)) {
throw new IllegalArgumentException("Invalid content digest");
}
}

private String extractAndValidateSignatureInputValue(String header) {
if (!EXPECTED_SIGNATURE_INPUT.equals(header)) {
throw new IllegalArgumentException("Unsupported Signature-Input profile");
}

return header.substring((SIGNATURE_LABEL + "=").length());
}

private String extractSignatureValue(String header, String label) {
String prefix = label + "=:";
String suffix = ":";
if (!header.startsWith(prefix) || !header.endsWith(suffix)) {
throw new IllegalArgumentException("Malformed Signature header");
}
return header.substring(prefix.length(), header.length() - suffix.length());
}
}

IDaaS event payload example

{
"id": "019adb89-60dd-750e-90a3-e860c924aa29",
"type": "authentication.succeeded",
"accountId": "c8485a88-4fd0-4248-8dcd-fb4ac0749fb7",
"eventTime": "2025-12-01T20:10:04Z",
"data": {
"subject": "f7475916-56ab-44a1-ab8a-3d4407baa102",
"subjectName": "john.smith",
"subjectType": "USER",
"resourceName": "Administration Portal",
"sourceIp": "127.0.0.1",
"token": "OTP"
}
}

IDaaS webhook payloads include top-level event metadata and a data object that contains event-specific fields. The exact contents of data vary by event type. For event-specific payload details, see the pages in Events.

Process a verified event payload

After you verify both Content-Digest and Signature, parse the raw event body and process the event based on the type field and the values inside data.

package com.entrust.idaas.webhooks;

import org.json.JSONObject;

public class WebhookEventProcessor {

public void processWebhookEvent(
String rawEventBody,
String contentDigestHeader,
String signatureInputHeader,
String signatureHeader,
String targetUri,
String webhookToken
) throws Exception {
new WebhookSignatureVerifier().verifyPayload(
rawEventBody,
contentDigestHeader,
signatureInputHeader,
signatureHeader,
targetUri,
webhookToken
);

JSONObject payload = new JSONObject(rawEventBody);
String eventType = payload.getString("type");
JSONObject data = payload.getJSONObject("data");

if ("authentication.succeeded".equals(eventType)) {
String userId = data.getString("subject");
String username = data.getString("subjectName");
String authenticationMethod = data.optString("token", "unknown");

// Process the verified IDaaS webhook event.
}
}
}