Protecting forms pre-authentication

Sometimes you'll experience bots and bad actors attempting to abuse your service at registration or login, and you would like to block them before they even access the service. At registration, you might want to block out multi-accounters or disposable emails before the user accounts are created, and at login you might want to block out any bots before they even test the credentials against your database.

The Filter API lets you send and assess user activity for anonymous users.

The filter call is primarily designed to be used with the for the following events along with the $attempted status:

  • $login
  • $registration
  • $password_reset_request

We also recommend, as outlined in the integration guides for registration and login, to use the Filter API for $failed $login and $registration calls. Note that these can also be sent to the Log API, but that would degrade risk scoring performance since the risk score isn't evaluated for Log events.

Lastly, since your public $password_reset_request form won't authenticate users, both the $succeeded and $failed statuses should be sent to Filter, and not to Risk like for all other events.

The differences from the Risk API

  • user payload is optional, it only supports the optional attributes email and phone, and doesn't support id
  • no users will be created in Castle
  • the format of user.email and user.phone is not validated

Example: Blocking a signup before an account is created

castle = ::Castle::Client.new

begin
  token = request.params['castle_request_token']
  context = Castle::Context::Prepare.call(request)

  res = castle.filter(
    type: '$registration',
    status: '$attempted',
    request_token: token,
    context: {
      ip: context[:ip],
      headers: context[:headers]
    },
    user: { # Optional for the Filter API
      email: '<EMAIL-FROM-FORM-PARAMS>', # Optional
      phone: '<PHONE-FROM-FORM-PARAMS>' # Optional
    }
  )

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

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

  $res = Castle::filter([
    'event' => '$login',
    'request_token' => $token,
    'context' => [
      'ip' => Castle_RequestContext::extractIp(),
      'headers' => Castle_RequestContext::extractHeaders()
    ],
    'user' => [ // Optional for the Filter API
      'email' => '<EMAIL-FROM-FORM-PARAMS>', // Optional
      'phone' => '<PHONE-FROM-FORM-PARAMS>' // Optional
    ]
  ]);

  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.filter({
        'event': '$login',
        'request_token': token,
        'context': {
          'ip': context['ip'],
          'headers': context['headers']
        },
       'user': { # Optional for the Filter API
          'email': '<EMAIL-FROM-FORM-PARAMS>', # Optional
          'phone': '<PHONE-FROM-FORM-PARAMS>' # Optional
        }
    })

    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().filter(ImmutableMap.builder()
    .put(Castle.KEY_EVENT, "$login")
    .put(Castle.KEY_CONTEXT, ImmutableMap.builder()
      .put(Castle.KEY_IP, context.getIp())
      .put(Castle.KEY_HEADERS, context.getHeaders())
      build()
    )
    .put(Castle.KEY_REQUEST_TOKEN, token)
    .put(Castle.KEY_USER, ImmutableMap.builder() // Optional for the Filter API
      .put(Castle.KEY_EMAIL, "<EMAIL-FROM-FORM-PARAMS>m") // Optional
      .put(Castle.KEY_PHONE, "<PHONE-FROM-FORM-PARAMS>") // Optional
      .build()
    )
    .build()
  );
} catch (CastleRuntimeException runtimeException) {
  // Handle error
}

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

if (risk > 0.9) {
  // IMPLEMENT: Deny attempt
};
try {
  var 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.filter({
    event: '$login',
    request_token: token,
    context: {
      ip: context.ip,
      headers: context.headers
    }
    user: { // Optional for the Filter API
      email: '<EMAIL-FROM-FORM-PARAMS>', // Optional
      phone: '<PHONE-FROM-FORM-PARAMS>' // Optional
    }
  });

  if (res.risk > 0.9) {
    // IMPLEMENT: Deny attempt
  }
} catch (e) {
  console.error(e);
}

Tuning and taking action

We recommend implementing the follow user experience based on Castle's risk score:

  • Block the worst of the worst: when you're protecting the login form, when Castle returns 90 or above, you should display the same “Wrong credentials” message you would use for when the user actually get the credentials wrong. This way a bot or bad actor can’t tell the difference between a rule being triggered and a simple incorrect password. When you're protecting the registration form, instead return a message such as "Sorry, you can't create an account right now". Before going live with blocking, you can always visualize all the attempts in the Events view in the Castle dashboard.

  • CAPTCHA the gray area: CAPTCHAs doesn't always deliver the best user experience, but if you can keep the customer exposure low, they can be an effective component in a risk-based approach. Use the Events view to tune the proportion of registrations you're ok with presenting a CAPTCHA. Normally this threshold would end up somewhere around 60-90.

Use the Events view to tune the thresholds for when to block and show a CAPTCHAUse the Events view to tune the thresholds for when to block and show a CAPTCHA

Use the Events view to tune the thresholds for when to block and show a CAPTCHA


Did this page help you?