New

If you signed up before June 3, 2021, see the migration guide to learn about the recent API changes.

Prevent account takeovers

A quick guide for assessing the risk of logins using Castle's Risk API. This API can be used at any point in your application to determine when to step up verification.

Client-side integration

This guide shows you how to integrate Castle into a web environment using the browser SDK, Castle.js. There are also guides for integrating Castle with native mobile apps.

Step 1. Include Castle.js

Include Castle.js in the <head> section of each page on your site, not just the signup page.

Replace the sample App ID 114165884929488 with the actual one that you’ll find in the Castle dashboard.

<script src="https://d2t77mnxyo7adj.cloudfront.net/v1/c.js?114165884929488"></script>

We also provide an npm package that makes it easier to load and use Castle.js as a module, and it will prevent privacy plugins from blocking it.

Step 2. Create a request token

Whenever the user submits a request to you app, for instance during login or registration, you need to create a request_token and pass it as a parameter in the request to your server.

For standard form posts, intercept the submit event for the form you want to protect, call castle.createRequestToken() to generate single-use token, and pass the token to your server.

_castle('createRequestToken').then(function(requestToken) {
  var requestToken = requestToken
});

View this gist for a complete example on how to inject the request token into a HTML form.

“Single-use” in this context means that a request_token can actually be used twice per request: once for a call to the Filter API, and one for the Risk API, should you use call both for the same request.

Server-side integration

Start by installing the server-side SDK for Ruby, Python, PHP, Java, or Node

Step 3. Pass the request token to the Castle API

Provide your API Secret as well as the request token string that was passed in the request in the previous step.

See the list of supported event names, as well as the API definition.

castle = ::Castle::Client.new

begin
  token = request.params['castle_request_token']

  res = castle.risk(
    event: '$login',
    status: '$succeeded',
    request_token: token,
    context: Castle::Context::Prepare.call(request)
    user: {
      id: user.id,
      email: user.email
    }
  )

  if res[:risk] > 0.9
    # IMPLEMENT: Deny attempt
  end

rescue Castle::Error => e
  # Handle error
end
try {
  $token = $_POST['castle_request_token'];

  $res = Castle::risk([
    'event' => '$login',
    'status' => '$succeeded',
    'request_token' => $token,
    'context' => Castle_RequestContext::extract(),
    'user' => [
      'id' => user->id,
      'email' => user->email
    ]
  ]);

  if ($res->risk > 0.9) {
    // IMPLEMENT: Deny attempt
  }

} catch (Castle_Error $e) {
  // Handle error
}
try:
    token = request.form['castle_request_token'] # Using Flask

    client = Client()

    res = client.risk({
        'event': '$login',
        'status': '$succeeded',
        'request_token': token,
        'context': ContextPrepare.call(request),
        'user': {
            'id': user.id,
            'email': user.email
        }
    })

    if res['risk'] > 0.9:
        # IMPLEMENT: Deny attempt

except CastleError as e:
     # Handle error
String token = request.getParameter("castle_request_token");

Castle castle = Castle.initialize();
CastleContextBuilder context = castle.contextBuilder().fromHttpServletRequest(request)

try {
  CastleResponse response = castle.client().risk(ImmutableMap.builder()
    .put(Castle.KEY_EVENT, "$login")
    .put(Castle.KEY_STATUS, "$succeeded")
    .put(Castle.KEY_CONTEXT, ImmutableMap.builder()
      .put(Castle.KEY_IP, context.getIp())
      .put(Castle.KEY_HEADERS, context.getHeaders())
      build()
    )
    .put(Castle.KEY_USER, ImmutableMap.builder()
      .put(Castle.KEY_USER_ID, user.getId())
      .put(Castle.KEY_EMAIL, user.getEmail())
      .put("username", user.getUsername())
      .build()
    )
    .put(Castle.KEY_REQUEST_TOKEN, token)
    .build()
  );
} catch (CastleRuntimeException runtimeException) {
    // Handle errors
}

float risk = response.json()
   .getAsJsonObject()
   .get("risk")
   .getAsFloat();

if (risk > 0.9) {
  // IMPLEMENT: Deny attempt
};
try {
  const token = request.body["castle_request_token"]; // Using Express

  const castle = new Castle({ apiSecret: 'YOUR SECRET HERE' });

  const payload = PayloadPrepareService.call(
    {
      event: '$login',
      status: '$succeeded',
      request_token: token,
      user: {
        id: user.id,
        email: user.email
      }
    },
    request,
    castle.configuration
  );

  const res = castle.risk(payload);

  if (res.risk > 0.9) {
    // IMPLEMENT: Deny attempt
  }
} catch (e) {
  console.error(e);
}
As a starting point, it’s recommended that you deny any attempts where the risk score is above 0.9.

The API response can be used to write granular risk logic. Read the complete list of signals and the guide on Policies for more information.

{
  "risk": 0.67,
  "signals": {
    "new_device": {},
    "new_country": {},
    "proxy_ip": {},
    "impossible_travel": {},
    "multiple_accounts_per_device": {},
  },
  "policy": {
    "action": "challenge",
    "name": "Trigger MFA on suspicious logins",
    "id": "e14c5a8d-c682-4a22-bbca-04fa6b98ad0c",
    "revision_id": "b5cf794e-88c0-426e-8276-037ba1e7ceca"
  },
  "device": {
    "token": "eyJhbGciOiJIUzI1NiJ9.eyJ0b2tlbiI6IlQyQ"
  }
}

Step 4. Logging failed login attempts

The example above assumes that you’ve successfully verified the users credentials. Whenever this verification failed, we recommend logging a $login event with status $failed to Castle. This extra information will enhance the risk detection and allow Castle to more accurately discover malicious activity such as credential stuffing or brute force attacks

The Log API is asynchronous and does not return any data. This also makes the response time of this API very low.

The Log API does not require request_token value since it can be called out of band and the resulting event will not attach to an existing device.

castle = ::Castle::Client.new

castle.log(
  event: '$login',
  status: '$failed',
  user: {
    id: user.id,
    email: user.email # optional for Log
  },
  context: Castle::Context::Prepare.call(request)
)
Castle::log([
  'event' => '$login',
  'status' => '$failed',
  'context' => Castle_RequestContext::extract(),
  'user' => [
    'id' => user->id,
    'email' => user->email // optional for Log
  ]
]);
client = Client()

client.log({
  'event': '$login',
  'status': '$failed',
  'context': ContextPrepare.call(request),
  'user': {
    'id': user.id,
    'email': user.email # optional for Log
  }
})
Castle castle = Castle.initialize();
CastleContextBuilder context = castle.contextBuilder().fromHttpServletRequest(request)
castle.log(
  ImmutableMap.builder()
    .put("event", "$login")
    .put("status", "$failed")
    .put("context", ImmutableMap.builder()
      .put("ip", context.getIp())
      .put("headers", context.getHeaders())
      .build()
    .put("user", ImmutableMap.builder()
      .put("id", user.getId())
      .put("email", user.getEmail()) // optional for Log
      .build()
    .build()
)
  const castle = new Castle({ apiSecret: 'YOUR SECRET HERE' });

  const payload = PayloadPrepareService.call(
    {
      event: '$login',
      status: '$failed',
      user: {
        id: user.id,
        email: user.email  // optional
      }
    },
    request,
    castle.configuration
  );

  castle.log(payload);
});

🎉 Congratulations! – You have now completed a basic integration of Castle!