Protecting login

How to send $login activity to Castle and use the response to prevent bad actors from taking over legitimate user accounts.

The login activity is sent to Castle whenever a user authenticates on your platform, and provides important information for detecting login related fraud, such as credential stuffing attacks or account takeovers. The inline response from the Risk endpoint can be used at any point in your application to determine when to step up verification.

How to add the $login activity:

  1. The user submits a form with credentials, e.g. email and password
  2. The email and password are checked against the user database.
  3. If the credentials are correct, send $login with $succeeded status.
  4. If the credentials were incorrect, both in the case where the email doesn't exist and/or the password was incorrect, send $login with $succeeded status.
Where in the login flow to send the different statusesWhere in the login flow to send the different statuses

Where in the login flow to send the different statuses

Sending successful login activity

Use the Risk API to send information about the user at the time of session creation.

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: 'ca1242f498', # Required. A unique, persistent user identifier
      email: '[email protected]', # Required
      registered_at: '2012-12-02T00:30:08.276Z', # Recommended
      phone: '+1415232183', # E.164 format
      name: 'Mike Gray',
      traits: { # Custom user data for visualization purposes
        nationality: 'US',
        birth_date: '1976-02-02'
      }
    },
    authentication_method: { # Optional. See link below
      type: '$password' # The most common type
    },    
    properties: { # Custom event data for visualization purposes
      solved_captcha: true,
      attempts: 3
    }
  )

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

rescue Castle::Error => e
  # Handle error
end
// NOTE: See the Ruby example for a more comprehensive set of parameters

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
    ],
    'authentication_method' => [
       'type' => '$password'
    ]
  ]);

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

} catch (Castle_Error $e) {
  // Handle error
}
# NOTE: See the Ruby example for a more comprehensive set of parameters

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
        },
        'authentication_method': {
            'type': '$password'
        }
    })

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

except CastleError as e:
     # Handle error
// NOTE: See the Ruby example for a more comprehensive set of parameters

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)
    .put("authentication_method", ImmutableMap.builder()
      .put("type", "$password")
    )
    .build()
  );
} catch (CastleRuntimeException runtimeException) {
    // Handle errors
}

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

if (risk > 0.9) {
  // IMPLEMENT: Deny attempt
};
// NOTE: See the Ruby example for a more comprehensive set of parameters

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
    },
    authentication_method: {
      type: '$password'
    }
  });

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

:notebook-with-decorative-cover: See the documentation for authentication_method to see all the available options, such as how to specify authentication with SMS or biometrics.

📘

You may have noted that the registered_at property is the only user property not present in the Registration example. It's because it's automatically set to the current time for $registration events, but since the user might have registered before Castle was integrated, you should set this property on every successful login event.

Taking action

The response from the API call to Risk can then be used to take different actions, typically:

  • Upon deny, block the request and redirect the user back to the login form, with a message that the credentials were incorrect
  • Upon challenge, prompt the user for additional verification, e.g. via email or 2FA, if applicable.

See the section on Automating Account Recovery to learn more.

Sending failed login activity

Sending failed login attempts to the Filter API is highly recommended, as it provides valuable information in detecting malicious traffic and devices. To learn more about how Castle utilizes activities with failed status, see this page.

The Filter API is used for sending anonymous user activity, such as a failed login attempt. The event will appear on the timeline of any existing user with the same email as the user.email, but it will not create users nor devices, nor contribute to the risk score of any existing users. Note that user.id is not supported for Filter events.

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

castle.filter(
  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::filter([
  '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.filter({
  '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.filter(
  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.filter({
  event: '$login',
  status: '$failed',
  request_token: token,
  user: {
    email: request.body["email"]
  },
  context: {
    ip: context.ip,
    headers: context.headers
  }
});

Please see for more details.


Did this page help you?