Protect Your Login

This guide serves as an outline to the integration steps needed for a Castle Baseline Integration. Upon completing the Castle Baseline Integration, Castle will be able to detect malicious traffic at your login endpoint and allow you block bad actors from accessing your users accounts in real time. Metrics around login attempts, security threats, and user risk scores will also begin populating in the Castle Dashboard, and Castle will highlight user accounts that have been comprimised.

Server Side SDKs

Castle offers several Server Side SDKs to make various parts of the integration easier. If you would like to you one of our Server Side SDKs, you can find them linked below. To configure them, you will need you API Secret from the Castle Dashboard and then install and configure the server-side SDK.

What's being collected behind the scenes

Castle's SDK will automatically extract all the necessary information from the HTTP request. The table below outlines what's being collected, and the Curl authenticate example in the section below illustrates how the data is then sent to Castle through the context object.

Field Type Description
client_id String Either a __cid cookie set by Castle.js, or a X-Castle-Client-Id header set by the Castle mobile SDK.
ip String The user’s IP address During the integration on your local computer this will likely by 127.0.0.1. Make sure your load balancer or firewall doesn't override the IP with an internal one.
headers Object A list of all the HTTP headers, excluding the Cookie or any sensitive authentication headers. The character casing and order of the headers should be intact when sent to Castle.

If your app is split into separate services

In some cases you want to track data to Castle from a context where the above data isn't available, for example when authenticate is called from an separate micro service. In that case you'll need to forward the request context between services.

Step 1. Authenticate Logins

Whenever you make a call to Castle’s authenticate endpoint, you'll receive a recommended action on whether to allow, challenge or deny the event.

As part of the baseline integration, to begin, you can ignore the returned action all together, and treat Castle as if it's in monitoring mode. Once you're comfortable and ready, you can then start taking action and blocking malicious attempts based on Castle's responses.

Example Request

begin
  # Put this after the user's password has been validated, but before you
  # create the login session
  verdict = castle.authenticate(
    event: '$login.succeeded',
    user_id: 'e325bcdd10ac',
    user_traits: {
      email: 'johan@castle.io',
      registered_at: '2015-02-23T22:28:55.387Z'
    }
  )

  # You can ignore the returned action during evaluation
  case verdict[:action]
  when 'allow'
    # ...
  when 'challenge'
    # ...
  when 'deny'
    # ...
  end

rescue Castle::Error => e
  # handle error
end
<?
try {
  // Put this after the user's password has been validated, but before you
  // create the login session
  $verdict = Castle::authenticate(array(
    'event' => '$login.succeeded',
    'user_id' => 'e325bcdd10ac',
    'user_traits' => array(
      'email' => 'johan@castle.io',
      'registered_at' => '2015-02-23T22:28:55.387Z'
    )
  ));

  // You can ignore the returned action during evaluation
  if ($verdict->action == 'allow') {
    // ...
  } else if ($verdict->action == 'challenge') {
    // ...
  } else if ($verdict->action == 'deny') {
    // ...
  }

} catch (Castle_Error $e) {
  // handle error
}
# Put this after the user's password has been validated, but before you
# create the login session
verdict = castle.authenticate({
    'event': '$login.succeeded',
    'user_id': 'e325bcdd10ac',
    'user_traits': {
        'email': 'johan@castle.io',
        'registered_at': '2015-02-23T22:28:55.387Z'
    }
})

# You can ignore the returned action during evaluation
if verdict['action'] == 'allow':
    # ...
elif verdict['action'] == 'challenge':
    # ...
else verdict['action'] == 'deny':
    # ...
ImmutableMap traits = ImmutableMap.builder()
  .put("email", "johan@castle.io")
  .put("registered_at", "2015-02-23T22:28:55.387Z")
  .build());

// Put this after the user's password has been validated, but before you
// create the login session
Verdict verdict = castle.authenticate(
  "$login.succeeded",
  "e325bcdd10ac",
  null,
  null,
  traits
);

