Protect the password reset endpoint

Integrating Castle to protect a password reset request workflow

Summary

Account Takeover attempts frequently abuse password reset endpoints. Use this tutorial as a guideline for integrating Castle into a password reset workflow.


Prerequisites

You will need a Castle account and an instance of the Castle SDK configured with your Castle API Secret for the applicable environment.

You can sign up for a free Castle account at Castle.io.


Steps

  1. Step 1. Fingerprinting
  2. Step 2. Intercept the password update form
  3. Step 3. Handle Failed password update attempts
  4. Step 4. Evaluate risk for password update requests
  5. Step 5. (optional) Handle step-up authentication
  6. Step 6. Password resets
  7. Step 7. Test
  8. Addendum: Notes on the Castle fingerprint client_id

Supported events

Events to evaluate with Castle (/authenticate)

  • $password_reset_request.succeeded
  • $password_reset_request.failed

Feedback-only events (/track)

  • $password_reset.succeeded
  • $password_reset.failed

Tutorial

Step 1. Fingerprinting

For optimal performance, especially when end users are permitted to use mobile devices to access your application, Castle fingerprinting must be installed and configured. However, the implementation of Castle fingerprinting is not a technical blocker to continue with the next steps.

If you plan to return to install Castle fingerprinting at a later time, you may skip to step 3.

In order to get the best performance from your Castle integration, Castle’s c.js and/or mobile SDK fingerprinting should be in place before going live in a production environment

Please refer to our fingerprinting tutorials for further instructions.

Step 2. Intercept the password update form

As mentioned in Step 1, if you have not yet implemented Castle fingerprinting, skip to step 3 and return to complete this step once Castle fingerprinting has been implemented.

Mobile apps

After the “Update Password” request object has been initialized in your mobile app code, but prior to sending the request, call Castle.clientId(). Append a header to the request, using the configuration.CastleClientIdHeaderName as the header name and the Castle.clientId() result as the header value. See the fingerprinting tutorial for more information and code samples.

Web apps

Castle’s c.js script will set a Cookie named __cid. Please ensure that this Cookie is visible in the request details that reach your application server. Castle’s server-side SDK’s will look for this Cookie in request objects when you form requests to the Castle API.

If the __cid Cookie is unavailable due to domain constraints or other reasons, you can access the value by using the _castle('getClientId') method. This method is exposed on the global window object by c.js. You must pass the result of this method call to the application server so that it can be handled by the Castle server-side SDK. See below for an example, where we use a hidden form attribute to send the value.

var form = document.getElementById('password-update-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()
});

If your mobile or web app required custom work here, be sure to read the addendum on handling client_id at the bottom of this page.

Step 3. Handle failed password update attempts

To complete this step:

  1. Check that the password update attempt failed (due to wrong current password, or a different authentication failure)
  2. Assign additional properties to the Castle API request context
  3. Send the $password_reset_request.failed event to Castle’s /track endpoint

Assign the submitted email or username to properties.email or properties.username. Additionally, if the user account exists, it is helpful to train Castle’s models for that user by assigning the user_id.

Since no verdict is returned from the /track endpoint, the steps above can be performed in parallel to other application logic (i.e. using async methods).

Example with Castle Java SDK:

ImmutableMap traits = ImmutableMap.builder()
  .put("email", "admin@cstl.io")
  .put("registered_at", "2015-02-23T22:28:55.387Z")
  .build();

castle.track(
  "$password_reset_request.failed",
  "e325bcdd10ac", // the user_id
  null,
  null,
  traits
);

Step 4. Evaluate risk for password update requests

To complete this step:

  1. Check that the password update attempt submitted valid credentials, but hold off on processing the update until step 4
  2. Assign additional properties to the Castle API request context
  3. Send the $password_reset_request.succeeded event to Castle’s /authenticate endpoint
  4. Handle the Castle verdict:
  • allow: process the update
  • challenge: step-up authentication
  • deny: return a “failed password update” experience (i.e. a 403)

Assign the submitted email or username to user_traits.email or user_traits.username.

Add any additional context information to the Castle API payload. The user identifier submitted on the client side (email, username, phone, etc) is a required field and is used by Castle to do the trust assessment. Assign the identifier(s) to properties.email, properties.username, properties.phone, etc.

Example with Castle Java SDK:

ImmutableMap traits = ImmutableMap.builder()
  .put("email", "johan@castle.io")
  .put("registered_at", "2015-02-23T22:28:55.387Z")
  .build());

// Put this after the password update request is received
Verdict verdict = castle.authenticate(
  "$password_reset_request.succeeded",
  "e325bcdd10ac", // the user_id
  null,
  null,
  traits
);

// You can ignore the returned action during evaluation
switch (verdict.getAction()) {
  case ALLOW: {
    // ...
  }
  break;
  case CHALLENGE: {
    // ...
  }
  break;
  case DENY: {
    // ...
  }
  break;
}

