Skip to main content

How to secure a webhook

Check the events that Cryptr sends to your webhook endpoints for more security and make sure that these events have not been sent by a third party.

Each event sent to your

The fallback content to display on prerendering
by Cryptr contains a signature. Security is an important concept, which is why this signature has been added. You can ignore it if you wish, but it is not recommended to do so. Indeed, this key will allow you to verify that the events received have indeed been sent by Cryptr and not by an outside person.

1. Get the necessary data

Get the Cryptr Signature Key

The Cryptr Signature is located in the header of the HTTP request under the cryptr-signature key. Indeed, when you receive an HTTP request or response, the payload is made up of HTTP protocol information, such as headers, a URL, body content, version and status information.

This is what an HTTP header looks like:

Header
Content-Type: application/json; charset=UTF-8
User-Agent: Mozilla/5.0 (X11; Linux i686; rv:10.0) Gecko/20100101 Firefox/10.0
cryptr-signature: t=1676905124,v1=sha256.640e3f227fed2123e84101cc69f41fba3435ec3b8f28d4ea249231f928efa990,v0=sha256.3d5990d850a7bbe430bf6ed93329f5b61b3fb72871afed9a071f3b95317d73ab

For example, you can retrieve the cryptr-signature by performing the following function:

ruby
cryptrSignature = request.env["HTTP_CRYPTR_SIGNATURE"]

Here is an example of a Cryptr signature:

cryptr-signature: t=1676905124,v1=640e3f227fed2123e84101cc69f41fba3435ec3b8f28d4ea249231f928efa990,v0=3d5990d850a7bbe430bf6ed93329f5b61b3fb72871afed9a071f3b95317d73ab

Extract the timestamp

Each cryptr-signature contains a timestamp. This timestamp, which you'll need to retrieve & use, prevents replay attacks.

You can proceed as follows to retrieve the timestamp:

ruby
# Split the cryptr-signature
array = cryptrSignature.split

# Get the timestamp from the splitted signature
timestamp = array.first

Extract the signature

Each cryptr-signature also contains a v1 Signature. But it can also contains a v0 Signature if your secret has been updated.

Signature v1 is the most recent signature, signature v0 is the signature made with the previous signature_key. This was thought so that you have time to make the necessary changes without being blocked if the signature_key changes. Although both versions are usable, it is strongly recommended to use only v1 whenever possible.

To retrieve your signatures, proceed as follows:

ruby
# Split the cryptr-signature
array = cryptrSignature.split

# Get the Signature V1 from the splitted signature
signatureV1 = array[1]
# Get only the necessary data
signatureV1.slice! "sha256."

# OPTIONAL: Get the Signature V0 from splitted signature
# signatureV0 = array[2]
# Get only the necessary data
# signatureV0.slice! "sha256."
info

Note that we don't take the raw value of the signature, but only what comes after the sha256. .

⚠️ Don't forget the . .

Once that's done, we can move on to the next step.

2. Retrieve your Secret Key

To verify your signatures, you will need to retrieve the signature_key of the webhooks for which you want to ensure the authenticity. If you don't remember it from when you created your webhooks, you can always retrieve it via the following query:

cURL
curl 'https://${cryptr_service_url}/api/v2/webhooks/${webhook_id}'

This will give you the following result:

Webhook Payload
{
"__environment__": "production",
"__type__": "Webhook",
"active": true,
"event_codes": ["dir_sync.user.provision.success"],
"id": "41f555de-6dac-4f24-8d0a-dc3499497ef0",
"inserted_at": "2023-11-07T15:31:30",
"name": "SCIM Webhook",
"signature_key": "0Zrk1pQnc10hh5ZDecqQfMDKy0S2FfdWU7ZJQ40Mh2TgweRcXM5Um3b6P0aUkFqf",
"target_url": "https://cryptr.site/webhook",
"updated_at": "2023-11-07T15:31:30"
}

Each webhook you create will have a different signature_key. Even if you create webhooks with almost identical parameters or you have two identical webhooks in production and in sandbox environment, these will have different keys so be sure to check that the key you are going to use is the corresponding key.

Once you have the timestamp, the v1 (or v0) Cryptr Signature and the signature_key, we can move on to the next step. Signing the event you've received.

