> For AI agents: [llms.txt](https://docs.trustedauth.com/llms.txt) provides a Markdown index of this documentation.

# Webhook Signature Verification

IDaaS webhook requests use [RFC 9421 HTTP Message Signatures](https://datatracker.ietf.org/doc/html/rfc9421).

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:

```http
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:

```text
"@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**

```java
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());
    }
}
```

**CSharp**

```csharp
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);
        }
    }
}
```

**Python**

```python
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]
```

**NodeJS**

```typescript
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

```json
{
  "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](/developer/webhook/events.md).

## 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**

```java
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.
        }
    }
}
```

**CSharp**

```csharp
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.
            }
        }
    }
}
```

**Python**

```python
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.
```

**NodeJS**

```typescript
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 });
  }
};
```
