Protecting the profile update

🚧

Make sure you've first integrated the client-side integration in order to generate the "request tokens" required for each call to the Risk and Filter API.

The $profile_update activity is sent to the Risk API whenever a user updates their account information on your platform, and provides additional information for detecting account related fraud after the actual registration or login. The inline response can then be used to determine when to step up verification.

The activity can be sent at different stages of the profile update, which is denoted by setting the status field:

  • $attempted – The form was posted but hasn't yet been validated by your backend. Useful when you want to use the Castle response to verify the user before the user profile update persists
  • $succeeded –  The user profile was updated
  • $failed – The user profile was not updated, likely because of validation errors

The changeset object

The optional changeset object lets you specify which attributes were changed in the profile update process.

For security reasons, the password field only accepts the changed: true value, whereas email, phone, name, and authentication_method.type accepts an object with from and to values. The format of the from and to values are not validated.

Setting from to null means that the value was previously not set, and to to null means that the field was unset, for example in the case of authentication_method.type it could mean that a 2FA method was disabled.

In addition to the standard attributes, you can add any number of custom values, and they will all appear in the dashboard, however they will not be filterable like the standard attributes.

Auto-detection of profile updates

Any events that are sent to the Risk API, i.e. not only the $profile_update event, will will be monitored for changes to the following attributes: user.email, user.phone, and user.name. This is a convenience feature for situations where it's tricky to know exactly when the field is changed. If you want to achieve the most predictable results, however, you should try tracking these changes explicitly using the $profile_update event to avoid confusion about when an attribute was actually changed.

Example

:notebook-with-decorative-cover: 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.risk(
    type: '$profile_update',
    status: '$succeeded',
    request_token: token,
    context: {
      ip: context[:ip],
      headers: context[:headers]
    },
    user: {
      id: 'ca1242f498', # Required. A unique, persistent user identifier
      email: '[email protected]' # Recommended
    },
    changeset: {
      password: { changed: true },
      email: {
        from: '[email protected]',
        to: '[email protected]'
      },
      'authentication_method.type': {
        from: null,
        to: '$authenticator'
      },
      custom_property: {
        # ...
      }
    }
  )

  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
// NOTE: See the Ruby example for a more comprehensive set of parameters

try {
  $token = $_POST['castle_request_token'];

  $res = Castle::risk([
    'type' => '$profile_update',
    'status' => '$succeeded',
    'request_token' => $token,
    'context' => [
      'ip' => Castle_RequestContext::extractIp(),
      'headers' => Castle_RequestContext::extractHeaders()
    ],
    'user' => [
      'id' => $user->id,
      'email' => $user->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
}
# 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({
        'type': '$profile_update',
        'status': '$succeeded',
        'request_token': token,
        'context': {
          'ip': context['ip'],
          'headers': context['headers']
        },
        'user': {
          'id': user.id,
          'email': user.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
// 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("type", "$profile_update")
    .put("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("id", user.getId())
      .put("email", user.getEmail())
      .build()
    )
    .put(Castle.KEY_REQUEST_TOKEN, token)
    .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
}
// 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({
    type: '$profile_update',
    status: '$succeeded',
    request_token: token,
    user: {
      id: user.id,
      email: user.email
    },
    context: {
      ip: context.ip,
      headers: context.headers
    }
  });

  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
  }
}

Taking action

The response from the API call to Risk can then be used to take different actions. Typically, for profile updates, you'd want to ask the user to either re-authenticate using their password, or preferably by stepping up the authentication to a stronger factor.