Directory Sync

Webhooks

Use webhooks to accommodate changes in users and groups.

Subscribe to changes in your directory

WorkOS uses webhooks to automatically notify your application any time certain changes are made to users or groups in your directory.

A webhook URL is an endpoint hosted by your application that subscribes to notifications from WorkOS. WorkOS sends these notifications as Events which detail changes to users and groups. Each time there is a change, WorkOS makes a request to your application's webhook URL containing an Event. You'll receive an Event when new users are created, group properties are updated, or users have been added to a group, just to name a few examples. Each Event contains specific information explaining what happened to trigger the webhook.


When to use Directory Sync webhooks

Webhooks can be used to notify your application of the following directory events:

  • When a user is created or added to the directory.
  • When a user's properties have been updated
  • When a user has been removed from a directory.
  • When a group is created or added to a directory.
  • When a group's properties have been updated.
  • When a user has been added to a group.
  • When a user has been removed from a group.
  • When a group is removed from a directory.

WorkOS will send an Event anytime your application needs to be updated for changes like these. This will keep your application access in sync for occasions like onboarding new employees, deprovisioning an employee who has resigned, or updating a group.


Building a webhook URL

Webhook data is sent as a JSON object in the body of the POST request WorkOS makes to your webhook URL. The complete event details are included and can be used after parsing the response's JSON body.

Creating your application's webhook URL is similar to creating any page on a website or application. For example, using Node Express to receive webhooks might look like:

app.js

file_copy
const express = require('express')
const app = express();
const port = 9000;

app.use(express.json());
app.post('/webhook', (req, res) => {
  const sig = req.header['x-workos-signature'];
  const { data, event } = req.body;

  res.json(req.body);
});

app.listen(port, () => console.log(`localhost:9000/webhook`));

Configuring a webhook URL

After building, testing, and deploying your webhook URL to to your desired environment, you need to configure the URL where WorkOS will send events.

Set up is simple—add your webhook URL to the field labelled "Webhook URL" in the Webhooks Dashboard, and click the "Save Settings" button.

Configure webhook URL


Verifying webhook signatures

WorkOS signs the webhook events it sends to your endpoints by including a signature in each event's x-workos-signature header. This lets you verify that all events you're receiving have been sent by WorkOS, and not a third party.

Before you can verify your event signatures, you need to retrieve your Webhook Secret from the Webhooks Dashboard.

Webhooks secret

After retrieving your Webhook Secret, you can use the following information to verify your event signaures.

The x-workos-signature header contains a unix epoch timestamp and one or more signatures.

The timestamp is prefixed by t=, and each signature is prefixed by a scheme. Schemes begin with v, followed by an integer. Currently, the only valid signature scheme is v1.

x-workos-signature Header

file_copy
WorkOS-Signature: t=1492774577, v1=88cd2108b5347d973cf39cdf9053d7dd42704876d8c9a9bd8e2d168259d3ddf7

Note: x-workos-signature headers will always be contained all in one line.

WorkOS generates event signatures using a HMAC with SHA-265.

Step 1. Extract the timestamp and signatures from the header

Split the x-workos-signature header, using the , character as the separator. Then, using this list of elements, split each element using the = character as the separator. This will give you a prefix and value pair.

The value of the prefix t corresponds to the timestamp, and the v1 corresponds to the signature(s).

These are the only relevant pieces of information you'll need. You can discard all other elements.

Step 2. Prepare the signed_payload string

