Secure and frictionless device change
The section on protecting app logins explains how to use Incognia more broadly to improve the recognition of trusted users at login without adding friction to the user experience. However, there are several specific high-risk authentication scenarios where MFA is always required and traditional factors and authentication controls are very high friction for users. One such scenario is device change. New devices authenticating to known accounts are a challenge because it may be an attacker trying to take over a compromised account or a legitimate user who simply bought a new phone. Because it's traditionally been difficult to distinguish between a good and bad device change, organizations have been forced to increase authentication friction and limit app functionality on the new device prior to completing MFA for all users, legitimate or otherwise.
With Incognia’s layered location behavior and device intelligence solution, your organization can identify new devices, silently authenticate good users based on their Trusted Locations, and block compromised account access using Location Behavior and Device Reputation (see the section on understanding risk assessments).
In this how-to, you will learn how to integrate the Incognia Transaction API to automatically authenticate trusted login attempts coming from new devices with no added friction, trigger step-up authentication when necessary, and block suspicious logins.
Requirements
- An Incognia account. Contact us to get one.
- An app with the Incognia SDK initialized. Follow SDK getting started guide for this.
- API credentials to be able to make requests to Incognia APIs.
Step-by-step
Setting the Account ID
In order to allow the detection of permanent logins, Incognia needs to have an association between a device and a unique account identifier. The Account ID is used for this purpose.
important
This association must happen in two moments:
1. Whenever the application initializes
This covers the scenario where the application is updated with the Incognia SDK and the user is already logged in. The Application.onCreate()
on Android or the [AppDelegate application:didFinishLaunchingWithOptions:]
on iOS are good places to do this.
If the Account Id is not available in those locations, it is important to forward the value to the Incognia SDK as soon as it is available. The important action here is to call setAccountId whenever the application initializes so it is guaranteed that we always know that a specific user is associated with a specific device.
2. Whenever the user logs in and out
It is necessary to call the setAccountId method when the user finishes the login and clearAccountId when the user logs out. This is usually done in the callback of these two actions.
tip
- Kotlin
- Java
- Swift
- Objective-C
- Javascript
// Set the Account ID
Incognia.setAccountId(accountId)
// Set the Account ID
Incognia.setAccountId(accountId);
// Set the Account ID
ICGIncognia.setAccountId(accountId)
// Set the Account ID
[ICGIncognia setAccountId:accountId];
// Set the Account ID
Incognia.setAccountId(accountId);
warning
Forwarding the device's Installation ID to your server
To verify a login attempt, Incognia needs to receive an Installation ID to identify the device from which the login originates. Since your server will request the Incognia API to assess the risk of this login, it needs to receive this information from your mobile application.
The installation ID can be forwarded to your server in two ways.
Option 1: Sending the Installation ID as a header
Before sending a login request from your mobile application to your server, call Incognia.getInstallationId
and set its value as a header of the request. Choose a clear name for this header, like Incognia-Installation-ID
, so you can easily identify it on the server-side.
This option has a clear benefit if your application will use more than one Incognia solution because you won't need to change each request's properties, like signup, login, payment, password change, etc.
- Kotlin
- Java
- Swift
- Objective-C
- Javascript
// This method may return null if the SDK has not yet been initialized or if an error occurs on initialization.
val installationId = Incognia.getInstallationId()
// HttpURLConnection
httpUrlConnection.setRequestProperty("Incognia-Installation-ID", installationId)
// Send the request with the installationId to your backend server
// This method may return null if the SDK has not yet been initialized or if an error occurs on initialization.
String installationId = Incognia.getInstallationId();
// HttpURLConnection
httpUrlConnection.setRequestProperty("Incognia-Installation-ID", installationId);
// Send the request with the installationId to your backend server
// This method may return nil if the SDK has not yet been initialized or if an error occurs on initialization.
let installationId = ICGIncognia.installationId()
// NSURLRequest
request.setValue(installationId, forHTTPHeaderField: "Incognia-Installation-ID")
// Send the request with the installationId to your backend server
// This method may return nil if the SDK has not yet been initialized or if an error occurs on initialization.
NSString *installationId = [ICGIncognia installationId];
// NSURLRequest
[request setValue:installationId forHTTPHeaderField:@"Incognia-Installation-ID"];
// Send the request with the installationId to your backend server
import Incognia from 'react-native-incognia';
let sendIncogniaInstallationId = async () => {
await Incognia.fetchInstallationId()
// Send the installationId to your server
// ...
};
Option 2: Sending the Installation ID in the body of the request
Before sending the login request from your mobile application to your server, call Incognia.getInstallationId
and send it as additional information about this login. Choose a clear name for this property like Incognia-Installation-ID
, so you can easily identify it on the server-side.
Handling the user's first login request
When your server receives a login request, you can use Incognia intelligence to identify first login attempts from a new device and assess the risk inside this request/response cycle. This is possible because you set the Account ID in previous step (Setting the Account ID on the app).
To evaluate this login attempt risk, your server will request the Login Transaction API informing that a login attempt was made alongside its Installation ID and the Account ID of the account that the device is attempting to access.
A sample implementation of a controller/handler
Let's consider a toy example as back-end with the controller below:
- Java
- Ruby
// LoginController.java
@Controller("/login")
public class LoginController {
private final PasswordAuthenticator passwordAuthenticator;
private final TokenGenerator tokenGenerator;
@Post
public HttpResponse<?> postLogin(@Valid @Body LoginRequest request) {
boolean isPasswordValid = passwordAuthenticator.isValid(request.getEmail(), request.getPassword());
if (isPasswordValid) {
Token token = tokenGenerator.getTokenForUser(request.getEmail());
return HttpResponse.ok(token);
}
return HttpResponse.unauthorized();
}
}
# sessions_controller.rb
class SessionsController < ApplicationController
def create
@login_form = LoginForm.new(params)
if @user = @login_form.submit
sign_in @user
redirect_to @user, notice: "Welcome!"
else
render action: :new
end
end
end
# login_form.rb
class LoginForm < BaseForm
attr_accessor :email, :password
validates :email, :password, presence: true
# Other validations...
def submit
return nil if invalid?
user&.authenticate(password)
end
private
def user
@user ||= User.find_by(email: email.downcase)
end
end
important
Considering that the authentication logic is implemented, you can add risk assessment requests to your login handler:
- Java
- Ruby
// LoginController.java
@Controller("/login")
public class LoginController {
private final PasswordAuthenticator passwordAuthenticator;
private final TokenGenerator tokenGenerator;
// read how to use Incognia's Java Wrapper: https://github.com/inloco/incognia-api-java
private final IncogniaAPI incogniaAPI;
@Post
public HttpResponse<?> postLogin(@Valid @Body LoginRequest request) {
boolean isPasswordValid = passwordAuthenticator.isValid(request.getEmail(), request.getPassword());
if (!isPasswordValid) {
return HttpResponse.unauthorized();
}
RegisterLoginRequest incogniaRequest =
RegisterLoginRequest.builder()
.installationId(request.getIncogniaInstallationId())
.accountId(request.getAccountId())
.evaluateTransaction(true)
.build();
TransactionAssessment transactionAssessment = incogniaAPI.registerLogin(incogniaRequest);
Assessment riskAssessment = transactionAssessment.getRiskAssessment();
Boolean isFirstDeviceLogin = transactionAssessment.getEvidence().get("first_device_login");
if (isFirstDeviceLogin) {
if (riskAssessment.equals(Assessment.HIGH_RISK)) {
// Automatically denies if Incognia gives high risk!
return HttpResponse.unauthorized();
}
else if (riskAssessment.equals(Assessment.UNKNOWN_RISK)){
// Trigger step-up authentication if Incognia gives unknown risk.
return HttpResponse.forbidden();
}
}
else {
// Handle common login attempts
}
}
}
# login_form.rb
class LoginForm < BaseForm
attr_accessor :email, :password, :incognia_installation_id
validates :email, :password, presence: true
validate :device_risk
# Other validations...
def submit
return nil if invalid?
user&.authenticate(password)
end
private
def user
@user ||= User.find_by(email: email.downcase)
end
def device_risk
return unless user
api = Incognia::Api.instance
response = api.register_login(
installation_id: incognia_installation_id,
account_id: user.id
)
risk_assessment = response['risk_assessment']
is_first_login = response.dig('evidence', 'first_device_login')
# Special handling for first device login attempt
if is_first_login
# Automatically denies if Incognia gives high risk!
if risk_assessment == 'high_risk'
errors.add(:incognia_installation_id, 'considered unsafe!')
# Trigger step-up authentication if Incognia gives unknown risk.
elsif risk_assessment == 'unknown_risk'
errors.add(:incognia_installation_id, 'needs additional authentication.')
end
else
# Handle common login attempts
end
end
end
# incognia/api.rb
require 'faraday'
require 'json'
module Incognia
class Api
include Singleton
API_HOST = 'https://api.incognia.com/api/'.freeze
def register_login(installation_id:, account_id:)
transactions_endpoint = 'v2/authentication/transactions'
params = {
installation_id: installation_id,
account_id: account_id,
type: 'login'
}
response = Faraday.post(
"#{API_HOST}#{transactions_endpoint}",
params.to_json,
headers
)
if response.status == 200
JSON.parse(response.body)
else
# Error handling
end
end
private
def headers
{
'Content-Type': 'application/json',
# Read more about how to generate a fresh token at our
# Authenticating in Incognia APIs section.
Authorization: "Bearer #{fresh_token}"
}
end
end
end
Deployment considerations
Incognia relies on the relation between a device and a unique account identifier to identify new devices authenticating to known accounts. When you call Incognia.setAccountId
on your app (as detailed in Setting the Account ID on the app) the link between account and device is made and the first_device_login
evidence it is set to true only for actual unknown devices. To avoid considering your logged in users and their devices as device changes (new devices), you need to roll out the app before starting calling the API as follows:
1. Release the app integrated with Incognia SDK
Every authenticated user will call Incognia.setAccountId
.
2. Give time to Incognia know your account's identifiers and devices
Over time Incognia will receive the SDK events and have the devices linked with your unique account identifier. You need to monitor the percentage of your users with the SDK and the percentage of active installs with Account ID. When the first reaches 100% and the second stabilizes the growth curve, you can move forward and release the server integrated with Incognia.
For example, the image below shows metrics of an app that was released recently and did not reached the needed stability regarding the % of active installs with Account ID.
This second image shows metrics of an app that reached the stability and the server can be released.
See how to monitor the percentage of active installs with Account ID at Incognia App metrics dashboard.
3. Release the server integrated with Incognia Login Transaction API
Your server will start calling Incognia Login Transaction API that will identify only new devices authenticating to known accounts as first login attempts.
important
If your app has persistent sessions, it is possible that Incognia.setAccountId
will be called only when the user logs out and logs in again. This can increase the time necessary to Incognia know your account's identifiers and devices, and it is also influenced by the length of your app user session. To circumvent this, you can force the termination of your user’s session.
Using the Incognia risk assessment
When your server makes a request to the Transaction API endpoint, it will receive a response like the following:
{
"id": "96fafbb9-93af-433a-b047-af1f6fc3c279",
"risk_assessment": "low_risk",
"reasons": [
{
"code": "trusted_location",
"source": "local"
}
],
"evidence": {
"device_model": "LM-X520",
"known_account": true,
"location_services": {
"location_permission_enabled": true,
"location_sensors_enabled": true
},
"device_integrity": {
"probable_root": false,
"emulator": false,
"gps_spoofing": false,
"from_official_store": true
},
"device_fraud_reputation": "unknown",
"device_behavior_reputation": "unknown",
"distance_to_trusted_location": 3.4356838410826773,
"last_location_ts": "2022-04-25T13:33:54.285Z",
"sensor_match_type": "wifi_connection",
"account_integrity": {
"recent_high_risk_assessment": false
},
"accessed_accounts": 1,
"app_reinstallations": 1,
"first_device_login_at": "2022-04-20T12:28:44.433318Z",
"first_device_login": false
},
"device_id": "dOgLG4Sm8wRIeyWRyl_-ty-ofSXgZOCACnx1w7T4JLeQAOgRt_7trL24yGmfU5JSR1JEZMAzG-JOtXNFhT3h2A"
}
The response contains the id
of the created entity (in this case, a login), the risk_assessment
provided by Incognia based on device behavior, and evidence
that supports this assessment. You can learn more about all the returned data in this article: Understanding risk assessments.
The returned assessment can be used with other risk-related data, such as in a risk engine, to decide if this login attempt should be accepted.
The first_device_login
evidence indicates if this is the first time that the given device is bind with the given account, you can use this evidence to handle this kind of login attempt differently. It can be used together with Incognia's risk assessments as detailed below:
- First login assessed as
high_risk
: the login performed by the new device may be fraudulent and we advise you to take preventive actions in these scenarios, such as requiring additional step-up authentication or simply blocking this login attempt; - First login assessed as
low_risk
: the login performed by the new device seems to be safe to accept; - First login assessed as
unknown_risk
: we are unable to provide a precise assessment at the time of the request. You should require additional authentication (e.g. 2FA).
Wrapping Up
After these steps, your application is ready to recognize trusted users at device change without friction and preventing account takeovers.