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
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-DigestSignature-InputSignatureContent-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:
- Read the raw request body exactly as received.
- Read the
Content-Digest,Signature-Input, andSignatureheaders. - Validate the
Content-Digestheader against the raw request body. - Validate that the
Signature-Inputmember matches the current IDaaS signing profile and extract the signature parameters. - Rebuild the RFC 9421 signature base from the actual HTTP request.
- Compute an HMAC-SHA256 over that signature base using the webhook secret token.
- Base64-encode the result and compare it to the value in the
Signatureheader using a constant-time comparison.
Important requirements
- Use the raw request body, not a JSON object that was serialized again.
- Verify
Content-Digestbefore trusting the payload. - Use the actual request URL received by your webhook endpoint when rebuilding
@target-uri. - Validate that
Signature-Inputis exactlysig=("@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-sha256for this webhook profile.
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
- Java
- CSharp
- Python
- NodeJS
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());
}
}
using System;
using System.Security.Cryptography;
using System.Text;
namespace com.entrust.idaas.webhooks
{
public class WebhookSignatureVerifier
{
private const string SignatureLabel = "sig";
private const string ExpectedSignatureInput = "sig=(\"@method\" \"@target-uri\" \"content-digest\");alg=\"hmac-sha256\"";
public void VerifyPayload(
string rawEventBody,
string contentDigestHeader,
string signatureInputHeader,
string signatureHeader,
string targetUri,
string webhookToken)
{
var signatureParameters = ExtractAndValidateSignatureInputValue(signatureInputHeader);
var signatureValue = ExtractSignatureValue(signatureHeader, SignatureLabel);
ValidateContentDigest(rawEventBody, contentDigestHeader);
var signatureBase = string.Join("\n", new[]
{
"\"@method\": POST",
$"\"@target-uri\": {targetUri}",
$"\"content-digest\": {contentDigestHeader}",
$"\"@signature-params\": {signatureParameters}"
});
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(webhookToken));
var expectedSignature = Convert.ToBase64String(
hmac.ComputeHash(Encoding.UTF8.GetBytes(signatureBase))
);
if (!CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expectedSignature),
Encoding.UTF8.GetBytes(signatureValue)))
{
throw new ArgumentException("Invalid webhook signature");
}
}
private void ValidateContentDigest(string rawEventBody, string contentDigestHeader)
{
var bodyHash = SHA256.HashData(Encoding.UTF8.GetBytes(rawEventBody));
var expectedDigest = $"sha-256=:{Convert.ToBase64String(bodyHash)}:";
if (!CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expectedDigest),
Encoding.UTF8.GetBytes(contentDigestHeader)))
{
throw new ArgumentException("Invalid content digest");
}
}
private string ExtractAndValidateSignatureInputValue(string header)
{
if (!string.Equals(header, ExpectedSignatureInput, StringComparison.Ordinal))
{
throw new ArgumentException("Unsupported Signature-Input profile");
}
return header.Substring((SignatureLabel + "=").Length);
}
private string ExtractSignatureValue(string header, string label)
{
var prefix = label + "=:";
if (!header.StartsWith(prefix, StringComparison.Ordinal) || !header.EndsWith(":", StringComparison.Ordinal))
{
throw new ArgumentException("Malformed Signature header");
}
return header.Substring(prefix.Length, header.Length - prefix.Length - 1);
}
}
}
import base64
import hashlib
import hmac
class WebhookSignatureVerifier:
SIGNATURE_LABEL = "sig"
EXPECTED_SIGNATURE_INPUT = 'sig=("@method" "@target-uri" "content-digest");alg="hmac-sha256"'
@staticmethod
def verify_payload(raw_event: str, content_digest: str, signature_input: str, signature: str, target_uri: str, webhook_token: str):
signature_parameters = WebhookSignatureVerifier.extract_and_validate_signature_input_value(signature_input)
signature_value = WebhookSignatureVerifier.extract_signature_value(signature, WebhookSignatureVerifier.SIGNATURE_LABEL)
WebhookSignatureVerifier.validate_content_digest(raw_event, content_digest)
signature_base = "\n".join([
'"@method": POST',
f'"@target-uri": {target_uri}',
f'"content-digest": {content_digest}',
f'"@signature-params": {signature_parameters}',
])
expected_signature = base64.b64encode(
hmac.new(
key=webhook_token.encode("utf-8"),
msg=signature_base.encode("utf-8"),
digestmod=hashlib.sha256,
).digest()
).decode("ascii")
if not hmac.compare_digest(expected_signature, signature_value):
raise ValueError("Invalid webhook signature")
@staticmethod
def validate_content_digest(raw_event: str, content_digest: str):
expected_digest = "sha-256=:" + base64.b64encode(
hashlib.sha256(raw_event.encode("utf-8")).digest()
).decode("ascii") + ":"
if not hmac.compare_digest(expected_digest, content_digest):
raise ValueError("Invalid content digest")
@staticmethod
def extract_and_validate_signature_input_value(header: str) -> str:
if header != WebhookSignatureVerifier.EXPECTED_SIGNATURE_INPUT:
raise ValueError("Unsupported Signature-Input profile")
prefix = f"{WebhookSignatureVerifier.SIGNATURE_LABEL}="
return header[len(prefix):]
@staticmethod
def extract_signature_value(header: str, label: str) -> str:
prefix = f"{label}=:"
if not header.startswith(prefix) or not header.endswith(":"):
raise ValueError("Malformed Signature header")
return header[len(prefix):-1]
import crypto from "node:crypto";
const SIGNATURE_LABEL = "sig";
const EXPECTED_SIGNATURE_INPUT =
'sig=("@method" "@target-uri" "content-digest");alg="hmac-sha256"';
const validateContentDigest = (
rawEventBody: string,
contentDigestHeader: string,
) => {
const expectedDigest = `sha-256=:${crypto
.createHash("sha256")
.update(rawEventBody)
.digest("base64")}:`;
if (expectedDigest.length !== contentDigestHeader.length) {
throw new Error("Invalid content digest");
}
if (
!crypto.timingSafeEqual(
Buffer.from(expectedDigest, "utf8"),
Buffer.from(contentDigestHeader, "utf8"),
)
) {
throw new Error("Invalid content digest");
}
};
const extractAndValidateSignatureInputValue = (header: string) => {
if (header !== EXPECTED_SIGNATURE_INPUT) {
throw new Error("Unsupported Signature-Input profile");
}
const prefix = `${SIGNATURE_LABEL}=`;
return header.slice(prefix.length);
};
const extractSignatureValue = (header: string, label: string) => {
const prefix = `${label}=:`;
if (!header.startsWith(prefix) || !header.endsWith(":")) {
throw new Error("Malformed Signature header");
}
return header.slice(prefix.length, -1);
};
const verifyPayload = (
rawEventBody: string,
contentDigestHeader: string,
signatureInputHeader: string,
signatureHeader: string,
targetUri: string,
webhookToken: string,
) => {
const signatureParameters =
extractAndValidateSignatureInputValue(signatureInputHeader);
const signatureValue = extractSignatureValue(
signatureHeader,
SIGNATURE_LABEL,
);
validateContentDigest(rawEventBody, contentDigestHeader);
const signatureBase = [
'"@method": POST',
`"@target-uri": ${targetUri}`,
`"content-digest": ${contentDigestHeader}`,
`"@signature-params": ${signatureParameters}`,
].join("\n");
const expectedSignature = crypto
.createHmac("sha256", webhookToken)
.update(signatureBase)
.digest("base64");
if (expectedSignature.length !== signatureValue.length) {
return false;
}
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, "utf8"),
Buffer.from(signatureValue, "utf8"),
);
};
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.
- Java
- CSharp
- Python
- NodeJS
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.
}
}
}
using System.Text.Json;
namespace com.entrust.idaas.webhooks
{
public class WebhookEventProcessor
{
public void ProcessWebhookEvent(
string rawEventBody,
string contentDigestHeader,
string signatureInputHeader,
string signatureHeader,
string targetUri,
string webhookToken)
{
new WebhookSignatureVerifier().VerifyPayload(
rawEventBody,
contentDigestHeader,
signatureInputHeader,
signatureHeader,
targetUri,
webhookToken);
using JsonDocument payload = JsonDocument.Parse(rawEventBody);
JsonElement root = payload.RootElement;
string eventType = root.GetProperty("type").GetString();
bool hasData = root.TryGetProperty("data", out JsonElement data);
if (eventType == "authentication.succeeded")
{
string userId = hasData && data.TryGetProperty("subject", out JsonElement subject)
? subject.GetString()
: null;
string username = hasData && data.TryGetProperty("subjectName", out JsonElement subjectName)
? subjectName.GetString()
: null;
string authenticationMethod = hasData && data.TryGetProperty("token", out JsonElement token)
? token.GetString()
: "unknown";
// Process the verified IDaaS webhook event.
}
}
}
}
import json
class WebhookEventProcessor:
@staticmethod
def process_webhook_event(raw_event: str, content_digest: str, signature_input: str, signature: str, target_uri: str, webhook_token: str):
WebhookSignatureVerifier.verify_payload(
raw_event,
content_digest,
signature_input,
signature,
target_uri,
webhook_token,
)
payload = json.loads(raw_event)
event_type = payload.get("type")
data = payload.get("data", {})
if event_type == "authentication.succeeded":
user_id = data.get("subject")
username = data.get("subjectName")
authentication_method = data.get("token", "unknown")
# Process the verified IDaaS webhook event.
const processWebhookEvent = async (
rawEventBody: string,
contentDigestHeader: string,
signatureInputHeader: string,
signatureHeader: string,
targetUri: string,
webhookToken: string,
) => {
const isValid = verifyPayload(
rawEventBody,
contentDigestHeader,
signatureInputHeader,
signatureHeader,
targetUri,
webhookToken,
);
if (!isValid) {
throw new Error("Invalid webhook signature");
}
const payload = JSON.parse(rawEventBody);
const eventType = payload.type;
const data = payload.data ?? {};
if (eventType === "authentication.succeeded") {
const userId = data.subject;
const username = data.subjectName;
const authenticationMethod = data.token ?? "unknown";
// Process the verified IDaaS webhook event.
console.log({ userId, username, authenticationMethod });
}
};