Prepare the signed_payload by concatenating:

  • The timestamp (as a string)
  • The . character
  • The JSON payload (i.e. the request's body)

Step 3. Determine the expected signature

Compute an HMAC using the SHA-256 hash function. Then, use your URL's Webhook Secret as the key, and use the signed_payload string as the message.

Step 4. Compare signatures

Compare the signature(s) in the header to the expected signature. If a signature matches, compute the difference between the current timestamp and the received timestamp. Finally, decide if the difference is within your tolerance.

To protect against timing attacks, use a constant-time string comparison to compare the expected signature to each of the received signatures.

Directory Sync Webhook Events

Event Types

dsync.user.created: Triggers when a user is created or added to the directory.

dsync.user.updated: Triggers when a user's properties have been updated.

dsync.user.deleted: Triggers when a user has been removed from a directory.

dsync.group.created: Triggers when a group is created or added to a directory.

dsync.group.updated: Triggers when a group's properties have been updated.

dsync.group.user_added: Triggers when a user has been added to a group.

dsync.group.user_removed: Triggers when a user has been removed from a group.

dsync.group.deleted: Triggers when a group is removed from a directory.

Event Payloads

dsync.user.created

Triggers when a user is created or added to the directory.

JSON

file_copy
{
  "event": "dsync.user.created",
  "data": {
    "directory_id": "scim_edp_01E1X194NTJ3PYMAY79DYV0F0P",
    "id": "scim_usr_01E1X1B89NH8Z3SDFJR4H7RGX7",
    "first_name": "Lela",
    "username": "veda@{foo-corp.com}",
    "last_name": "Block",
    "emails": [
      {
        "type": "work",
        "value": "veda@{foo-corp.com}",
        "primary": true
      }
    ]
  }
}

dsync.user.updated

Triggers when a user's properties have been updated.

JSON

file_copy
{
  "event": "dsync.user.updated",
  "data": {
    "directory_id": "scim_edp_01E1X194NTJ3PYMAY79DYV0F0P",
    "id": "scim_usr_01E1X1B89NH8Z3SDFJR4H7RGX7",
    "first_name": "Veda",
    "username": "veda@{foo-corp.com}",
    "last_name": "Block",
    "emails": [
      {
        "type": "work",
        "value": "veda@{foo-corp.com}",
        "primary": true
      }
    ]
  }
}

dsync.user.deleted

Triggers when a user has been removed from a directory.

Note: Removing a user from a directory differs from a user being deactivated. If a user is deactivated, then a dsync.user.updated webhook event will be triggered.

Certain directory providers, like Okta, will never issue DELETE commands, and will instead deactivate the user, resulting in dsync.user.updated webhooks.

JSON

file_copy
{
  "event": "dsync.user.deleted",
  "data": {
    "directory_id": "scim_edp_01E1X194NTJ3PYMAY79DYV0F0P",
    "id": "scim_usr_01E1X1B89NH8Z3SDFJR4H7RGX7",
    "first_name": "Veda",
    "username": "veda@{foo-corp.com}",
    "last_name": "Block",
    "emails": [
      {
        "type": "work",
        "value": "veda@{foo-corp.com}",
        "primary": true
      }
    ]
  }
}

dsync.group.created

Triggers when a group is created or added to a directory

Note: All of the returned group's memberships are provided in the dsync.group.created webhook event payload.

JSON

file_copy
{
  "event": "dsync.group.created",
  "data": {
    "directory_id": "scim_edp_01E1X194NTJ3PYMAY79DYV0F0P",
    "id": "scim_grp_01E1X5GPMMXF4T1DCERMVEEPVW",
    "name": "Developers",
    "users": [
      {
        "id": "scim_usr_01E1X2NKBWA5YDYF23Q7G45YGA",
        "first_name": "Kiana",
        "username": "kiana@{foo-corp.com}",
        "last_name": "Flatley",
        "emails": [
          {
            "type": "work",
            "value": "kiana@{foo-corp.com}",
            "primary": true
          }
        ]
      },
      {
        "id": "scim_usr_01E1X1B89NH8Z3SDFJR4H7RGX7",
        "first_name": "Veda",
        "username": "veda@{foo-corp.com}",
        "last_name": "Block",
        "emails": [
          {
            "type": "work",
            "value": "veda@{foo-corp.com}",
            "primary": true
          }
        ]
      },
      {
        "id": "scim_usr_01E1X56GH84T3FB41SD6PZGDBX",
        "first_name": "Eric",
        "username": "eric@{foo-corp.com}",
        "last_name": "Schneider",
        "emails": [
          {
            "type": "work",
            "value": "eric@{foo-corp.com}",
            "primary": true
          }
        ]
      }
    ]
  }
}

dsync.group.updated

Triggers when a group's properties have been updated.

JSON

file_copy
{
  "event": "dsync.group.updated",
  "data": {
    "directory_id": "scim_edp_01E1X194NTJ3PYMAY79DYV0F0P",
    "id": "scim_grp_01E1X1B89NH8Z3SDFJR4H7RGX7",
    "name": "Developers"
  }
}

dsync.group.user_added

Triggers when a user has been added to a group.

JSON

file_copy
{
  "event": "dsync.group.user_added",
  "data": {
    "directory_id": "scim_edp_01E1X194NTJ3PYMAY79DYV0F0P",
    "user": {
      "id": "scim_usr_01E1X56GH84T3FB41SD6PZGDBX",
      "first_name": "Eric",
      "username": "eric@{foo-corp.com}",
      "last_name": "Schneider",
      "emails": [
        {
          "type": "work",
          "value": "eric@{foo-corp.com}",
          "primary": true
        }
      ]
    },
    "group": {
      "id": "scim_grp_01E1X5GPMMXF4T1DCERMVEEPVW",
      "name": "Developers"
    }
  }
}

dsync.group.user_removed

Triggers when a user has been removed from a group.

JSON

file_copy
{
  "event": "dsync.group.user_removed",
  "data": {
    "directory_id": "scim_edp_01E1X194NTJ3PYMAY79DYV0F0P",
    "user": {
      "id": "scim_usr_01E1X56GH84T3FB41SD6PZGDBX",
      "first_name": "Eric",
      "username": "eric@{foo-corp.com}",
      "last_name": "Schneider",
      "emails": [
        {
          "type": "work",
          "value": "eric@{foo-corp.com}",
          "primary": true
        }
      ]
    },
    "group": {
      "id": "scim_grp_01E1X5GPMMXF4T1DCERMVEEPVW",
      "name": "Developers"
    }
  }
}

dsync.group.deleted

Triggers when a group is removed from a directory.

JSON

file_copy
{
  "event": "dsync.group.group_deleted",
  "data": {
    "directory_id": "scim_edp_01E1X194NTJ3PYMAY79DYV0F0P",
    "id": "scim_grp_01E1X5GPMMXF4T1DCERMVEEPVW",
    "name": "Developers"
  }
}