Sign In

Webhooks

Introduction

Whenever Castle detects an increased risk in one of your user’s accounts, a security event is triggered and you can configure Castle to notify your application when these events occur using webhooks. In order to help you implement robust account recovery or user review automation workflows, Castle can be configured to send webhooks to your application based on two types of security events:

Security event Description
$review.opened This event will be triggered whenever the action for a device is challenge
$incident.confirmed This event will be triggered whenever action for a device is deny

Device action is referring to the recommended action computed by Castle, either through the risk score or a custom policy. This action corresponds to the value that will be returned by the authenticate API.

Common for both of these events is that they will only be triggered once per device. This is so that you can confidently use them, for example, to trigger user facing messaging such as a security notification email. You will receive at most two webhooks per device.

Note: If your Castle plan includes bot detection, additional webhooks will be triggered for challenge and deny based on bot risk score. For example, if a deny action was determined for a device based on a high bot risk score and, later, another deny verdict was determined based on the authentication risk score, your endpoint would receive two $incident.confirmed webhooks for this device. To determine which type of score triggered the webhook, you may check the data.policy.type field in the webhook payload. In the case where bot detection is included in your plan, you will receive at most four webhooks per device.

Building a webhook endpoint

Whenever security events are triggered and webhooks are enabled in your Castle environment, Castle will send an HTTP POST request to your endpoint in JSON format. At minimum, the webhook endpoint needs to expect and handle an HTTP POST request and confirm successful receipt of the data by returning a 2xx HTTP status code.

Below are the key steps for successfully building out a webhook endpoint

HTTPS URL: If you are using a HTTPS URL for your webhook endpoint, Castle will validate that the connection is secure and your server must be correctly configured with a valid certificate.

Try out the Castle webhooks

Before starting the implementation, it can be useful to try out the webhooks functionality and get to know the data:

  1. Use a service like Webhook Tester to set up a temporary endpoint
  2. Head over to the webhooks settings page, and follow the steps for testing webhooks below
  3. Watch the webhook appear in the web interface

Parse the payload

The data in the webhook request sent by Castle will be formatted using JSON and this will be indicated by the Content-Type HTTP header which will be set to application/json . For a complete specification and example of the content of the request body, see the webhook payload format section of this documentation. Your code for handling webhooks is expected to parse the request body into an object.

Respond with a status code

To let Castle know that you have received the webhook successfully, your endpoint must return a 2xx HTTP status code. Codes that are not in this range, including 3xx codes, will be treated as unsuccessful by Castle.

4xx and 5xx HTTP status codes typically indicate an issue with your endpoint. 4xx indicates Castle sent a request to your server, but the endpoint was not valid, such as a 404 which indicates that the path doesn’t exist. If a 5xx code is returned this usually indicates an unhandled exception in your application code.

Retries: In case of failure, Castle will attempt to deliver your webhooks up to three additional times with exponential back off before giving up.

Test your webhook endpoint

From the webhooks settings page in the Castle Dashboard you can test that your endpoint works correctly. Remember to test your webhook endpoint often, and especially:

  • Upon Creation of a webhook endpoint
  • After deploying it to a live environment
  • After making any changes

Check the signature

This step is optional, but highly recommended. Since a webhook endpoint is a publicly facing URL, anyone could potentially send requests to it, pretending to be Castle, if they got hold of the URL. In order to protect from spoofing, Castle includes a signature header, X-Castle-Signature, in the request so that you can verify the authenticity of the request. The signature is a Hash-based Message Authentication Code using the SHA256 hashing function (HMAC-SHA256) and is computed with the raw JSON request body together with the API secret. To validate the request, simply compute the HMAC within the code that handles the webhook and compare it to the value sent in the signature header. The two values should match.

Example signature checking code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
  require 'json'

  # Using Sinatra
  require 'rubygems'
  require 'base64'
  require 'openssl'
  require 'sinatra'

  helpers do
    # Compare the computed HMAC digest based on the shared secret and the
    # request contents to the reported HMAC in the headers
    def verify_webhook(data, hmac_header)
      digest  = OpenSSL::Digest.new('sha256')
      calculated_hmac =
        Base64.encode64(OpenSSL::HMAC.digest(
          digest,
          'YOUR_API_SECRET',
          data)
        ).strip
      calculated_hmac == hmac_header
    end
  end

  # Respond to HTTP POST requests sent to this web service
  post '/' do
    request.body.rewind
    data = request.body.read
    hmac_header = request.headers['X-Castle-Signature']
    verified = verify_webhook(data, hmac_header)

    # IMPLEMENT: handle the alert

    # Output 'true' or 'false'
    puts "Webhook verified: #{verified}"
  end
  

