Account takeover (ATO) is when a fraudster gains control of an account that belongs to a genuine customer. Fraudsters can then make unauthorised transactions, sell the compromised accounts on and/or scrape personal information out of the account which can be sold.
Phishing, spyware and malware can all be used to commit more sophisticated ATO attacks. However, credential stuffing is the most common tactic used to launch ATO attacks.
Credential stuffing uses stolen username and password combinations to automate login requests in order to gain access to user accounts. Estimates on the average success rate of credential stuffing attacks range from 0.1-2% with a peak of 8% quoted by some sources.
Following a breach, stolen credentials are purchased on the darkweb and shared via cracking forums. Data breaches are therefore a major contributing factor to increasing levels of ATO attacks. Combined with high levels of password reuse, this is especially concerning.
Credential stuffing can be scripted by more skilled fraudsters. However, automated tools like Sentry MBA make credential stuffing very easy for anyone to commit. This means there is a range of actors involved in committing ATO; from sophisticated hackers through to teenagers looking to order a ‘free’ pizza.
ATO attacks have significant consequences for merchants. It’s not just the cost of replacing goods or refunding payments - it’s also the time spent by support teams dealing with angry customers and other teams tackling the legal and operational fallout. In addition, there is often significant reputational damage incurred.
Taking into account the nature of ATO, Ravelin have introduced a number of different ways to combat the issue.
Credential stuffing relies on a list of username and password combinations. In order to mitigate against this, we maintain a breached credentials database. Calls can be made to the database either via the v3/login
event at login or via /v2/lookup/credentials/check
during registration or password change to verify if we’ve seen the credentials in the wild. Though we cannot guarantee that every breached credential will be in our database, this can go a long way to preventing ATO. You can also search for and view users that have logged in with credentials that appear in our database - providing additional context during investigations.
We have added customisable rate limits at login around device, username and IP. Thresholds can be set for this depending on your specific operational requirements. Rate limits are useful for tackling high volume attacks.
In addition, we check for things we know can indicate that a user is a fraudster like proxies and TOR.
We can set ATO specific rules around device, username, IP and velocity. For example, if the same device tries to access X accounts within a set time frame. Rules are especially useful for tackling ‘low and slow’ volume attacks.
By leveraging data collected via v3/login
event, we provide oversight of login activity within our dashboard. Login activity reporting is available to explore via our Analytics tool, making it easier to spot anomalies in ‘normal’ login activity across your customer base. In addition, you can interact with login data via a filterable login list and drill down to view login information for a specific customer on their customer profile.
Account changes can be used as an important signal for ATO. For example, if a new device or location is detected at login or if there is a change made to account details like email or delivery address. We can also send you information on changes to account details so you can verify the change was legitimately made by the customer.
The point of login is a critical step in detecting account takeover. We want to prevent the malicious actor from being able to log in just like we want to prevent the fraudster from completing an order.
This is the endpoint used to tell Ravelin about every login attempt, both successful and unsuccessful. Unsuccessful logins can be important signals for an ATO attack. It will provide an ATO recommendation response if asked for via the query parameter score=true. Otherwise it will simply record the data without further processing.
Name | Type | Description |
---|---|---|
timestamp
required
|
integer |
Unix timestamp with milliseconds (nanoseconds also accepted)
|
login
required
|
object | |
device
|
object | Not required but highly recommended. Defined below |
location
|
object |
{
"timestamp": 1512828988826,
"login": {
"customerId": "abc-123-ZYZ",
"loginId": "abc-123-ZYZ",
"success": false,
"username": "example_username",
"authenticationMechanism": {
"password": {
"passwordHashed": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
"success": false,
"failureReason": "BAD_PASSWORD"
},
"social": {
"success": false,
"failureReason": "SOCIAL_FAILURE",
"socialProvider": "google"
},
"oneTimeCode": {
"success": false,
"failureReason": "INVALID_CODE"
},
"u2f": {
"failureReason": "INVALID_KEY",
"success": false
},
"rsaKey": {
"success": false,
"failureReason": "INVALID_KEY"
},
"smsCode": {
"phoneNumber": "+16045555555",
"success": false,
"failureReason": "INVALID_CODE"
},
"magiclink": {
"transport": "email",
"email": "jsmith123@example.com",
"success": false,
"failureReason": "INVALID_LINK"
},
"recaptcha": {
"success": false,
"failureReason": "FAILED_TEST"
}
},
"app": {
"name": "Our App Lite",
"platform": "web",
"domain": "us.brand.com"
},
"custom": {
"additionalProp1": {},
"additionalProp2": {},
"additionalProp3": {}
}
},
"device": {
"deviceId": "string",
"ipAddress": "81.152.92.84",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"model": "Pixel XL",
"os": "android",
"type": "phone",
"location": {
"country": "GBR",
"postalCode": "E1 1AA",
"latitude": 51.503252,
"longitude": -0.127899,
"addresseeName": "John Smith",
"street1": "123 fake st.",
"street2": "floor 4, flat 48",
"neighbourhood": "Hackney",
"zone": "1",
"city": "London",
"region": "California",
"poBoxNumber": "1234"
}
},
"location": {
"country": "GBR",
"postalCode": "E1 1AA",
"latitude": 51.503252,
"longitude": -0.127899,
"addresseeName": "John Smith",
"street1": "123 fake st.",
"street2": "floor 4, flat 48",
"neighbourhood": "Hackney",
"zone": "1",
"city": "London",
"region": "California",
"poBoxNumber": "1234"
}
}
Above is the full login request object. This is similar to our /v2/login
endpoint but with additional information regarding the authentication attempt. This request is expected to be sent after all successful and unsuccessful logins from your backend system.
Name | Type | Description | |||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
username
required
|
string |
The username of the user. This is the unique string that ties this particular user to the authentication mechanism used. Typically this is the email address. |
|||||||||||||||||||||||||||
customerId
required
|
string |
customerId is the unique identifier for a user. This identifier does not change even if the username is changed. This is optional for login events as you will see failed logins against usernames that do not exist in your system and therefore do not have a customerId. Please send this on login attempts where a customerId is available. If you are using our fraud solution, this should be the same customerId as used there. |
|||||||||||||||||||||||||||
success
required
|
boolean |
If the login attempt was successful and access was granted |
|||||||||||||||||||||||||||
authenticationMechanism
required
|
object |
At least one authentication mechanism must be provided. More than one may be provided. For example a password and a oneTimeCode may be used to authenticate. Hide definition |
|||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
app
important
|
object |
The mobile or web app that this login was done from. |
|||||||||||||||||||||||||||
While Device is not a required field on the login event, it is highly recommended as it is a vital signal to account breaches.
The most important Device fields are deviceId
and ipAddress
.
deviceId
can be generated using our JavaScript library.
Details about that can be found in the device fingerprinting section.
Any other fields can be very useful but are not strictly necessary.
For more details about device tracking please see our device API
and the Guide.
Location is optional as we are aware you will not typically have a location at login time. If you do have location as part of your flow this can be very useful to localise ongoing or repeat attackers. Please see here for details about the location object: Location API
You can see an example of a successful response from the /v3/login endpoint below. This response is identical to other responses given by the Ravelin platform for payment fraud endpoints. Note that we do provide payment fraud scores at login time as well. The Payment fraud and ATO responses are both included. Ensure you use the correct action when dealing with ATO vs. Payment Fraud.
Name | Type | Description | ||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
status
|
integer |
HTTP status code of the response |
||||||||||||||||||||||||||||||||||||
success
|
string |
Indicates if the request was successful or not |
||||||||||||||||||||||||||||||||||||
timestamp
|
string |
RFC3339Nano encoded timestamp for the response |
||||||||||||||||||||||||||||||||||||
credentialStatus
|
object |
The status of the username and password Hide definition |
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
data
|
object |
The Fraud and Account Takeover results. This is identical to fraud endpoint responses Hide definition |
||||||||||||||||||||||||||||||||||||
|
Example response:
{
"checks": {
"additionalProp1": {
"action": "string"
},
"additionalProp2": {
"action": "string"
},
"additionalProp3": {
"action": "string"
}
},
"credentialStatus": {
"passwordBreached": true,
"usernameBreached": true
},
"data": {
"action": "string",
"ato": {
"action": "string",
"rules": {
"passiveAction": "string",
"triggered": [
{
"action": "string",
"description": "string",
"state": "string",
"triggered": true
}
]
}
},
"cachedScore": true,
"comment": "string",
"connect": {
"fraudulent": true
},
"customerId": "string",
"effectiveTime": "string",
"lookup": {
"lookupAction": "string",
"lookupResult": {
"email": {
"address": "string",
"hasChargebacks": true,
"reviewedAsFraudster": true
},
"hasChargebacks": true,
"industry": "string",
"ipAddress": {
"address": "string",
"fromTime": 0,
"hasChargebacks": true,
"reviewedAsFraudster": true,
"toTime": 0
},
"reviewedAsFraudster": true,
"telephone": {
"hasChargebacks": true,
"number": "string",
"reviewedAsFraudster": true
}
}
},
"market": {
"city": "string",
"country": "string",
"region": "string"
},
"rules": {
"passiveAction": "string",
"triggered": [
{
"action": "string",
"description": "string",
"state": "string",
"triggered": true
}
]
},
"score": 0,
"scoreId": "string",
"source": "string",
"thresholds": {
"prevent": 0,
"review": 0
},
"warnings": [
{
"class": "string",
"help": "string",
"msg": "string",
"state": "string"
}
]
},
"message": "string",
"status": 0,
"success": "string",
"timestamp": "string",
"traceId": "string",
"warnings": [
{
"docs": "string",
"id": 0,
"message": "string"
}
]
}
You can see an example of a failed response from the /v3/login endpoint below. A failure response will indicate why the failure happened and will try to direct you to relevant documentation.
{
"docs": "string",
"errors": [
{
"Docs": "string",
"Error": "string",
"Path": "string"
}
],
"message": "string",
"retryable": true,
"status": 0,
"success": "string",
"timestamp": "string",
"traceId": "string"
}
Using the information supplied to Ravelin, logins and other events are categorised in one of three buckets: PERMIT
WARN
BLOCK
This information is sent in the response body to scored POST requests (with ?score=true
) as defined above.
Events sent to Ravelin using POST requests get an empty response body by default. The information is stored in our system and no further action is taken during the request.
To force an immediate evaluation based on the information received up to and including an event add the ?score=true
query parameter.
This will do two things: 1. Process the event payload 2. Force immediate recalculation of the action and return it
The score will be calculated based on the information in this event, and in all events previously received and met with a 200 OK
status.
The ?score=true
query parameter is available on every event endpoint. This includes both ATO and Payment Fraud related endpoints. Specify it when you want to make a decision regarding a login attempt.
The most important field in the payload is action under the ATO object:
On this action | In your system |
---|---|
PERMIT |
Allow this login or account detail change to proceed - no action is required |
WARN |
We advise that you allow the login but take extra validation steps for this login or account detail change. Additional validation could include enforcing 2FA if you have a verified phone number, notifying the user that there has been suspicious account activity and asking them to confirm if the activity was legitimate or not or prompting the user to complete a recaptcha |
BLOCK |
We advise that you block this login or account change and show a generic error message which does not explain why the user was prevented from login. This is important as we do not want to make it obvious to the fraudster that we have detected the attempt. If we return a |
Other fields are provided for analytic and debugging purposes.
Ravelin maintains an up to date list of breached credentials that can be found in the public domain. At login, you can view whether the login attempt was using breached credentials by looking at the credentialStatus object within the /v3/login
response.
You can use the database to check if a user is attempting to register a new account, or update an existing one, with credentials that have been seen in the wild via our v2/lookup/credentials/check
endpoint. The below request is expected to be sent at password creation or update. The response will tell you if the username and/or password is found in our breached credential database.
Passwords are extremely sensitive, so please do not send us plaintext passwords. Instead, please hash the password with SHA256 and HEX encode it before sending it. We do not store the hashed password, and discard it immediately after processing your request.
If you are still concerned about sending us a hash of the password please contact us for alternative options.
{
"username": "string",
"passwordHash": "string"
}
{
"usernameBreached": true,
"passwordBreached": true
}
There are three distinct flows where we check if a users credentials have been breached. For each flow, you may want to take a different action. Below we have outlined what we suggest for each flow.
While your backend is processing a login request, you can call our v3/login
endpoint. Our response will indicate within the credential status object whether or not the username and password appear in Ravelin’s breached credential database.
Credential Status | Action |
---|---|
|
If the response is PERMIT and the credential status is FALSE for both username and password, we suggest you allow the login - no action is required.
|
|
If the credential status is only true for username, we suggest you take no action. |
|
If the credential status is true for both username and password, we suggest you take at least one of the following actions taking into account your risk appetite:
|
At registration, you can call our v2/lookup/credentials/check
endpoint. We will return the responses outlined below.
Credential Status | Action |
---|---|
|
No action is required if we return false. |
|
If the credential status is only true for username, we suggest you take no action. |
|
If a user is trying to register using credentials that appear in the breached database, we advise that you do NOT allow the user to set that password at registration. |
During an account update (e.g. forgot password, password change), you can call our v2/lookup/credentials/check
endpoint. We will return the responses outlined below.
Credential Status | Action |
---|---|
|
No action is required if we return false. |
|
If the credential status is only true for username, we suggest you take no action. |
|
If a user is trying to update their account using credentials that appear in the breached database we advise that you do NOT allow the user to use the password at time of update. |
When a customer makes changes to their account those details should be sent to Ravelin. This can be done via the core API. We will take these updates as signals to aid in account takeover detection. Further we are able to return these detected changes as well as the previous value to you in the response of the request. This includes a URL that can be used to verify that the customer was indeed the one to make those changes. This is typically done by sending an email to the customer asking if they recognise the new details.
The response object will be inside the typical scoring response. As an example, this is the response received when a customer changes their email address on a new device:
{
...
"customerChanges": [
{
"changeId": "abc-123-XYZ",
"customerId": "customer1",
"changeType": "EMAIL",
"newValue": {
"email":{
"email": "newEmail@example.com",
"timestamp": "2019-02-11T15:23:05.468789646Z"
}
},
"previousValue": {
"email":{
"email": "oldEmail@example.com",
"timestamp": "2018-10-11T17:12:06.584698542Z"
}
},
"timestamp": "2019-02-11T15:23:06.741447896Z",
"verificationURL": "https://api.ravelin.com/v2/change/verify?id=Y2xpZW50SWTCp2N1c3RvbWVySWTCp2NoYW5nZUlk"
},
{
"changeId": "XYZ-abc-123",
"customerId": "customer1",
"changeType": "DEVICE",
"newValue": {
"device":{
"deviceId": "new-device-id",
"deviceType": "phone",
"deviceManufacturer": "google",
"deviceModel": "Pixel XL",
"deviceOS": "android",
"ipAddress": "10.11.12.13",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"timestamp": "2019-02-11T15:23:05.468789646Z"
}
},
"previousValue": {
"device":{
"deviceId": "old-device-id",
"deviceType": "phone",
"deviceManufacturer": "apple",
"deviceModel": "iPhone X",
"deviceOS": "iOS",
"ipAddress": "10.11.12.13",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"timestamp": "2018-11-03T17:43:05.468789646Z"
}
},
"timestamp": "2019-02-11T15:23:06.741447896Z",
"verificationURL": "https://api.ravelin.com/v2/change/verify?id=X48IkdxpZW50SWTCp2N1c3RvbWVySWTCp2NoYW5nZUlk"
}
]
...
}
Name | Type | Description | ||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
customerChanges
|
array |
An array containing all the changes detected as a result of this request Hide definition |
||||||||||||||||||||||||
|
If you chose to verify that the changes were legitimate by requesting confirmation from the user, it is important to use the URL we provide as we can use this data to improve how we detect account takeover.
We recommend that you email the previously listed email for confirmation if the email address has been updated as part of the account changes.
The verification URL is given in the change response. Making a GET
request on this URL will inform the ravelin system of updates to the verification status. There are a number of query parameters that may be used. They are as follows:
Query Parameter | Description |
---|---|
verified |
This is a required field. This can be set to true or false . It indicates if the change is verified to be made by the customer (true ) or if the change is not made by the customer (false )
|
r | r is an optional parameter that will tell Ravelin to return a redirect to the client. The expectation here is that you may construct a simple, static page on your domain thanking the customer for verifying a change. You can put the Ravelin Verification URL directly in an email and when the customer clicks it they will be directed to this page. In this way it will keep branding and domains consistent for the customer experience. |
id | This parameter is prepopulated, it contains an ID that allows Ravelin to tie this verification to the particular change. This must be included and should not be changed |
all |
all is an optional parameter that will verify all the changes from a particular response. This can be set to true or false . For example, if a customer is updating their email from a new device on a different IP we could have 3 changes in one event: DEVICE , IP_LOCATION and EMAIL . By adding all=true against any one of these verificationURLs, the endpoint will verify all 3.
|
These query parameters may be added to the end of the URL. This is what a complete request may look like:
GET https://api.ravelin.com/v2/change/verify?id=Y2xpZW50SWTCp2N1c3RvbWVySWTCp2NoYW5nZUlk
&verified=true
&all=true
&r=https://www.ravelin.com
The highlighted parameters are examples of what would be added by you. When the r
query parameter is set, the response will contain a 303
status with the contents of the r
query parameter in the Location
header. This will redirect the browser to that URL. This allows you to host a static result page on your domain thanking the user for this verification while still only needing to put this link into an email.
The verification process requires more complexity than most aspects of the Ravelin integration. This includes emailing customers and, optionally, providing an API within your backend in which the customers are able to verify changes. We recommend contacting Ravelin to discuss your options.
To get the most out of change verification, you need to know the result of the verification within your own backend. This allows you to take immediate further action as well as reassuring customers as the link in the email will be within your domain. To do this we recommend you set up an endpoint in your system which accepts the verification request from the email. You can then use the VerificationURL we provide to forward that response to us.
If the customer clicks on the link indicating they don’t recognise a change, we recommend you reset the customers password, locking them (and any intruder) out of the account. At the same time, send them a password reset email so they can recover the account. We suggest you verify if the email associated with the customer account has been changed recently. If the email has been changed, we advise you send any password reset information to the previously used email. Then the customer can be redirected to a page informing them of what has happened and the next steps that they should take to recover the account
If the verification comes back as a true change, no further action against the customer account is required. You can forward them to a page thanking them for the feedback.
In both cases the result of the verification must be forwarded to us.
If your team can not prioritise the construction of a backend API, you can embed the links we provide directly into the email. This will allow us to know about verification status without the need for a backend api on your part.
That said, you will have to construct a landing page for both the yes and no verification requests that Ravelin can redirect the user to. These can be simple static pages that must be hosted on your domain.
You should add a redirect on to the links for both verifying yes and no with the appropriate page.
The page indicating an unrecognised change should include information on resetting their password and a suggestion to contact customer support immediately. When the customer clicks this link we will use this information to update the customers account which will greatly increase the likelihood any login attempt on this account will be blocked by the Ravelin system if we receive no indication that the account has been reclaimed.
The page indicating a change was recognised should simply thank them for their feedback.
The change values in the response define what change is actually being made and what the previous value was before the change. They are defined as follows:
{
"device":{
"deviceId": "string",
"deviceType": "string",
"deviceManufacturer": "string",
"deviceModel": "string",
"deviceOS": "string",
"ipAddress": "string",
"userAgent": "string",
"timestamp": "string"
},
"ipAddress":{
"ipAddress": "string",
"timestamp": "string",
"city": "string",
"countryISO": "string",
"continentISO": "string",
"isp": "string",
"ipAddrId": "string"
},
"email":{
"email": "string",
"timestamp": "string"
},
"password":{
"timestamp": "string"
},
"telephone":{
"telephone": "string",
"telephoneCountry": "string",
"timestamp": "string"
},
"deliveryAddress":{
"streetAddress1": "string",
"streetAddress2": "string",
"addressLocality": "string",
"addressRegion": "string",
"addressCountry": "string",
"addressCountryISO": "string",
"postOfficeBoxNumber": "string",
"postalCode": "string",
"latitude": 0.0,
"longitude": 0.0,
"timestamp": "string",
"locationId": "string"
},
"billingAddress":{
"streetAddress1": "string",
"streetAddress2": "string",
"addressLocality": "string",
"addressRegion": "string",
"addressCountry": "string",
"addressCountryISO": "string",
"postOfficeBoxNumber": "string",
"postalCode": "string",
"latitude": 0.0,
"longitude": 0.0,
"timestamp": "string",
"locationId": "string"
}
}
In cases where an account is reclaimed by the legitimate owner you need to notify us that the account has been secured. This will prevent the account from being blocked due to activity that happened while the account was compromised. Accounts should be reclaimed if a customer login has been blocked by Ravelin because of an ATO attempt. A reclaim should also be sent if a customer experiences an ATO but the account has since been secured.
We strongly recommend notifying us about account reclaims only when you are sure that the 3rd party has lost access to the account. A reclamation on a customer account is taken into consideration during the decision making process when detecting account takeover. This means that at least for a short time, new logins are likely going to be permitted for that customer even in cases that might otherwise look suspicious.
A request has a limit of 1000 customers per batch. If more customers are present in the batch, we will respond with an error.
Name | Type | Description | |||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
timestamp
required
|
integer |
Unix timestamp with milliseconds (nanoseconds also accepted) |
|||||||||
source
required
|
string |
Indicates the source of the request. Valid values: |
|||||||||
customers
required
|
array |
An array of between 1 and 1000 accounts. Hide definition |
|||||||||
|
{
"timestamp": 1536578369411033583,
"source": "ATO",
"customers": [
{
"customerId": "customerID-1",
"method": "PasswordReset"
},
{
"customerId": "customerID-2",
"method": "AccountDeleted"
},
{
"customerId": "customerID-3",
"method": "PasswordReset"
}
]
}
On a successful request, we will respond with a message containing the number of accounts processed. This will always be the same as the amount provided to us in request payload.
{
"status": 200,
"message": "3 customer accounts reclaimed successfully"
}
{
"status": 400,
"message": "No customer accounts provided. Check https://developer.ravelin.com/apis/ato/reclaim for more details",
"traceId": "7fffffffa35375cb0292ba0fa-45c3336a-7592-4ecd-4f76-99d8d0fa2cfd",
"timestamp": 1554811444
}