Protecting anonymous actions

It is common to experience bots and bad actors attempting to abuse your service at registration or login, and in most cases you would want 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.

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

  • instead of user.id, matching_user_id is used to reference a user in your database when it matches the user-submitted form parameters
  • instead of a user object, there's a params object the format of params.email and params.phone is not validated. If the email or phone value matches any existing user in Castle, the user object will be resolved and populated for this event.
  • an authenticated: false attribute will be set on the events so that they can conditionally be filtered out from user timelines

Example: Blocking a signup before an account is created

📔 You will need to generate and forward the request_token string from your frontend by using the Browser SDK or a Mobile SDK.

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]
    },
    matching_user_id: 'ca1242f498', # Optional
    params: {
      email: request.params['email'] # Optional
    },
  )

  if res[:policy][:action] == 'deny'
    # IMPLEMENT: Deny attempt
  end

rescue Castle::InvalidRequestTokenError
  # Deny attempt. Likely a bad actor bypassing fingerprinting
rescue Castle::Error => e
  # Allow attempt. Data missing or invalid, or a server or timeout error
end
try {
  $token = $_POST['castle_request_token'];

  $res = Castle::filter([
    'type' => '$registration',
    'status' => '$attempted',
    'request_token' => $token,
    'context' => [
      'ip' => Castle_RequestContext::extractIp(),
      'headers' => Castle_RequestContext::extractHeaders()
    ],
    'matching_user_id' => 'ca1242f498', // Optional
    'params' => [
      'email' => $_POST['email']
    ]
  ]);

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

} catch (Castle_InvalidRequestTokenError $e) {
  // Deny attempt. Likely a bad actor bypassing fingerprinting
} catch (Castle_Error $e) {
  // Allow attempt. Data missing or invalid, or a server or timeout error
}
try:
    token = request.form['castle_request_token'] # Using Flask
    context = ContextPrepare.call(request)

    client = Client()

    res = client.filter({
        'type': '$registration',
        'status': '$attempted',
        'request_token': token,
        'context': {
          'ip': context['ip'],
          'headers': context['headers']
        },
      'matching_user_id': 'ca1242f498', # Optional
      'params': {
        'email': request.form['email']
      }
    })

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

except InvalidRequestTokenError:
    # Deny attempt. Likely a bad actor bypassing fingerprinting
except CastleError as e:
    # Allow attempt. Data missing or invalid, or a server or timeout 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("type", "$registration")
    .put("status", "$attempted")
    .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("matching_user_id", "ca1242f498") // Optional
    .put("params", ImmutableMap.builder()
      .put("email", request.getParameter("email"))
      .build()
    .build()
  );
  
  float risk = response.json().getAsJsonObject().get("risk").getAsFloat();

  if (risk > 0.9) {
    // IMPLEMENT: Deny attempt
  };
  
} catch (CastleApiInvalidRequestTokenException requestTokenException) {
    // IMPLEMENT: Deny attempt. Likely a bad actor
} catch (CastleRuntimeException runtimeException) {
    // Data missing or invalid. Needs to be fixed
}
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({
    type: '$registration',
    status: '$attempted',
    request_token: token,
    context: {
      ip: context.ip,
      headers: context.headers
    },
    matching_user_id: 'ca1242f498', // Optional
    params: {
      email: request.body["email"]
    },
  });

  if (res.risk > 0.9) {
    // IMPLEMENT: Deny attempt
  }
} catch (e) {
  if (e instanceof InvalidRequestTokenError) {
     // IMPLEMENT: Deny attempt. Likely a bad actor
  } else if (e instanceof APIError) {
     // Allow attempt. Data missing or invalid, or a server or timeout error
  }
}