Account takeover is an attack where bad actors gain unauthorized access to the victim's online accounts, such as their email or bank account. Attackers can steal data from the account and execute other kinds of attacks, despite additional identity factors (2FA/MFA).
This guide describes step by step how to protect your user accounts against these attacks using Mosaic Fraud Prevention.
Common attack vectors for account takeover include:
- credential stuffing using stolen credentials (e.g., from a data breach)
- bots or automation tools used to execute brute-force attacks or credential stuffing
- device takeover using remote access (e.g., RDC or malware)
- social engineering to obtain personal info or trick users into providing remote access
- session/cookie hijacking to perform unauthorized actions within the user's session
The account takeover protection flow works by continuously monitoring user actions and assessing risk before authentication. When a user attempts to log in, reset their password, or change account details, Mosaic analyzes the context (device, network, behavior patterns) and provides a risk recommendation. Based on this recommendation, your application can allow the action, require additional authentication (challenge), or deny the request.

Where the login action is reported: The frontend does not report the login event to the Fraud Prevention SDK. When the user clicks Log In, the frontend sends the session token and claimed user id to your backend. Your backend then reports the action (e.g. login) to Mosaic via the trigger-action API (see Step 2 and backend steps).
The following diagram shows the flow for any user action (login, password_reset, or account_details_change). All action types follow the same flow independently when triggered by the user.
Before integrating Fraud Prevention against account takeover, complete these prerequisites:
Get client credentials from the Admin Portal. See Get client credentials for detailed steps.
Enable communication with Mosaic's APIs by whitelisting IPs and URLs, and extending Content-Security-Policy headers if needed. See Enable communication with Mosaic APIs for details.
Install the Fraud Prevention SDK in your project either with npm or yarn:
npm install @transmitsecurity/platform-web-sdk@^2Configure your application backend — the service where you implement login and call Mosaic's Fraud Prevention APIs — with Mosaic's base URL and your client credentials. The base URL depends on your region. There, define:
const BASE_URL = 'https://api.transmitsecurity.io'; // Ensure you use your tenant’s region base URL or custom domain. const CLIENT_ID = 'CLIENT_ID'; // From Admin Portal const CLIENT_SECRET = 'CLIENT_SECRET';
The integration code below is organized into frontend and backend steps. As you progress, the relevant code is highlighted in the panel. The steps mirror the controls and checks in the Integration Checker tab.
This page provides an example implementation in JavaScript aligned with the Integration Checker. For platform-specific, step-by-step SDK integration, see:
- Web: JavaScript SDK, Angular SDK, React SDK
- Mobile: Android SDK, iOS SDK, React Native iOS SDK, React Native Android SDK
Step 1: Initialize SDK
Initialize the SDK in your application to start collecting telemetry data (device, network, behavior) and send it to Mosaic's machine learning engine.
- In your main HTML file (e.g.,
index.html) or in your application's entry point (e.g.,main.js,App.js)
Initialize the SDK as early as possible, ideally when your application starts or when the first page loads.
Step 2: Trigger an action event and send context to backend
Notify Mosaic that the user is attempting a sensitive action (e.g. login) so you can get a risk-based decision and respond by trusting, allowing, challenging, or denying the attempt.
What is an action event?
An action event notifies Mosaic that a user is attempting a sensitive action, such as logging in, resetting a password, or changing account details. Mosaic evaluates the action's context—including device, network, and behavioral signals—and returns a risk recommendation that you use to allow, challenge, or deny the action.
For account takeover protection, the most critical actions to monitor are login, password_reset, and account_details_change.
Why does claimed user id matter?
Even though the user hasn't authenticated yet, passing a claimed user id (a hashed identifier for the account they're trying to access—do not send PII like email in plain text) helps Mosaic compare the current behavior against the profile of that account. This is crucial for detecting account takeover attacks. See Associate users with risk actions for more details.
How does the backend use what I send?
Your backend receives the session token and claimed user id, calls Mosaic's API with them (Steps 4–6 in the Backend section), and returns the recommendation to your client. Your client then allows, challenges, or denies the attempt as in the snippet above.
In the event handler where the user attempts the sensitive action (e.g., login button click handler, password reset button click handler, form submit handler). Call it when the user tries the action—before authentication—so your backend can request a recommendation and you can handle the response before proceeding.
- Call
getSessionToken()to get the current device session token (it links this action to the device and telemetry already sent to Mosaic so risk is assessed in context). - Send the session token and the claimed user identifier to your backend. The claimed user id tells Mosaic which account is being accessed, so it can compare current behavior against that account's profile and detect account takeover—even before the user has authenticated.
- Handle the backend response: show an error on DENY, run your challenge flow (e.g. SMS OTP) on CHALLENGE, or complete login on ALLOW/TRUST.
Do not send PII (such as email or phone number) in plain text as claimed_user_id.
Use an opaque identifier instead—for example, a hashed value of the user’s email or username—so the same account consistently maps to the same ID without exposing personal data. For details, see Associate users with risk actions.
- When the user attempts the sensitive action (e.g., clicks login, clicks password reset, submits form).
- Before authentication occurs (for
loginandpassword_reset). - Before the action is processed on your backend.
Step 3: Clear user
When a user logs out or their session expires, clear the user identifier so they are not associated with future actions from other users on the same device. This does not happen automatically: you must call the clear-user method in both cases.
- In your logout handler/function (when the user explicitly logs out).
- Where your app detects that the session has expired (e.g. user left without logging out and the session later expires).
- As soon as the user logs out.
- When you detect that the session has expired.
Step 4: Get OAuth access token
In your backend, obtain an OAuth access token using your client credentials. This authorizes calls to Mosaic's Risk APIs (trigger-action and action/result).
In your application backend (in the same flow that will later call trigger-action and action/result).
- Call the OAuth token endpoint with your client credentials.
- Store the
access_tokenfrom the response (you use it when calling trigger-action and action/result).
- Before calling the trigger-action or action/result APIs.
- You can cache the token until it expires.
Step 5: Get recommendation
What is a recommendation?
A recommendation is Mosaic's real-time suggestion on how to respond to a user's action based on the assessed risk level. After analyzing the context (device, network, behavior patterns), Mosaic returns one of four recommendation types: trust, allow, challenge, or deny. Each recommendation tells you whether to proceed with the action, require additional authentication, or block it entirely. See Recommendations for a detailed explanation of each recommendation type and how to use them.
How do I get a recommendation in this flow?
Your backend calls POST /risk/v1/action/trigger-action?get_recommendation=true with the session_token, action_type, claimed_user_id_type and claimed_user_id received from the frontend in Step 2 (see API reference here). Mosaic returns a recommendation and an action_token. You then handle that recommendation and report the outcome (Step 6).
- In your backend endpoint that handles the sensitive action (e.g., your login endpoint).
- Use the
access_tokenfrom Step 4. - Call POST
/risk/v1/action/trigger-action?get_recommendation=truewithsession_token,action_type,claimed_user_id, and optionalcorrelation_id. (Use the hashed value received from the frontend—do not send PII in plain text.) - Receive the recommendation and
action_token(the latter is used in Step 6 when reporting the result).
- After receiving the session token and claimed user from your frontend (Step 2).
- After obtaining the OAuth access token (Step 4).
- Before completing the user's action (login, password reset, account details change).
Step 6: Handle recommendation and report result
Tell Mosaic how the action ended and, on success, link this device to the user so future risk checks can use that profile.
- In your backend, in the same flow that obtained the recommendation (Step 5): after you get the recommendation, branch on DENY / CHALLENGE / ALLOW|TRUST, then call the action/result API accordingly.
- Pass
user_idin the action/result request body whenresultissuccess(after the user has fully authenticated, including any 2FA/MFA).
For each outcome, call POST /risk/v1/action/result with the action_token and the result (success, failure, or incomplete). When the result is success, include user_id so Mosaic associates this device with that user and can build a behavioral profile. In practice:
- If the recommendation is DENY: report result
failureand return (e.g. respond to client with login denied). - If the recommendation is CHALLENGE: run your challenge (e.g. SMS OTP), then report result
successorfailureand optionallychallenge_type; includeuser_idon success. - If the recommendation is ALLOW or TRUST: report result
successwithuser_id(opaque identifier), then complete login.
The user_id must be an opaque identifier (not email, phone, or other PII in plain text).
- Immediately after getting the recommendation (Step 5).
- Only set
user_idafter the user is fully authenticated, including any 2FA/MFA that was required.
// ========== CLIENT INTEGRATION (Web SDK) ==========
// Install SDK: see Before you start in the guide.
import { drs, initialize } from '@transmitsecurity/platform-web-sdk';
// Initialize the SDK as early as possible (e.g. app entry point) to start telemetry collection.
// If SDK was loaded via script tag, use: window.tsPlatform.initialize({...})
initialize({
clientId: 'YOUR_CLIENT_ID',
drs: {
enableSessionToken: true,
serverPath: 'https://api.transmitsecurity.io/risk-collect/', // US
// For EU: 'https://api.eu.transmitsecurity.io/risk-collect/'
// For Canada: 'https://api.ca.transmitsecurity.io/risk-collect/'
// For Australia: 'https://api.au.transmitsecurity.io/risk-collect/'
},
});
// When the user attempts a sensitive action (e.g. login), get session token and send to your backend with claimed user id.
async function onLoginClick(username) {
const sessionToken = await drs.getSessionToken();
const response = await fetch('https://your-backend.com/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionToken: sessionToken,
username: username,
}),
});
const result = await response.json();
if (result.recommendation === 'DENY') {
showError('Login denied');
} else if (result.recommendation === 'CHALLENGE') {
presentChallenge(result.challengeType); // e.g. trigger SMS OTP
} else {
// TRUST or ALLOW — proceed with login (user is set via reportActionResult on backend)
completeLogin(result);
}
}
// On logout (or session expiry), clear the user so the device is not tied to that user for future actions.
async function onLogoutClick() {
await drs.clearUser();
performLogout();
}
// ========== BACKEND INTEGRATION ==========
// BASE_URL, CLIENT_ID, CLIENT_SECRET: see Before you start in the guide.
// Obtain an OAuth access token with client credentials before calling Risk APIs.
async function getAccessToken() {
const response = await fetch(`${BASE_URL}/oidc/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
}),
});
const data = await response.json();
return data.access_token;
}
// Trigger the action and get the risk recommendation in one call (session_token, action_type, claimed_user_id).
async function triggerLogin(accessToken, sessionToken, claimedUserId, correlationId) {
const response = await fetch(
`${BASE_URL}/risk/v1/action/trigger-action?get_recommendation=true`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
session_token: sessionToken,
action_type: 'login',
claimed_user_id: claimedUserId, // use hashed value in production
claimed_user_id_type: 'email',
correlation_id: correlationId,
}),
}
);
return await response.json();
}
// Handle DENY / CHALLENGE / ALLOW|TRUST and report the action result; set user_id on success.
async function handleLoginRequest(sessionToken, username) {
const accessToken = await getAccessToken();
const correlationId = crypto.randomUUID();
const { action_token, recommendation } = await triggerLogin(
accessToken,
sessionToken,
username,
correlationId
);
const recommendationType = recommendation.recommendation.type;
if (recommendationType === 'DENY') {
await reportActionResult(accessToken, action_token, 'failure', undefined, undefined);
return { recommendation: 'DENY' };
}
if (recommendationType === 'CHALLENGE') {
const challengeResult = await performChallenge('sms_otp');
await reportActionResult(
accessToken,
action_token,
challengeResult.success ? 'success' : 'failure',
challengeResult.success ? 'opaque-internal-user-id' : undefined,
'sms_otp'
);
return {
recommendation: 'CHALLENGE',
challengeType: 'sms_otp',
success: challengeResult.success,
};
}
// ALLOW or TRUST — report success and set user (user_id must be opaque, not PII)
await reportActionResult(accessToken, action_token, 'success', 'opaque-internal-user-id', undefined);
return { recommendation: recommendationType };
}
// Report action result to Mosaic; include user_id on success to associate the device session with the user.
async function reportActionResult(accessToken, actionToken, result, userId, challengeType) {
const body = {
action_token: actionToken,
result: result, // 'success', 'failure', or 'incomplete'
};
if (result === 'success' && userId) {
body.user_id = userId;
}
if (challengeType) {
body.challenge_type = challengeType;
}
await fetch(`${BASE_URL}/risk/v1/action/result`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify(body),
});
}