Configuring Webhooks with Eventplans
Most entities and changes in Unimicro will emit an event. These events can be subscribed to through Eventplans. Whenever an event is emitted it looks for eventplans that listen to that entity. The eventplans contains filters for operations like create, delete and update. In addition an expression can be added them to further filter out which events are of value to the listener. This page explains some of the fields of an eventplan, you can read more about creating or editing an Eventplan, EventSubscriber or ExpressionFilter on the Unimicro swagger documentation.
Unimicro implements an interface with an expression builder on all it's frontend environments under https://baseurl.domain/#/admin/flow/flows
, where Eventplans can be created or editet from the flows list.
Webhook
One type of Eventplan that can be created is a Webhook plan. This is as simple as setting the Eventplan.PlanType property to Webhook (enum value 0), and providing a webhook url on a EventSubscriber's Endpoint property. An example of an Eventplan setup to send events to a webhook might look like: (In json)
{
"Name": "Webhook for CustomerInvoices"
"Active": true
"ModelFilter": "CustomerInvoice", // Which Entity to listen to
"OperationFilter": "CUD", // Create, Update, Delete
"PlanType": 0, // Webhook
"EventSubscribers": [
{
"Name": "Webhook subscriber",
"Endpoint": "https://webhook.site/some/path",
"Active" true
}
]
}
This is a simple example of what an Eventplan might look like. Additional properties that can be added to an Eventplan is [SigningKey](#Privat nøkkel - Unimicro-Signature) and [ExpressionFilters](#ExpressionFilters - Control the flow of data)
SigningKey - Unimicro-Signature
Verify the event Unimicro sends your endpoints
Every webhook sent by Unimicro will include a signature in each event's Unimicro-Signature
header. This header allows you to verify that every event is sent by Unimicro and not by a third-party. You'll need to [verify the signature](#Verify Signature) manually.
Before you can verify the signature you need to create or get your signing key from Unimicro. This token can be set when creating your webhook, and found on the flow dashboard (/#/admin/flow/flows). If you do not set a token for signing, then a random guid will be generated for you. When creating an Eventplan through our Api you can set this key by setting the SigningKey
property on the Eventplan.
// Eventplan
{
"Name": "My eventplan",
...
"SigningKey": "6e9eab0d-1391-4aef-954f-dd0498917c0b"
"Subscribers": [...]
}
The signature does not need to be validated, but we highly recommend validating it and it's timestamp to prevent tampering, man-in-the-middle attacks, spoofing and replay attacks.
Verify Signature
The header includes a timestamp and a signature. The timestamp is prefixed with t=
while the signature is prefixed with v1=
, and the fields are comma separated. A full header could look like:
Unimicro-Signature:
t=1600327644,
v1=2b1cc858a14bac03ac283819d16e43d8f2da40591db87c99b6ced02589ae74a8
no new lines are present in the actual header. The new lines are added for readability.
The signature (v1
) is generated with HMAC-SHA256.
Verifying
Step 1: Extract the fields from the headers
Split the header on ','
to seperate the timestamp and signature. Then split each part on '='
to get the key and value pairs. The key t
is referencing the timestamp, and v1
is referencing the signature. All other elements can be discarded.
Step 2: Generate the signaturePayload
They payload used to create the signature consists of the timestamp with the full JSON payload. The timestamp is concatinated with the JSON, with a '.'
as a seperator.
Example;
timestamp: 1600327644
json: {"Event": "webhook", "Entity": "CustomerInvoice"}
signaturePayload: timestamp + "." + json.ToString()
Step 3: Generate the expected signature
Use the set or autogenerated signing token to compute an HMAC with the SHA256 hash function. Use the signaturePayload
string as the message, and signing token as secret key. Your token can be found when creating the webhook, or on the flow dashboard.
Step 4: Compare signatures
Use the generetated signature and compare it to the v1
signature in the Unimicro-Signature
header. If they match you have a valid payload. To protect against replay-attack; validate the timestamp in the header by comparing it to the current time (utc), then decide if it's within your tolerance of difference.
Example code in C#:
static readonly UTF8Encoding encodingUTF8
= new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
static string rawData = @"{""EventType"":""Create"",""EntityName"":""CustomerInvoice"",""Reason"":""POST /api/biz/invoices""}";
static string header = "t=1600333361,v1=46f82a2f3ea8e9e9e0d1c962fbddd71846c671ea927659f5f3265d172913ec30";
static string SigningKey = "d643b78d-f4bd-4538-b7a0-a1119c6e5c7b";
void Main()
{
var headerDictionary = ParseEconomySignature(header);
var signature = GenerateSignature(SigningKey, rawData, headerDictionary["t"]);
Assert.IsTrue(signature == headerDictionary["v1"])
}
static Dictionary<string, string> ParseEconomySignature(string signatureHeader)
{
return signatureHeader.Trim()
.Split(',')
.Select(item => item.Trim().Split('=', 2))
.ToDictionary(item => item[0], item => item[1]);
}
static string GenerateSignature(string signingKey, string content, string time)
{
var secret = encodingUTF8.GetBytes(signingKey);
var payload = encodingUTF8.GetBytes($"{time}.{content}");
using var hmac = new HMACSHA256(secret);
var hash = hmac.ComputeHash(payload);
return BitConverter.ToString(hash).Replace("-", "").ToLower();
}
ExpressionFilters - Control the flow of data
When an event is emitted it uses 3 properties to deterine if and where to post the event. The first is from the entity handler that emits the event, if there are no plans listening to this handler nothing will happen. This is set by the ModelFilter
property. Secondly it ensures the plan we're using is listening to the operation that's happening, if its a (C) created event, (D) deleted or (U) updated operation. This is set by the OperationFilter
property. This filters out a lot of events a listener might not be interested in.
ExpressionFilters
For users that want even more control of what data they recieve from our API can create Expressionfilters. An ExpressionFilter is an object linked to an entity. It's important to match the ModelFilter
names with the ExpressionFilters, else the Eventplan api will abort with a BadRequest response.
An ExpressionFilter looks like: (in json)
{
"EntityName": "CustomerInvoice", // Must be present in ModelFilter
"Expression": "<expression code>", // See next section on how to write these
"EventplanID": 0 // Which eventplan this belongs to
}
An eventplan can have multiple Expressionfilters, but only 1 expressionfilter per ModelFilter in Eventplan.
Expression code
ExpressionFilters allows for my advanced filtering by writing simple logic. When writing expressions for your ExpressionFilter there are a few rules that need to be followed:
- Logical equality must be written with a single equals sign (
=
) . So 1 == 1 has to be written as1 = 1
. - Parenthesis are supported
- Example:
updated(Customer, "Name") and (Customer.Name = "Kjell" or Customer.Name = "Sarah")
- Name has to have changed, and it has to be changed to Kjell or Sarah.
- Example:
- Logical operators like
||
and&&
are written asor
andand
- There are 5 built in functions that can be used
updated(EntityName, "PropertyName")
- Returns true if the property provided changes value, in this exampleEntityName.PropertyName
startswith(variable, "text")
- Returns true if the property (must be a string) starts with "text".- Note: This is case-insensitive
contains(variable, "text")
- Returns true if the variable (must be a string) contains the substring "text"- Note: This is case-insensitive
isnull(variable)
- Returns true if the variable is null (empty string "", null, "''" or 0)isnotnull(variable)
- Returns true if the variable has a value (not "", "''", 0 or null)
- The entity that is being listened to must be present in the expression. For instance:
CustomerInvoice.StatusCode = 42004
(StatusCode is Paid)- The entity must be valid, a 400 - Badrequest response will be returned if the entity is not valid
- All properties on a model can be used in the expression, as long as they are numbers, strings, datetimes, nodatimes, enums. Nested properties might be unstable at this current time.
Example Expression
This expression filters out all Invoices that are sent out through factoring (third party invoice distributors), and are marked as paid. With the addition of updated
in the expression, it will only evaluate to true if the status updates, and being set to paid. When this happens the customer paid the invoice directly instead of paying it through the third-party, which is an attractive event so that the third-party invoice can be cancled.
updated(CustomerInvoice, "StatusCode") and CustomerInvoice.StatusCode = 42004 and (CustomerInvoice.CollectorStatusCode > 42500 and CustomerInvoice.CollectorStatusCode < 42507)