// You can ignore the returned action during evaluation
switch (verdict.getAction()) {
  case ALLOW: {
    // ...
  }
  break;
  case CHALLENGE: {
    // ...
  }
  break;
  case DENY: {
    // ...
  }
  break;
}
curl https://api.castle.io/v1/authenticate \
  -X POST \
  -u ":YOUR-API-SECRET" \
  -H "Content-Type: application/json" \
  -d '
    {
      "event": "$login.succeeded",
      "user_id": "e325bcdd10ac",
      "user_traits": {
        "email": "johan@castle.io",
        "registered_at": "2018-07-10T17:37:39.123Z" // ISO 8601 format of user's account creation timestamp
      },
      "context": {
        "client_id": "faf117b2-9457-4e3b-9c13-d2795656b78e-094e81caa170c1d2",
        "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",
          "Accept-Encoding": "gzip, deflate, br",
          "Connection": "Keep-Alive",
          "Content-Length": "122",
          "Content-Type": "application/javascript",
          "Origin": "https://castle.io/",
          "Referer": "https://castle.io/login"
        }
      }
    }'
action Recommendation
allow Log the user in and create a session
challenge If you have access to a second factor such as the user's phone number; halt the login and redirect the user to a Challenge prompt.

If not; log the user in and rely on them reporting any suspicious activity through the Review workflow

deny Deny the login and show the same error message as if the user entered the wrong password, e.g. "Failed to log in"

Step 2. Track Failed Logins

Track $login.failed when the user fails to enter the correct password.

  • user_traits.email: the user-submitted form field value
  • user_id: set if the attempted email matched one of your users

Example Request

begin
  castle.track(
    event: '$login.failed',
    user_id: 'e325bcdd10ac',
    user_traits: {
      email: 'admin@cstl.io',
      registered_at: '2018-07-10T17:37:39.123Z'
    }
  )
rescue Castle::Error => e
  puts e.message
end
<?
Castle::track(array(
  'event' => '$login.failed',
  'user_id' => 'e325bcdd10ac',
  'user_traits' => array(
    'email' => 'admin@cstl.io',
    'registered_at' => '2018-07-10T17:37:39.123Z'
  )
));
castle.track({
    'event': '$login.failed',
    'user_id': 'e325bcdd10ac',
    'user_traits': {
        'email': 'admin@cstl.io',
        'registered_at': '2018-07-10T17:37:39.123Z'
    }
})
ImmutableMap traits = ImmutableMap.builder()
  .put("email", "admin@cstl.io")
  .put("registered_at", "2015-02-23T22:28:55.387Z")
  .build();

castle.track(
  "$login.failed",
  "e325bcdd10ac",
  null,
  null,
  traits
);
curl https://api.castle.io/v1/track \
  -X POST \
  -u ":YOUR-API-SECRET" \
  -H "Content-Type: application/json" \
  -d '
    {
      "event": "$login.failed",
      "user_id": "e325bcdd10ac",
      "user_traits": {
        "email": "admin@cstl.io",
        "registered_at": "2018-07-10T17:37:39.123Z" // ISO 8601 format of user's account creation timestamp
      },
      "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"
        }
      }
    }'

Important: Make sure to set the user_id if there is one for the email address attempted. This will help us identify if a particular user may be getting targeted. If there is no user_id matching the email that attempted to login, you can set it to null: "user_id": null

Backend Checklist

Upon completing the backend side integration, it is important to confirm the following:

☐ Make sure the client_id is populated when available. If there ever is no client_id value available, set it to the boolean false. The value of the client id generate via Castle.js and passed from the front end. See Step 3 for more details

☐ Make sure $login.succeeded events are authenticated after you have validated the credentials

☐ Make sure $login.failed events are tracked when the password is invalid. In this case, be sure to set the user_id value

☐ Make sure $login.failed events are tracked when the username is invalid. In this case, be sure to mark the user_id as null

☐ Make sure when it goes to production, that the ip value is grabbing the value of the true client ip, not an internal ip of one of your servers

☐ For any track or authenticate events that contain a user_id, make sure you are also settings the user's email and registered_at timestamp within the user_traits object. For example:

