PHPReaction Event Hooking (Webhooks)
Objectives
Allow third-party integrations to build apps which can receive a notification about an action that has been done to an entity, and do something with the information.
For example, when a product is updated, a third-party Shopify synchronization middleware can receive the webhook to a callback endpoint and proceed to synchronize the product on Shopify.
Supported events
Create(postPersist)Update(postUpdate)Delete(postRemove)
Toggleable feature on ERP
*All KVS are available from the quick add list
Webhooking must be enabled on the ERP using the following KeyValue:
phpreaction.refs.activateWebHooking => 1
Project secret key
The phpreaction.refs.projectKey key is to add the ERP’s security key for webhook signature.
Use a secure string long string. This key has a clearance level of 0 (encrypted) and should not be available again,
except if regenerated.
TODO generate project secret instead of user input…
EventSubscriber object
An EventSubscriber is a webhook definition.
List available at /event_hooking/event_subscriber.
Each event subscriber needs these properties:
Entity class: object that we want to have notifications fromEvent type: the action that we want to have notifications fromCallback URL: the url to send the webhook data toEvent key: additional secret string to add to the signature secret TODO remove eventKey, should be generated and stored in a clearance level0(encrypted) KVS instead.
Webhook Request Body
Request body fields explanation:
class: entity fully qualified nameshortname: API shortname for the entityeventType: Type of the event (Create, Update, Delete), identified by it’s slugresourceId: ID of the resourceresourceIri: API identifier of the resourceeventSubscriberId: ID of the EventSubscriber which triggered the webhook notificationeventSubscriberIri: API identifier of the EventSubscribertimestamp: Webhook timestamptenant: Tenant which owns the data
Typically, you will often use shortname, eventType, resourceId and resourceIri.
Example
This is an example of webhook request body using the “Notes” entity:
{
"class": "PHPReaction\\Entity\\NoteBundle\\Note",
"shortname": "notes",
"eventType": "postUpdate",
"resourceId": 1,
"resourceIri": "/open-api/v3/notes/1",
"eventSubscriberId": 1,
"eventSubscriberIri": "/open-api/v3/event_subscribers/1",
"timestamp": 1761699183,
"tenant": "demo1"
}Webhook Signature Secret
Every webhook are signed using the project key and the event key as secret, concatenated using an underscore ”_”.
projectKey_eventKey
For example:
- Project key:
foo - Event key:
bar
The secret key for the webhook signature will be: foo_bar
N.B : If the event key is not specified, only the project key is used as secret.
projectKey
Verify Webhook Signature
When you receive a webhook, ideally you want to confirm that it was indeed sent by the app.
To do so, you have to validate the Signature using these headers:
X-Webhook-Signature: For the signature to verifyX-Webhook-Timestamp: The timestamp of the requestX-Webhook-Version: The webhook version
You also need the webhook secret.
This is a PHP example on how to verify the webhook signature:
/**
* Make signature hash using request body, secret and request timestamp.
*
* @param string $messageBody
* @param string $secret
* @param int|null $timestamp
* @return string
*/
public static function cryptSignature(string $messageBody, string $secret, ?int $timestamp = null): string
{
if (null === $timestamp) {
$date = new \DateTime();
$timestamp = strtotime($date->format('Y-m-d H:i:s'));
}
$hashedSignature = hash_hmac('sha256', $timestamp.'.'.$messageBody, $secret, true);
return base64_encode($hashedSignature);
}
/**
* Verify signature from PHPR.
* Returns true if valid, else returns false.
*
* @param Request $request
* @param string $secret
*
* @return bool
*/
public static function signatureVerify(Request $request, string $secret): bool
{
// Time when the request is received
$verifiedDatetime = new \DateTime();
$signature = $request->headers->get('X-Webhook-Signature');
$timestamp = $request->headers->get('X-Webhook-Timestamp');
$encodedMessageBody = $request->getContent();
$verifiedSignature = SignatureHelper::cryptSignature($encodedMessageBody, $secret, $timestamp);
// Time the request was received
$datetime = new \DateTime(strtotime($timestamp));
$dateTimeDiff = date_diff($datetime, $verifiedDatetime);
$secondsDelay = ($dateTimeDiff->h * 3600) + ($dateTimeDiff->i * 60) + $dateTimeDiff->s;
// Validate signature, optionally validate that it was sent less than a minute ago.
if (hash_equals($signature, $verifiedSignature) && $secondsDelay <= 60) {
return true;
}
// Invalid
return false;
}