Protect the login endpoint

Integrating Castle with an end-user login process

Prerequisites

You will need a Castle account and an application configured with your Castle API Secret for the applicable environment. Castle also offers many SDKs to streamline the integration process for most languages and frameworks - see the links to Github in our menu for our offerings.

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


Supported events

  • $login.attempted
  • $login.succeeded
  • $login.failed

Steps

  1. Step 1. Fingerprinting
  2. Step 2. Intercept the login form
  3. Step 3. Handle login attempts on the server side
  4. Step 4. (optional) Handle successful logins
  5. Step 5. Handle step-up authentication
  6. Step 6. Test

Summary

Integrating Castle at the login is the most popular way to get started. Castle can provide risk analysis at the time of authentication. Castle’s verdicts and risk scoring can result in blocking malicious login attempts, while Castle’s webhooks can be used to initiate account recovery workflows.


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 Castle.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 login 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 login request object has been initialized in your mobile app code, but prior to sending the request, call Castle.clientId(). Append a header to the login 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 Castle.js script generates a string that should be assigned to the context.client_id property of requests to the Castle API. You can access the value by using the _castle('getClientId') method in the browser environment. This method is exposed on the global window object by Castle.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('login-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 the fingerprint at the bottom of this page.

Step 3. Handle login attempts on the server side

To complete this step:

  1. When a login request arrives to your application, prepare the Castle API call
  2. Assign additional properties to the Castle API request context (including context.client_id)
  3. Send the $login.attempted events to Castle’s /authenticate endpoint

Login attempts require that you Assign the submitted email in properties.email.

curl -s https://api.castle.io/v1/authenticate \
  -X POST \
  -u ":$SECRET" \
  -H "Content-Type: application/json" \
  -d '
  {
    "event": "$login.attempted",
    "properties": {
      "email": "{user_email}"
    },
    "context": {
      "client_id": "{castle_client_id}",
      "ip": "{ip}",
      "headers": {
        "User-Agent": "{User-Agent}"
      }
    }
  }' | json_pp

When Castle returns action: deny or action: challenge - handle these appropriately. A challenge should include a step-up authentication, while a deny verdict should be used to block the login attempt. The in-app implementations of these responses is up to you. If the user_id was provided, Castle will also send webhooks about these events to your webhooks endpoint. The “Threats” that Castle detects are available for viewing in the Castle Dashboard.

Step 4. (optional) Handle successful logins

Informing Castle about $login.succeeded events will allow us to train our models to apply risk scoring to users on an individual basis. A call to Castle with event: $login.succeeded means that the submitted email (or username) and password were valid. The Castle API call should be made after this validation, but before a session initiation.

To complete this step:

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

Assign the user_id and user_traits.email to the Castle API request body.

curl -s https://api.castle.io/v1/authenticate \
  -X POST \
  -u ":$SECRET" \
  -H "Content-Type: application/json" \
  -d '
  {
    "event": "$login.succeeded",
    "user_id": "{user_id}",
    "user_traits": {
      "email": "{user_email}"
    },
    "context": {
      "client_id": "{castle_client_id}",
      "ip": "{ip}",
      "headers": {
        "User-Agent": "{User-Agent}"
      }
    }
  }' | json_pp

The deny or challenge action is determined by the policies that you have configured in your Policy Settings. All Castle accounts are created with default policies, including one that will issue a deny for high risk scores and challenge for medium risk scores seen in $login events.

A screenshot of the Castle Dashboard Policy Settings page
The default $login.succeeded policies

Step 5. Handle step-up authentication

This step is optional, but most of our customers eventually use Castle’s verdicts to initiate step-up authentication for user accounts. If you already have one or more step-up authentication mechanisms in place, we recommend sending event: $challenge.succeeded when users complete step-up authentication challenges. This helps to train Castle’s models, and it will allow Castle to remember that the device in question has already succeeded in a step-up authentication challenge for that user.

To complete this step:

  1. Challenge users via at least one form of step-up authentication when Castle returns challenge
  2. On successful challenge completion, send event: $challenge.succeeded to Castle’s /track endpoint

When the response from Castle’s /authenticate endpoint recommends to challenge a login, 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.

Castle believes any form of MFA is better than no MFA at all. However, we have also seen elaborate social-engineering and phishing attacks that have allowed bad actors to get past MFA implementations. You should take care to design an MFA solution that fits your needs.

The event: $challenge.succeeded request will mark a user’s device as “approved”. This means that Castle’s default policies will no longer issue the challenge verdict to that user when they log in with that device. This default behavior can be modified according to your needs.

Step 6. 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 log in, unimpeded, to an existing user account.

Imitate a new device

You can send a test event: $login.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 (123, in the below example), a known headers.user_agent (iPhone in the example below) and a known ip (1.1.1.1 in the example below). This establishes a baseline for that user. Note that while the only header in the example is User-Agent, you should send the full set of headers that are available from the client request.
  2. Send an additional event with the same user_id, but with a few contextual details modified. Change the User-Agent (from iPhone to something else) and change the IP address (from 1.1.1.1 to something else). 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/123 (use user_id in the url path, or search for it in the Users page).
echo "\nReceived correct password for: castle-docs-user (castle-docs-user@example.net)\nSending request to Castle's /authenticate endpoint:\n\"event\": \"\$login\", \"status\": \"\$succeeded\"\n"
curl -s https://api.castle.io/v1/authenticate \
  -X POST \
  -u ":$SECRET" \
  -H "Content-Type: application/json" \
  -d '
  {
    "event": "$login.succeeded",
    "user_id": "123",
    "user_traits": {
      "email": "castle-docs-user@example.net"
    },
    "context": {
      "client_id": false,
      "ip": "1.1.1.1",
      "headers": {
        "User-Agent": "iPhone"
      }
    }
  }' | json_pp

Note that command-line testing with a real context.client_id is not possible, since a valid fingerprint value (client_id) can only be generated in a browser or native mobile environment.