Validating Received Events
In order to be sure that Toggl is the one sending events to your URL endpoint we include a special HTTP header X-Webhook-Signature-256
that you can use to validate that no one else is sending those requests.
In order to perform this validation you should:
- Set the field
secret
while creating a new subscription. If omitted, the system will assign one automatically. - When delivering events to your subscription's URL endpoint the system will add the
X-Webhook-Signature-256
header. - Signature has the form of
sha256={value}
where value is a HMAC hash based on SHA256 algorithm + secret + full body. - Make sure you are calculating the hash on the full unparsed body from the incoming request.
- Example:
sha256=6d011bcd0b5bfb7e45372af01bc18f30cc04599df72eca189cdac1094008b095
.
Code Examples
In the following examples we will assume that:
- We have a subscription where its
secret
field has the valuePGuRrhCFajIyEvFlreKL
. - That we received this PING event:
{
"event_id": 0,
"created_at": "2022-06-25T03:58:10.207820267Z",
"creator_id": 6,
"metadata": {
"request_type": "POST",
"event_user_id": 6
},
"payload": "ping",
"subscription_id": 6,
"url_callback": "https://callback-url.com",
"timestamp": "2022-06-25T03:58:10.207820267Z"
}
Which in its raw form is really sent unformatted like this:
{"event_id":0,"created_at":"2022-06-25T03:58:10.207820267Z","creator_id":6,"metadata":{"request_type":"POST","event_user_id":6},"payload":"ping","subscription_id":6,"timestamp":"2022-06-25T03:58:10.207820267Z","url_callback":"https://callback-url.com"}
- That we also received the HTTP header
x-webhook-signature-256
with the following signature value:sha256=bf829606cda0ca6923defb5ca70a43135adc7e8887486a201a19cb50ca6006b1
.
We will then show in different languages how an end user can compute on their side the signature value using the received raw JSON value and the subscription's secret.
- cURL
- Go
- Ruby
- Node
- Python
- Rust
message='{"event_id":0,"created_at":"2022-06-25T03:58:10.207820267Z","creator_id":6,"metadata":{"request_type":"POST","event_user_id":6},"payload":"ping","subscription_id":6,"timestamp":"2022-06-25T03:58:10.207820267Z","url_callback":"https://callback-url.com"}'; \
signature=$(echo 'sha256=bf829606cda0ca6923defb5ca70a43135adc7e8887486a201a19cb50ca6006b1' | sed 's/^.*=//'); \
secret=PGuRrhCFajIyEvFlreKL; \
if [[ $signature == $(echo -n $message | openssl dgst -sha256 -hmac $secret | sed 's/^.*= //') ]];
then echo "Valid HMAC";
else echo "Invalid HMAC";
fi
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"log"
"strings"
)
func hmacIsValid(message, signature, secret string) bool {
messageMAC, _ := hex.DecodeString(strings.TrimPrefix(signature, "sha256="))
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(message))
expectedMAC := mac.Sum(nil)
return hmac.Equal([]byte(messageMAC), expectedMAC)
}
func main() {
log.Println(
hmacIsValid(
`{"event_id":0,"created_at":"2022-06-25T03:58:10.207820267Z","creator_id":6,"metadata":{"request_type":"POST","event_user_id":6},"payload":"ping","subscription_id":6,"timestamp":"2022-06-25T03:58:10.207820267Z","url_callback":"https://callback-url.com"}`,
"sha256=bf829606cda0ca6923defb5ca70a43135adc7e8887486a201a19cb50ca6006b1",
"PGuRrhCFajIyEvFlreKL",
),
)
}
require 'openssl'
def hmac_is_valid(message, signature, secret)
signature == "sha256=#{OpenSSL::HMAC.hexdigest('SHA256', secret, message)}"
end
hmac_is_valid(
'{"event_id":0,"created_at":"2022-06-25T03:58:10.207820267Z","creator_id":6,"metadata":{"request_type":"POST","event_user_id":6},"payload":"ping","subscription_id":6,"timestamp":"2022-06-25T03:58:10.207820267Z","url_callback":"https://callback-url.com"}',
'sha256=bf829606cda0ca6923defb5ca70a43135adc7e8887486a201a19cb50ca6006b1',
'PGuRrhCFajIyEvFlreKL'
)
const crypto = require('crypto');
const message = '{"event_id":0,"created_at":"2022-06-25T03:58:10.207820267Z","creator_id":6,"metadata":{"request_type":"POST","event_user_id":6},"payload":"ping","subscription_id":6,"timestamp":"2022-06-25T03:58:10.207820267Z","url_callback":"https://callback-url.com"}';
const signature = 'sha256=bf829606cda0ca6923defb5ca70a43135adc7e8887486a201a19cb50ca6006b1';
const secret = 'PGuRrhCFajIyEvFlreKL';
const hmac = crypto.createHmac('sha256', secret).setEncoding('hex');
hmac.update(message);
if (signature.replace(/^.*=/, '') == hmac.digest('hex')) {
console.log('Valid HMAC');
} else {
console.log('Invalid HMAC');
}
import hmac
def hmac_is_valid(message, signature, secret):
digest = hmac.new(secret.encode('utf-8'), message.encode('utf-8'), 'sha256').hexdigest()
return hmac.compare_digest(signature, f'sha256={digest}')
if hmac_is_valid(
'{"event_id":0,"created_at":"2022-06-25T03:58:10.207820267Z","creator_id":6,"metadata":{"request_type":"POST","event_user_id":6},"payload":"ping","subscription_id":6,"timestamp":"2022-06-25T03:58:10.207820267Z","url_callback":"https://callback-url.com"}',
'sha256=bf829606cda0ca6923defb5ca70a43135adc7e8887486a201a19cb50ca6006b1',
'PGuRrhCFajIyEvFlreKL'):
print("Valid HMAC")
else:
print("Invalid HMAC")
use sha2::Sha256;
use hmac::{Hmac, Mac};
use hex_literal::hex;
type HmacSha256 = Hmac<Sha256>;
fn main() {
let message = b"{\"event_id\":0,\"created_at\":\"2022-06-25T03:58:10.207820267Z\",\"creator_id\":6,\"metadata\":{\"request_type\":\"POST\",\"event_user_id\":6},\"payload\":\"ping\",\"subscription_id\":6,\"timestamp\":\"2022-06-25T03:58:10.207820267Z\",\"url_callback\":\"https://callback-url.com\"}";
let signature = hex!("
bf829606cda0ca6923defb5ca70a4313
5adc7e8887486a201a19cb50ca6006b1
");
let secret = b"PGuRrhCFajIyEvFlreKL";
let mut mac = HmacSha256::new_from_slice(secret).expect("");
mac.update(message);
let result = mac.finalize();
assert_eq!(result.into_bytes()[..], signature, "Invalid HMAC");
}
Advanced Validation Techniques
The timestamp
field
This field indicates the time when the server sent the event to your subscription URL endpoint. If your server does not answer with a 2xx HTTP status code, then our server will retry the event some time later, and the timestamp
value will reflect on each case the current time at which our server is delivering it to the subscription endpoint. You can use this field to prevent replay attacks by disregarding messages with old timestamps (perhaps allowing some margin of a minute to account for network message propagation and network clock skewing).
The url_callback
field
You can use this field to validate that the received message was intended for your subscription URL endpoint by comparing both values. If you have different webhooks subscriptions that share the same secret (not recommended) and some malicious actor has access to the events sent to one of your subscription URL endpoint, they could forward the same events to another one of your subscriptions endpoint and given that the secret is the same the HMAC validation would be valid. By comparing the value of url_callback
in the received event with the expected one for that subscription URL you can prevent this kind of attacks. You may as well always use different secrets so that the HMAC validation fails if someone forwards a message from another subscription.
Other considerations
The Content-Type
header sent by our server to your subscriptions URL endpoint will be application/json
and the HTTP method will be POST
. This is yet another check you can enforce to disregard unexpected messages arriving at your registered endpoints.