Baseline Integration of Castle

This guide serves as an outline to the integration steps needed for a baseline integration. It also details how Castle will expose your existing security blind spots and how you can leverage Castle’s tools to ensure your users are protected.

Step 1. Castle.js

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

<script src="https://d2t77mnxyo7adj.cloudfront.net/v1/c.js"></script>
<script>
  // Your Castle App ID
  _castle('setAppId', '451236789012343');

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

Placement on the page

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.

On what pages?

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.

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.

Step 2. Server-side SDK

This event tracking enriches Castle’s fingerprinting and serves to spot botnet or machine automated attacks on your application. A spike in failed login attempts for example, is indicative of a brute force or credential stuffing attack and allows you to make use of Castle’s auto-mitigation tools to defend against hacking attempts.

For optimal performance, you should disable any dynamic authentication logic such as IP rate limiting or WAF rules, as well as remove any CAPTCHA prompts after you've added the login tracking. Mechanisms like these will "hide" malicious traffic from Castle.

Fetch your API Secret from the Castle Dashboard and then install and configure the server-side SDK:

Step 3. authenticate successful login attempts

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.

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',
      created_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',
      'created_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',
        'created_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':
    # ...
CastleContext context = Castle.contextBuilder()
    .fromHttpServletRequest(req)
    .build();

// Put this after the user's password has been validated, but before you
// create the login session
Verdict verdict = Castle.client().authenticate(CastleMessage.builder("$login.succeeded")
    .context(context)
    .userId(userId)
    .userTraits(ImmutableMap.builder()
        .put("email", "johan@castle.io")
        .put("created_at", "2015-02-23T22:28:55.387Z")
        .build())
    .build()
);

// 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"
      },
      "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"

What's being collected behind the scenes

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 previous section 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 4. track failed login attempts

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
begin
  castle.track(
    event: '$login.failed',
    user_id: 'e325bcdd10ac',
    user_traits: {
      email: 'admin@cstl.io'
    }
  )
rescue Castle::Error => e
  puts e.message
end
<?
Castle::track(array(
  'event' => '$login.failed',
  'user_id' => 'e325bcdd10ac',
  'user_traits' => array(
    'email' => 'admin@cstl.io'
  )
));
castle.track({
    'event': '$login.failed',
    'user_id': 'e325bcdd10ac',
    'user_traits': {
        'email': 'admin@cstl.io',
    }
})
CastleContext context = Castle.contextBuilder()
    .fromHttpServletRequest(req)
    .build();

Castle.client().track(CastleMessage.builder("$login.failed")
    .context(context)
    .userId("e325bcdd10ac")
    .userTraits(ImmutableMap.builder()
        .put("email", "admin@cstl.io")
        .build())
    .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": "e325bcdd10ac",
      "user_traits": {
        "email": "admin@cstl.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"
        }
      }
    }'