Denying bots and abuse
Pre vs Post-authentication If you’re primarily interested in detecting anomalous user behavior post-authentication, sometimes referred to as user behavior analytics, or UBA, then you should instead check out Protecting your login.
Step 1. Select the form you want to protect
Step 2. Include Castle.js Client-side
Include the Castle.js script on each page of your website, not just your page. Castle.js analyzes device properties and user interactions in order to detect bots and scripted activity.
<script src="https://d2t77mnxyo7adj.cloudfront.net/v1/c.js?XTEXTapp_idZTEXT"></script>
<!-- https://www.npmjs.com/package/castle.js -->
<script type="text/javascript" src='dist/c.js'></script>
<script type="text/javascript">
_castle('setAppId', 'XTEXTapp_idZTEXT');
</script>
We’ve populated the example with the sample App ID XTEXTapp_idZTEXT
. Replace the sample App ID with the actual Sandbox App ID you’ll find in your dashboard.
No JavaScript? In order to detect sophisticated bots and abuse, Castle.js is required for
$XTEXTapiZTEXT.attempted
to work. That said, if you’re in an environment where you’re unable to integrate a JavaScript tag, you can still integrate Castle post-authentication in a server-side only manner.
Step 3. Intercept the form Client-side
When the user submits the form, you intercept the submission and call _castle('getClientId')
to generate a single-use token that you then pass to your server-side handler.
var form = document.getElementById('XTEXTapiZTEXT-form');
form.addEventListener("submit", function(evt) {
evt.preventDefault()
// Get the ClientID token
var clientId = _castle('getClientId');
// Populate a hidden <input> field named `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` to the HTML form
form.appendChild(hiddenInput);
form.submit()
});
Step 4. Handle the request Server-side
In your server-side handler, extract the parameter that was passed in the form submission in Step 2, in this example castle_client_id
.
The email
address (alternatively phone
or username
) submitted in the form is a required field and is used by Castle to do the trust assessment.
Castle.api_secret = 'XTEXTapi_secretZTEXT'
begin
castle = Castle::Client.from_request(request)
# IMPLEMENT: Get the Client ID from the form submission
client_id = params[:castle_client_id]
# IMPLEMENT: Get the email form field
email = params[:email]
res = castle.authenticate(
event: '$XTEXTapiZTEXT.attempted',
properties: {
email: email
},
context: {
client_id: client_id
}
)
# You can ignore the returned action during evaluation
if res[:action] == Castle::Verdict::DENY
# ...
end
rescue Castle::Error => e
# handle error
end
Castle::setApiKey('XTEXTapi_secretZTEXT');
try {
// IMPLEMENT: Get the Client ID from the form submission
$clientId = $params['castle_client_id'];
// IMPLEMENT: Get the email form field
$email = $params['email'];
$context = json_decode(Castle_RequestContext::extractJson(), true);
// override the client_id attribute
$context = array('client_id' => $clientId) + $context;
$res = Castle::authenticate(array(
'event' => '$XTEXTapiZTEXT.attempted',
'properties' => Array('email' => $email),
'context' => $context
));
// You can ignore the returned action during evaluation
if ($res->action == 'deny') {
// ...
}
} catch (Castle_Error $e) {
// handle error
}
from castle.configuration import configuration
from castle.client import Client
from castle import events
from castle.verdict import Verdict
configuration.api_secret = 'XTEXTapi_secretZTEXT'
try:
castle = Client.from_request(request)
# IMPLEMENT: Get the Client ID from the form submission
client_id = params.get('castle_client_id')
# IMPLEMENT: Get the email form field
email = params.get('email')
res = castle.authenticate({
'event': '$XTEXTapiZTEXT.attempted',
'properties': {
'email': email
},
'context': {
'client_id': client_id
}
})
# You can ignore the returned action during evaluation
if res['action'] == Verdict.DENY.value:
# ...
except CastleError as e:
# handle error
Castle castle = Castle.initialize("XTEXTapi_secretZTEXT");
try {
// IMPLEMENT: Get the Client ID from the form submission
String clientId = params.get('castle_client_id');
// IMPLEMENT: Get the email form field
String email = params.get('email');
CastleContext context = castle.contextBuilder()
.fromHttpServletRequest(req)
.build();
// override the client_id attribute
context.setClientId(clientId);
CastleMessage payload = CastleMessage.builder("$XTEXTapiZTEXT.attempted")
.properties(ImmutableMap.builder()
.put('email', email)
.build())
.context(context)
.build();
Verdict res = castle.client().authenticate(payload);
// You can ignore the returned action during evaluation
if (res.getAction() == AuthenticateAction.DENY) {
// ...
}
} catch (CastleServerErrorException e) {
// handle error
}
var castle = new CastleClient("XTEXTapi_secretZTEXT");
try {
// IMPLEMENT: Get the Client ID from the form submission
string clientId = params["castle_client_id"];
// IMPLEMENT: Get the email form field
string email = params["email"];
var res = await castle.Authenticate(new ActionRequest()
{
Event = "$XTEXTapiZTEXT.attempted",
Properties = new Dictionary<string, string>()
{
["email"] = email
},
Context = new RequestContext()
{
Ip = Request.HttpContext.Connection.RemoteIpAddress.ToString(),
ClientId = clientId,
Headers = Request.Headers.ToDictionary(x => x.Key, y => y.Value.FirstOrDefault())
}
});
// You can ignore the returned action during evaluation
if (res["action"] == ActionType.Deny) {
// ...
}
} catch (CastleExternalException e) {
// handle error
}
curl https://api.castle.io/v1/authenticate \
-X POST \
-u ":XTEXTapi_secretZTEXT" \
-H "Content-Type: application/json" \
-d '
{
"event": "$XTEXTapiZTEXT.attempted",
"properties": {
"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"
}
}
}'
The deny
action is determined by the policy that you’ve configured in your Policy settings. When you sign up for Castle, there will be a default policies that you can modify to your needs. If no policy triggered, you’ll receive an allow
action.
The Events Debugger will come in handy when inspecting Castle API calls to ensure you got all the details right.
What’s collected behind the scenes
Castle’s server-side SDK will automatically extract IP and key header information from the HTTP request. Sensitive headers such as Cookie
and anything related to authentication are not sent.
Note that during local development, the IP address will likely always be 127.0.0.1. In production, make sure your load balancer or firewall doesn’t override the client IP address with a static internal one.
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 a separate microservice. In that case you’ll need to forward the request context between services.