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
changeset
objectThe 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
📔 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.
Updated almost 2 years ago