3. Sign your data

Get the Event to Sign

Now, each time you receive an Event, you'll have all the information you need. To start with, here's what an event payload looks like:

Example of an event received
{
"__domain__": "shark-academy",
"__environment__": "sandbox",
"__type__": "Event",
"code": "dir_sync.user.update.success",
"data": {
"__domain__": "synchronizable",
"__type__": "DirectorySyncEvent",
"change": "update",
"directory_sync_id": "6aefc945-0815-4631-8ded-edc7bb1944ae",
"provider": "okta",
"resource": {
"changes": {
"new_values": {
"active": true,
"updated_at": "2023-10-17T08:12:37"
},
"previous_values": {
"active": false,
"updated_at": "2023-10-17T08:11:38"
}
},
"id": "7e2f977c-a8f0-4451-8967-fe63b243e7a1",
"type": "User"
}
},
"errors": null,
"issued_at": "2023-10-17T08:12:38.113207Z",
"params": {"active": true, "id": "7e2f977c-a8f0-4451-8967-fe63b243e7a1"},
"webhook_id": "webhook_2Wsnp8azBTeK2r29TExX9vBvnCg"
}
note

You can use the webhook_id contained in this payload to help you retrieve the correct information in step B.

Prepare the string to sign

To create the string to sign, you must then concatenate several elements, the elements to concatenate are as follows:

  • The timestamp (in string format)
  • The character .
  • The Stringified JSON payload (the content of the request you received)

Which would give something like this:

Ruby
require 'json'

stringifiedPayload = JSON.generate(payload)
stringToSign = timestamp + "." + stringifiedPayload
# => "1676905124.{\"__domain__\":\"shark-academy\",\"__environment__\":\"sandbox\",\"__type__\":\"Event\",...}"

We now need to sign this string.

Sign the resulting string.

To sign the previously obtained string, use the SHA256 hash function with the signature_key associated with your webhook as the secret. This will give you an HMAC which you can then compare with the values of signatureV1 or signatureV0 obtained in step A.3.

Ruby
# Create a digest for the SHA256 Algorithm
digest = OpenSSL::Digest.new('sha256')

# Generate the HMAC of your previously constructed string using your Webhook signature_key
hmac = OpenSSL::HMAC.digest(digest, signature_key, stringToSign)

# Encode the Result in Base64 with no padding /!\
yourSignature = Base64.urlsafe_encode64(hmac, false)
danger

Make sure that:

  • you are using the SHA 256 algorithm
  • Your payload has not been rewritten (i.e. that the key order is the same as when you received it)
  • You have disabled padding
  • You have stringified your payload

4. Compare the signature obtained with the signature provided

Now that you have established the signature of the received payload you can compare it with the one sent by Cryptr.

Ruby
# Compare the Signature sent by Cryptr with yours
if yourSignature == signatureV1
# You can proceed
else
# Signatures are not equal
end

If the two signatures are similar, you can assume that the payload you received was sent by Cryptr.

If you receive a correct signature, check the date and time it was issued. If the date is too far away, you can choose to refuse it.

Full example

Here is a full example:

Ruby
require 'json'
## STEP 1.Get the necessary data ##

# Get the Cryptr Signature Key
cryptrSignature = request.env["HTTP_CRYPTR_SIGNATURE"]

# Extract the timestamp
array = cryptrSignature.split
timestamp = array.first

# Extract the signature
signatureV1 = array[1]
signatureV1.slice! "sha256."

# OPTIONAL: Get the Signature V0 from splitted signature
# signatureV0 = array[2]
# signatureV0.slice! "sha256."

## STEP 2. Use your key

# See "Retrieve your Secret Key before" part

## STEP 3. Sign your data

payload = request.body.read

# Prepare the string to sign
stringifiedPayload = JSON.generate(payload)
stringToSign = timestamp + "." + stringifiedPayload

# Sign the result
digest = OpenSSL::Digest.new('sha256')
hmac = OpenSSL::HMAC.digest(digest, signature_key, stringToSign)
yourSignature = Base64.urlsafe_encode64(hmac, false)

## STEP 4. Compare the signature ##
if yourSignature == signatureV1
# You can proceed
else
# Signatures are not equal
end