The deny action is determined by the policies that you have configured in your Policy Settings. You MUST create a Policy to evaluate the $password_reset_request.succeeded event. Castle does not come enabled with a default policy for this event. See the Tutorial: Using Policies for more details.

A screenshot of some default Castle Policies, taken from the Castle Dashboard
Castle has a few Default Policies

Step 5. (optional) Handle step-up authentication

To complete this step:

  1. Challenge users via at least one form of step-up authentication when Castle returns challenge
  2. Send the $challenge.requested event to Castle
  3. Send the $challenge.failed event to Castle

When the response from Castle’s /authenticate endpoint recommends to challenge an event, you should initiate step-up authentication. Castle does not integrate directly with MFA providers. Common methods of step-up authentication include knowledge-based authentication, 2FA via SMS or email, or use of an Authenticator app.

Adding these events into the workflow will provide visibility into the events surrounding a user account password reset. Your team members will be able to determine, via the Castle Dashboard, whether a user was challenged when attempting to change their account password.

Example with Castle Java SDK:

ImmutableMap traits = ImmutableMap.builder()
  .put("email", "admin@cstl.io")
  .build();

castle.track(
  "$challenge.requested",
  "e325bcdd10ac", // the user_id
  null,
  null,
  traits
);

Step 6. Password resets

When you tell Castle about password resets on user accounts, Castle marks active threats as “Mitigated” and Castle also approves the device that is performing the password reset.

To complete this step:

  1. When users successfully complete a password reset, send the $password_reset.succeeded event to Castle’s /track endpoint

Castle recommends that you enforce strong password policies and that you do not allow users to re-use their old passwords. If a user account was compromised in a credential stuffing attack, their password has been compromised forever.

Example with Castle Java SDK:

ImmutableMap traits = ImmutableMap.builder()
  .put("email", "admin@cstl.io")
  .build();

castle.track(
  "$password_reset.succeeded",
  "e325bcdd10ac", // the user_id
  null,
  null,
  traits
);

Step 7. Test

The Events Debugger will come in handy when inspecting Castle API calls to ensure you got all the details right.

Make sure the client device context details, such as IP and User-Agent, are correct. Do not send the IP and User-Agent of your Load Balancer

Check that the user happy-path is unaffected

The first thing you should do is check that you can update a password for an existing user account. Then, using a different device and routing through an IP Proxy service, you will likely be able to force a challenge verdict.

Imitate a new device

You can send a test $password_reset_request.succeeded event using the following shell commands:

  1. Assign your Castle environment API Secret to a variable (below we use SECRET). The API Secret can be found in your Castle dashboard settings.
export SECRET=abc123...
  1. Send an event with a specific user_id, a known User-Agent and a known ip. This establishes a baseline for that user.
  2. Send an event with the same user_id, but a random User-Agent (iPhone is used below) and a random IP address (1.1.1.1 is used below). If the device and location are not within normal variance for that user, Castle will issue a challenge verdict. You can view the details of the user’s history/activity in the Castle dashboard at https://dashboard.castle.io/users (search by user_id).
curl https://api.castle.io/v1/authenticate \
-X POST \
-u ":$SECRET" \
-H "Content-Type: application/json" \
-d '
{
  "event": "$password_reset_request.succeeded",
  "user_id": "e325bcdd10ac",
  "user_traits": {
    "email": "johan@castle.io"
  },
  "context": {
    "client_id": false,
    "ip": "1.1.1.1",
    "headers": {
      "User-Agent": "iPhone",
      "Accept": "text/html",
      "Accept-Language": "en-us,en;q=0.5",
      "Accept-Encoding": "gzip, deflate, br",
      "Connection": "Keep-Alive",
      "Content-Length": "130",
      "Content-Type": "application/javascript",
      "Origin": "https://castle.io/",
      "Referer": "https://castle.io/account/settings"
    }
  }
}'

Notes on the Castle fingerprint client_id

If the __cid Cookie (web app requests) or X-Castle-Client-Id header (mobile app requests) is available on the server-side: The Castle server-side SDK should automatically extract this value and map it to context.client_id when you pass requests objects to the Castle SDK methods in order to initialize requests to the Castle API. If you see context.client_id appear in the request bodies in your Castle dashboard debugger - then you’re all set!

If the __cid Cookie or X-Castle-Client-Id header is not available on the server-side: In your server-side password-change handler, extract the clientId value. In the examples from step 2, the clientId will be in a custom header (Castle mobile SDK’s default to X-Castle-Client-Id) or in a form field (castle_client_id).

Ultimately, we want the Castle fingerprint client_id value to be set as context.client_id in the request body to the Castle /authenticate or /track endpoint`.

Example with Castle Java SDK:

Castle castle = Castle.initialize("abcxyz");

try {
    // IMPLEMENT: Get the Client ID from the form submission, if necessary
    String clientId = params.get('castle_client_id');
    // continue with building request...