Security events

track lets you record any actions your users perform, along with properties that describe the action. Castle recognizes common event sequences and discovers anomalies in user behavior.

Example

castle.track(
  event: '$login.succeeded',
  user_id: user.id,
  properties: {
    key: 'value'
  },
  user_traits: {
    key: 'value'
  }
)
castle.track(
    {
        'event': '$login.succeeded',
        'user_id': user.id,
        'properties': {
            'key': 'value'
        },
        'user_traits': {
            'key': 'value'
        }
    }
)
<?
Castle::track(
  array(
    'event' => '$login.succeeded',
    'user_id' => $user->id,
    'properties' => array(
      'key' => 'value'
    ),
    'user_traits' => array(
      'key' => 'value'
    )
  )
);
ImmutableMap properties = ImmutableMap.builder()
  .put("properties_key", "properties_value")
  .build(),

ImmutableMap userTraits = ImmutableMap.builder()
  .put("traits_key", "traits_value")
  .build());

castle.track(
  "$login.succeeded",
  user_id,
  null,
  properties,
  userTraits
);
curl https://api.castle.io/v1/track \
  -X POST \
  -u ":YOUR-API-SECRET" \
  -H "Content-Type: application/json" \
  -d '
    {
      "event": "$login.succeeded",
      "user_id": "1234",
      "properties": {
        "key": "value"
      },
      "user_traits": {
        "key": "value"
      },
      "context": {
        "client_id": "a97b492d-dcc3-4fc1-87d6-65682955afa5",
        "ip": "37.46.187.90",
        "headers": {
          "User-Agent": "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko",
          "Accept": "text/html",
          "Accept-Language": "en-us,en;q=0.5"
        }
      }
    }'
Field Type Description
event String Name of the event. Limited to 1024 characters. Event names that have semantic meaning are prefixed $, and we handle them in special ways.
user_id String When you have access to a logged in user, set this to the same user identifier as when you called identify.
properties Object A one-level hash containing key-value pairs to be associated with the event. Keys and values must be a number or string with fewer than 1024 characters.
user_traits Object A one-level hash containing key-value pairs to be associated with the user. Keys and values must be a number or string with fewer than 1024 characters.

Event types

Tracking Login attempts is considered to be part of a baseline integration of Castle. The rest are optional.

  • Login attempts: A sudden increased in failed login attempts may be the result of a credential scan.
  • Profile updates: Changes to the user’s password and email address right after login may be indicative of an account takeover.
  • Risk feedback: Responses from second factor challenges help Castle understand good versus bad behavior.
  • Custom events: Used to capture user activity that is to be considered risky, such as payouts or transactions.

Tracking login activity

Tracking login attempts is required in order to spot users that have blocked JavaScript in their browser.

Successful logins

Track this event when a user passed the first factor, usually password login.

castle.track(
  event: '$login.succeeded',
  user_id: user.id
)
castle.track(
    {
        'event': '$login.succeeded',
        'user_id': user.id
    }
)
<?
Castle::track(
  array(
    'event' => '$login.succeeded',
    'user_id' => $user->id
  )
);
castle.track("$login.succeeded", user_id)
curl https://api.castle.io/v1/track \
  -X POST \
  -u ":YOUR-API-SECRET" \
  -H "Content-Type: application/json" \
  -d '
    {
      "event": "$login.succeeded",
      "user_id": "1234",
      "context": {
        "client_id": "a97b492d-dcc3-4fc1-87d6-65682955afa5",
        "ip": "37.46.187.90",
        "headers": {
          "User-Agent": "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko",
          "Accept": "text/html",
          "Accept-Language": "en-us,en;q=0.5"
        }
      }
    }'

Failed logins

Tracking failed logins enables protection for account threats such as password guessing.

If you don't know which user that generated the failed login you just omit user_id.

Whenever you have access to the user-submitted form value, add this to the event properties as email or username depending on what you're using to log in.

castle.track(
  event: '$login.failed',
  user_id: '1234', # Omit this if no matching user was found
  properties: {
    email: 'johan@castle.io'
  }
)
castle.track(
    {
        'event': '$login.failed',
        'user_id': '1234', # Omit this if no matching user was found
        'properties': {
            'email': 'johan@castle.io'
        }
    }
)
<?
Castle::track(
  array(
    'event' => '$login.failed',
    'user_id' = > '1234', // Omit this if no matching user was found
    'properties' => array(
      'email' => 'johan@castle.io'
    )
  )
);
castle.track(
  "$login.failed",
  user_id, // Null if no matching user was found
  null,
  ImmutableMap.builder()
    .put("emai", "johan@castle.io")
    .build());
