How to secure a webhook
Each event sent to your
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:
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:
cryptrSignature = request.env["HTTP_CRYPTR_SIGNATURE"]
Here is an example of a Cryptr signature:
cryptr-signature: 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:
# 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:
# 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."
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 'https://${cryptr_service_url}/api/v2/webhooks/${webhook_id}'
This will give you the following result:
{
"__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:
{
"__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"
}
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:
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.
# 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)
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.
# 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:
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