{
  "event": "$login.succeeded",
  "user_id": "123",
  "user_traits": {
    "email": "joan@castle.io",
    "registered_at": "2018-07-10T17:37:39.123Z" // ISO 8601 format of user's account creation timestamp
  }
  ...

Step 3. Client Side Fingerprinting

Add Castle.js to Every Page

Castle.js profiles browser and app properties to build unique device fingerprints, as well as monitors UI interaction to detect bots and scripted logins.

The Castle.js script tag must be added to the <head> tag of the HTML to ensure all site visitors (and bots) are profiled before logging in to a user account. We recommend adding Castle.js as high up in the head tag as possible, before the code for any analytics or A/B testing platforms. Using a tag manager such as GTM is not recommended.

<head>
  <!-- Replace the digits at the end of the URL with your Castle App ID -->
  <script src="https://d2t77mnxyo7adj.cloudfront.net/v1/c.js?123456789012345"></script>

  ...
</head>

npm Package

If you want to optimize page load speed, you can use the npm package and bundle Castle.js with the rest of your JavaScripts.

Important: The JavaScript snippet should go into as many pages of your site as possible; both before and after the user is authenticated, including the landing page of your website. At a very minimum, you should place it on the page of the login form.

Identify the User

If the user is logged in, you must call our identify method, passing in the current user's id.

This method does not trigger any outbound request, it simply updates an identity variable. By default, Castle's tracking pixels -- which will inlcude the user id you set as a parameter -- are automatically sent on each pageload.

If you are use a templating language where the User ID can be set from the backend before the page is served up, then you should set the User ID in the <head> tag in order to ensure it is set in time.

Example - JS Identify

<head>
  <!-- Replace the digits at the end of the URL with your Castle App ID -->
  <script src="https://d2t77mnxyo7adj.cloudfront.net/v1/c.js?123456789012345"></script>

  <!-- Use your favorite templating language to inject the user ID -->
  <script>
    <% if user_logged_in? %>
      _castle('identify', '<%= current_user.id %>'); // Set the current user's id
    <% end %>
  </script>

  ...
</head>

If you are not using a templating language or are unable to set the user id within the <head> tag, then you should disable Castle from automatically sending tracking pixels. Instead, you will manually send the tracking pixel just after you set the User ID.

Example - JS Identify with Manual Tracking

<head>
  <!-- Replace the digits at the end of the URL with your Castle App ID -->
  <script src="https://d2t77mnxyo7adj.cloudfront.net/v1/c.js?123456789012345"></script>
  <script>
    _castle('autoTrack', false); // Disable Castle from auto-sending tracking pixels on page load
  </script>

  ...
</head>
<body>
  ...

  <!-- Once you are able to identify the user -->
  <script>
    _castle('identify', '{current_user_id}'); // Set the current user's id
    _castle('trackPageview'); // Manually send a Castle tracking pixel
  </script>

  ...
</body>

If you have a Single Page Application: You should call the identify method to update the user id whenever the user logs in. Also, whenever the user logs out, you need to reset the user id by calling _castle('reset');

Pass the Castle Client ID to the Backend

In Steps 1 and Step 2 above, your backend is sending events to Castle. One of the key parameters passed in those events is client_id. The value of the client_id is actually the Client Side Fingerprint. Therefore, it's critical that your front-end explicitly passes this value to the backend whenever a login form is submitted.

To do this, on a submit event, grab the current Castle Client ID and add it as a hidden field in your login form to ensure you are passing it to your backend. Make sure your backend team is able to locate this value and set it as the client_id in the backend events.

Via your JS, you can manually fetch the current Castle Client ID with: _castle('getClientId');

Example - Getting Client ID and Passing it via Form Submit

// The following assumes you have a Form with an onsubmit function like: 
// <form action="POST" onsubmit="onSubmit(this);">...</form>

function onSubmit(myForm) { 
  // The token is continously updated so fetch it right before form submit
  var clientId = _castle('getClientId');

  // Populate a hidden field called `castle_client_id`
  var hiddenInput = document.createElement('input');
  hiddenInput.setAttribute('type', 'hidden');
  hiddenInput.setAttribute('name', 'castle_client_id');
  hiddenInput.setAttribute('value', clientId);

  // Add the `castle_client_id` into the form so it gets sent to the server
  myForm.appendChild(hiddenInput);
};

Front End Checklist

Upon completing the client side integration, it is important to confirm the following:

☐ Castle's JS is loaded into the <head> tag of every page.

☐ On each page load, you see this Castle tracking pixel firing: t.castle.io/v1/c.gif?

☐ Your Castle App ID is correctly set in the tracking pixels. You can find this in the &ai= query string parameter of the pixel.

☐ Whenever a user is logged in, the user id is correctly set in the tracking pixels. You can find this in the &ui= query string parameter or the pixel.

☐ Whenever a form is submitted, the Castle Client ID is successfully passed to the backend. This should be confirmed by the backend team.