curl https://api.castle.io/v1/track \
  -X POST \
  -u ":YOUR-API-SECRET" \
  -H "Content-Type: application/json" \
  -d '
    {
      "event": "$login.failed",
      "user_id": "1234",
      "properties": {
        "email": "johan@castle.io
      "}
      "context": {
        "client_id": "a97b492d-dcc3-4fc1-87d6-65682955afa5",
        "ip": "37.46.187.90",
        "headers": {
          "User-Agent": "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko",
          "Accept": "text/html",
          "Accept-Language": "en-us,en;q=0.5"
        }
      }
    }'

Tracking profile updates

Password resets

Castle can track scenarios where a password reset request is made for a particular user account. Our engine is able to ingest password resets made for both existing users and non-existent users, and can also ingest the values in the username field on each attempt. We can also track when the legitimate users successfully reset their accounts or when they fail to do so.

  • $password_reset_request.succeeded: A successful attempt was made to reset a user's password.
  • $password_reset_request.failed: Use to record a failed attempt to reset a user's password e.g. reset request for non-existing user. The signature of this event is identical to $login.failed
  • $password_reset.succeeded: The user completed all of the steps in the password reset process and the password was successfully reset. Password resets do not require knowledge of the current password.
  • $password_reset.failed: Use to record when a user failed to reset their password.

User Profile Updates

Some bad actors will attempt to immediately change the profile information after successfully taking over an account. By tracking the $profile_update.succeeded event Castle is able to ingest these profile updates and will adapt the risk score if we detect anomalies around these kinds of activities.

Castle not only tracks when the profile was updated, but also detects when the profile details were actually changed by monitoring the following user_traits for updates:

  • email
  • phone
  • address

As well as when you set the following field to true:

  • password_changed

If you wish to not disclose the actual values for other fields than the password, there is also a *_changed version for each field, e.g. phone_changed

The update monitoring is trained to only detect actual updates and discard formatting updates such as updating phone from "+1 414-245-9224" to "4142459224".

Tracking risk feedback

Incidents

  • $incident.mitigated: Record when an open incident has been mitigated, typically through a password reset flow. This event doesn't require a device context.

Reviews

Out-of-band verification of a risky device:

  • $review.resolved: Record when a user clicks "This Was Me". This will approve the corresponding device and lower the risk to 0.
  • $review.escalated: Record when a user clicks "This Wasn't Me". This will flag the corresponding device, raise the risk to 100, and trigger a $incident.confirmed webhook.

Secondary authentication

Inline verification of a risky device:

  • $challenge.requested: Record when a user is prompted with additional second factor authentication.
  • $challenge.succeeded: Record when additional verification was successful.
  • $challenge.failed: Record when additional verification failed.

Tracking custom events

castle.track(
  event: 'My important event',
  user_id: '1234', # Omit this if no matching user was found
  properties: {
    'my_critical_property' => '52'
  }
)
castle.track(
    {
        'event': 'My important event',
        'user_id': '1234', # Omit this if no matching user was found
        'properties': {
            'my_critical_property' => '52'
        }
    }
)
<?
Castle::track(
  array(
    'event' => 'My important event',
    'user_id' = > '1234', // Omit this if no matching user was found
    'properties' => array(
      'my_critical_property' => '52'
    )
  )
);
castle.track(
  "My important event",
  user_id,
  null,
  ImmutableMap.builder()
    .put("my_critical_property", "52")
    .build());
curl https://api.castle.io/v1/track \
  -X POST \
  -u ":YOUR-API-SECRET" \
  -H "Content-Type: application/json" \
  -d '
  {
    "event": "My important event",
    "user_id": "1234",
    "properties": {
      "my_critical_property": "52"
    }
    "context": {
      "client_id": "a97b492d-dcc3-4fc1-87d6-65682955afa5",
      "ip": "37.46.187.90",
      "headers": {
        "User-Agent": "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko",
        "Accept": "text/html",
        "Accept-Language": "en-us,en;q=0.5"
      }
    }
  }'

Request headers

These headers are automatically set by Castle's server-side SDKs.

When calling APIs from your backend, IP address and user agent headers reflects your server configuration rather than the logged in user. This results in incorrect profiling. Therefore you will need to provide this information with the following headers:

  • X-Castle-Client-Id: The cookie set by Castle.js, available as __cid in your cookie store, or as the header X-Castle-Client-Id when called from mobile.
  • X-Castle-Ip: The user's IP address, e.g. 37.46.187.90
  • X-Castle-Headers: The available headers from the original request initiated by the user's browser, encoded as a JSON string, e.g. {"User-Agent": "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko", "Accept": "text/html", "Accept-Language": "en-us,en;q=0.5"}

The X-Castle-Client-Id header

By forwarding a client identifier from the client-side to the server-side, activity from the two sources can be linked together to form a strong protection against attacks where this link is not present.

Castle.js will forward the client identifier as a browser cookie named __cid.

The Castle iOS and Android SDKs will forward it as the HTTP header X-Castle-Client-Id. See the respective documentation pages for instructions on how to configure the header forwarding.

Here’s a Ruby example on how to extract the Client ID on your server-side:

client_id =
  request.cookies['__cid'] ||
  request.headers['X-Castle-Client-Id']

On iOS, forward the device UUID as client identifier:

[request setValue:uuid forHTTPHeaderField:@"X-Castle-Client-Id"];

NSURL *url = [NSURL URLWithString:@"https://api.yoursite.com/login"];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];

NSString *uuid = [UIDevice currentDevice].identifierForVendor.UUIDString;

: If you have a client-less integration, for instance if you're using Castle to protect a customer-facing API, set client_id to false.

The X-Castle-Headers header

By forwarding HTTP request headers from the server-side, Castle is able to build a richer device fingerprint and prevent malicious actors from spoofing the client environment.

For privacy reasons, you do not want to send the "Cookie" header to Castle, so make sure you delete if from the list of headers.

There are example implementations on how to extract request headers in PHP, Ruby, and Java.