Testing and enabling webhooks

From the webhooks settings page, you can choose which security events to subscribe to. If you’re using the same webhook endpoint for dispatching all of your security workflows, you can choose to subscribe to all events.

The first step is to create a new webhook entry. You can create as many endpoints as you wish, eg. one per security event, or eg. separate endpoints for internal logging and sending out user notifications.

Create a new webhook

Before enabling the webhook, make sure that the URL that you’ve entered is valid and publicly facing. It is also highly recommended that you test the webhook with a simulated event before enabling it:

Webhook test button

After pressing the “test” button, you’ll get to select which security event to simulate:

Test the webhook

After you select the “Send test webhook” button, Castle will attempt to deliver a webhook to the specified endpoint, simulating the selected security event. If a non 2xx code was returned by your webhook endpoint an error message will show up, indicating that the attempt was not successful together with additional information from the response, which can help you to debug the problem.

Finally, when everything seems to be working as expected, hit the “edit” button and enable the webhook.

Webhook payload format

Note: The geolocation data included in the webhook payload, such as city, longitude and latitude, is inferred from the provided IP address and may deviate from the user’s actual location. IP geolocation is more accurate for land-line and cable networks and less accurate for mobile networks.

The payload of the webhook delivered by Castle is a JSON document with the following fields:

Field Description
api_version Castle API version. This is set to “v1.”
app_id Application ID of your Castle environment.
type Security event type.
created_at Time when the security event was triggered.
data.id A unique webhook identifier.
data.device_token Device token, referencing the device that caused the security event.
data.user_id The user ID of the user that caused the security event.
data.trigger The Castle event that triggered the security event.
data.context.ip IP address of the request that caused the security event.
data.context.isp Object with additional information about the ISP that is tied to the IP address.
data.context.location Object with additional information about the location that is tied to the IP address. Location information, such as country, can be useful to present to the user when sending out notifications.
data.context.policy Object with information about which policy triggered the action.
data.context.user_agent Object with information about the UserAgent of the device that caused the security event. In addition to the raw UserAgent it also contains parsed values (for example, which OS and software was used). Device information, such as OS and browser name, can be useful to present to the user when sending out notifications.
data.properties Properties that were sent with the original request that triggered the security event
data.user_traits User traits object that were sent with the original request that triggered the security event

Example webhook payload:

{
  "api_version": "v1",
  "app_id": "382395555537961",
  "type": "$incident.confirmed",
  "created_at": "2018-06-01T19:38:28.483Z",
  "data": {
    "id": "test",
    "device_token": "eyJhbGciOiJI1NiJ9.eyJ0b2tlbiI6InRlc3QiLCJzaW9uIjowLjF9._-0l6TlDH7m78l19z1amMQ02m7s",
    "user_id": "2",
    "trigger": "$login.succeeded",
    "context": {
      "ip": "1.2.3.4",
      "isp": {
        "isp_name": "CastleNet",
        "isp_organization": "Castle"
      },
      "location": {
        "country_code": "US",
        "country": "United States",
        "region": "Massachusetts",
        "region_code": "MA",
        "city": "Boston",
        "lat": 42.3292,
        "lon": -71.0263
      },
      "policy": {
        "id": "cy_TIbBlS9WIqqNdHZ20tA",
        "revision": "cG2WKpiNQOOvmU0LGg8IJw",
        "name": "My Policy",
        "type": "authentication",
        "action": "deny"
      },
      "user_agent": {
        "raw": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:75.0) Gecko/20100101 Firefox/75.0",
        "browser": "Firefox",
        "version": "75.0",
        "os": "Mac OS X 10.15",
        "mobile": false,
        "platform": "Mac OS X",
        "device": "Unknown",
        "family": "Firefox"
      }
    },
    "user_traits": {
      "email": "test@example.com"
    },
    "properties": {
      "amount": "1000"
    }
  }
}