T
Twenty3w ago
Marian

Are webhook secrets actually used? How?

How should the secret defined in a webhoook setup get submitted with the webhook request? I set up a webhook for test purposes and defined a secret. The service listening to the requests is logging these details: request headers, request body, query string parameters. I can't find the secret anywhere.
No description
11 Replies
thomast
thomast2w ago
@martmull
martmull
martmull2w ago
Hey @Marian thank you for reaching us. Secret is not provided in the webhook data directly. We generate a signature (hashing webhook data, secret and timestamp) that we add to headers in X-Twenty-Webhook-Signature key. So to get an check your secret, you need to create the expected signature and compare it to the X-Twenty-Webhook-Signature header value. FYI here is the code that generates the signature -> https://github.com/twentyhq/twenty/blob/b5e6600c73492b7397b242b9b170f9d19107ef78/packages/twenty-server/src/modules/webhook/jobs/call-webhook.job.ts#L32
import crypto from 'crypto';

private generateSignature(
payload: CallWebhookJobData,
secret: string,
timestamp: string,
): string {
return crypto
.createHmac('sha256', secret)
.update(`${timestamp}:${JSON.stringify(payload)}`)
.digest('hex');
}
import crypto from 'crypto';

private generateSignature(
payload: CallWebhookJobData,
secret: string,
timestamp: string,
): string {
return crypto
.createHmac('sha256', secret)
.update(`${timestamp}:${JSON.stringify(payload)}`)
.digest('hex');
}
Hope it helps.
martmull
martmull2w ago
@Thomas do we have a documentation about secret usage? Looks like description in webhook form is not enough
No description
Marian
MarianOP2w ago
Thanks for the reply! Before I make an effort implementing this (in Go, in my case), I wonder: what exact format and resolution is timestamp supposed to have? Is it the current time on the webhook server?
martmull
martmull2w ago
which is a timestamp in string format
No description
Marian
MarianOP2w ago
Does this mean that Twenty server, Twenty worker, and webhook server are required to have their clocks synchronized to the millisecond? At least my current understanding is that in order to verify the signature, I have to reproduce the signature creation on the webhook server side. And that would require having the exact same timestamp.
martmull
martmull2w ago
no, the timestamp used to generated the signature is also provided in headers X-Twenty-Webhook-Timestamp
martmull
martmull2w ago
I have found the documentation on signature, it is in the rest api documentation, in webhooks, here is the link for company-created webhook -> https://twenty.com/developers/rest-api/core#/webhooks/Company-Created/post
Twenty.com
Open Source CRM
martmull
martmull2w ago
## Headers
### X-Twenty-Webhook-Nonce

Unique identifier for this webhook request to prevent replay attacks. Consumers should ensure this nonce is not reused.

### X-Twenty-Webhook-Signature

HMAC SHA256 signature of the request payload using the webhook secret. To compute the signature:

Concatenate X-Twenty-Webhook-Timestamp, a colon (:), and the JSON string of the request payload.
Compute the HMAC SHA256 hash using the shared secret as the key.
Send the resulting hex digest as this header value.
Example (Node.js):
## Headers
### X-Twenty-Webhook-Nonce

Unique identifier for this webhook request to prevent replay attacks. Consumers should ensure this nonce is not reused.

### X-Twenty-Webhook-Signature

HMAC SHA256 signature of the request payload using the webhook secret. To compute the signature:

Concatenate X-Twenty-Webhook-Timestamp, a colon (:), and the JSON string of the request payload.
Compute the HMAC SHA256 hash using the shared secret as the key.
Send the resulting hex digest as this header value.
Example (Node.js):
const crypto = require("crypto");
const timestamp = "1735066639761";
const payload = JSON.stringify({...});
const secret = "your-secret";
const stringToSign = `${timestamp}:${JSON.stringify(payload)}`;
const signature = crypto.createHmac("sha256", secret)
.update(stringToSign)
.digest("hex");
const crypto = require("crypto");
const timestamp = "1735066639761";
const payload = JSON.stringify({...});
const secret = "your-secret";
const stringToSign = `${timestamp}:${JSON.stringify(payload)}`;
const signature = crypto.createHmac("sha256", secret)
.update(stringToSign)
.digest("hex");
### X-Twenty-Webhook-Timestamp

Unix timestamp of when the webhook was sent. This timestamp is included in the HMAC signature generation to prevent replay attacks.
### X-Twenty-Webhook-Timestamp

Unix timestamp of when the webhook was sent. This timestamp is included in the HMAC signature generation to prevent replay attacks.
so you need hash 256 with your secret ${headers["X-Twenty-Webhook-Timestamp"]}:${JSON.stringify(body.payload)} and check this valus with headers["X-Twenty-Webhook-Signature"] @Félix can you confirm? @Marian is that clearer now?
Marian
MarianOP2w ago
Yes, that is clear now, thank you! For the record, I managed to implement the signature verification in Go.
func createSignature(timestamp string, secret string, payload []byte) string {
stringToSign := fmt.Sprintf("%s:%s", timestamp, string(payload))
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(stringToSign))
return hex.EncodeToString(mac.Sum(nil))
}

func validateSignature(signature string, timestamp string, payload []byte) error {
if signature == "" {
return errors.New("signature is empty")
}
if timestamp == "" {
return errors.New("timestamp is empty")
}
if payload == nil {
return errors.New("payload is nil")
}

expectedSignature := createSignature(timestamp, secret, payload)
if signature != expectedSignature {
return errors.New("signature mismatch")
}

return nil
}
func createSignature(timestamp string, secret string, payload []byte) string {
stringToSign := fmt.Sprintf("%s:%s", timestamp, string(payload))
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(stringToSign))
return hex.EncodeToString(mac.Sum(nil))
}

func validateSignature(signature string, timestamp string, payload []byte) error {
if signature == "" {
return errors.New("signature is empty")
}
if timestamp == "" {
return errors.New("timestamp is empty")
}
if payload == nil {
return errors.New("payload is nil")
}

expectedSignature := createSignature(timestamp, secret, payload)
if signature != expectedSignature {
return errors.New("signature mismatch")
}

return nil
}

Did you find this page helpful?