Preventing Account Takeover
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.
This guide requires that you have access to API credentials. If you don't have them at hand, we recommend following our Quickstart guide first
Client-side integration
This guide shows you how to integrate Castle into a web environment using the browser SDK, Castle.js. If a native mobile application is the primary point of interaction for your customers, you may instead want to check out our guides for integrating Castle with native mobile apps.
Step 1. Install Castle.js
Install Castle.js in your JavaScript app bundle by running npm install --save @castleio/castle-js
or yarn add @castleio/castle-js
. Replace the value YOUR_CASTLE_APP_ID
with the actual one that you'll find in the Castle dashboard.
import * as Castle from '@castleio/castle-js'
Castle.configure(YOUR_CASTLE_APP_ID);
Step 2. Create a request token
Whenever the user submits a request to your 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 or call Castle.createRequestToken();
to generate a single-use token and pass the token to your server with the form data.
import * as Castle from '@castleio/castle-js'
Castle.createRequestToken().then( (requestToken) => {
....
});
// or
const requestToken = await Castle.createRequestToken();
// TODO: add requestToken as parameter to the outgoing request
<!-- Use the form helper to automatically insert the request token on form submit //-->
<form onsubmit="_castle('onFormSubmit', event)">
// ....
</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
See section with available server-side SDKs.
Step 1. 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 Events as well as the API Reference
castle = ::Castle::Client.new
begin
token = request.params['castle_request_token']
context = Castle::Context::Prepare.call(request)
res = castle.risk(
event: '$login',
status: '$succeeded',
request_token: token,
context: {
ip: context[:ip],
headers: context[:headers]
}
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' => [
'ip' => Castle_RequestContext::extractIp(),
'headers' => Castle_RequestContext::extractHeaders()
],
'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
context = ContextPrepare.call(request)
client = Client()
res = client.risk({
'event': '$login',
'status': '$succeeded',
'request_token': token,
'context': {
'ip': context['ip'],
'headers': context['headers']
},
'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 context = ContextPrepareService.call(request, {}, castle.configuration);
const res = castle.risk({
event: '$login',
status: '$succeeded',
request_token: token,
user: {
id: user.id,
email: user.email
},
context: {
ip: context.ip,
headers: context.headers
}
});
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 2. Logging failed login attempts
The example above assumes that you've successfully verified the user's 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. However, the token should be sent whenever available.
castle = ::Castle::Client.new
token = request.params['castle_request_token']
context = Castle::Context::Prepare.call(request)
castle.log(
event: '$login',
status: '$failed',
request_token: token,
user: {
email: request.params['email']
},
context: {
ip: context[:ip],
headers: context[:headers]
}
)
$token = $_POST['castle_request_token'];
Castle::log([
'event' => '$login',
'status' => '$failed',
'request_token' => $token,
'context' => [
'ip' => Castle_RequestContext::extractIp(),
'headers' => Castle_RequestContext::extractHeaders()
],
'user' => [
'email' => $_POST['email']
]
]);
token = request.form['castle_request_token'] # Using Flask
client = Client()
context = ContextPrepare.call(request)
client.log({
'event': '$login',
'status': '$failed',
'request_token': token,
'context': {
'ip': context['ip'],
'headers': context['headers']
},
'user': {
'email': request.form['email']
}
})
String token = request.getParameter("castle_request_token");
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("email", request.getParameter("email"))
.build()
.put("request_token", token)
.build()
)
const token = request.body["castle_request_token"]; // Using Express
const castle = new Castle({ apiSecret: 'YOUR SECRET HERE' });
const context = ContextPrepareService.call(request, {}, castle.configuration);
castle.log({
event: '$login',
status: '$failed',
request_token: token,
user: {
email: request.body["email"]
},
context: {
ip: context.ip,
headers: context.headers
}
});
🎉 Congratulations! -- You have now completed a basic integration of Castle!
Updated almost 2 years ago