# Prevent account takeover

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.

Note
This guide references the [**Integration validator**](/guides/risk/integration_validator) (**Fraud Prevention** > **Configuration** > **Integration validator** tab), a tool that validates your implementation and flags potential issues. The feature is currently available on selected tenants and is being gradually rolled out.

## Attack vector examples

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


## How it works

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.

img
**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](#step-2-trigger-an-action-event-and-send-context-to-backend) 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.


```mermaid
sequenceDiagram
    participant U as User
    participant F as Frontend<br/><br/>(SDK)
    participant B as Backend
    participant M as Mosaic

    Note over F: SDK initialized<br/>Telemetry collection starts

    U->>F: Navigates to app
    F->>M: Telemetry data (automatic)

    U->>F: Performs action<br/>(e.g. login)
    F->>F: getSessionToken()
    F->>B: Action request + sessionToken, claimedUserId
    B->>M: Request OAuth access token
    M->>B: Returns access_token
    B->>M: POST /risk/v1/action/trigger-action<br/>?get_recommendation=true
    M->>B: Returns recommendation (+ action_token)
    B->>M: POST /risk/v1/action/result<br/>(result, user_id on success)
```

## Before you start

Before integrating Fraud Prevention against account takeover, complete these prerequisites:

1. **Get client credentials** from the [Admin Portal](https://portal.transmitsecurity.io/). See [Get client credentials](/guides/risk/quick_start_web#step-1-get-client-credentials) for detailed steps.
2. **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](/guides/quick_start/enable_communication) for details.
3. **Install the Fraud Prevention SDK** in your project either with npm or yarn:


npm

```bash
npm install @transmitsecurity/platform-web-sdk@^2
```

yarn

```bash
yarn add @transmitsecurity/platform-web-sdk@^2
```

1. **Configure 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:

```js
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';
```


Client setup recommendation
Use a separate client for each application integration you protect with Fraud Prevention. For example, use a web client for your browser app, one native client for your Android app, and a separate native client for your iOS app.

## Integration steps

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 validator**](https://portal.transmitsecurity.io/fraud-prevention/configuration/integration-validator) tab.

This page provides an **example implementation in JavaScript** aligned with the Integration validator. For platform-specific, step-by-step SDK integration, see:

- **Web:** [JavaScript SDK](/guides/risk/quick_start_web), [Angular SDK](/guides/risk/quick_start_angular), [React SDK](/guides/risk/quick_start_react)
- **Mobile:** [Android SDK](/guides/risk/quick_start_android), [iOS SDK](/guides/risk/quick_start_ios), [React Native SDK](/guides/risk/quick_start_react_native)


## Frontend integration

div
div
Integration validator: Telemetry collection
> Initialize the SDK in your application to start collecting telemetry data (device, network, behavior) and send it to Mosaic's machine learning engine.


Integration validation
When **No connectivity** appears in the Integration validator page, Mosaic has not received data from your client in the last 60 minutes. Once activity is detected, you'll be able to see your integration details in the [Integration validator](/guides/risk/integration_validator).

### Where to add this code

- In your main HTML file (e.g., `index.html`) or in your application's entry point (e.g., `main.js`, `App.js`)


### When to initialize

Initialize the SDK as early as possible, ideally when your application starts or when the first page loads.

div
div
Integration validator: Events are received for action types (login, etc.)
div
div
Integration validator: Claimed user ID and Claimed User ID type are available
> 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.


div
details
summary
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`.

details
summary
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](/guides/risk/maintaining_user_identity#claim-the-user) for more details.

details
summary
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.

### Where to add this code

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.

### What it does

1. 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).
2. 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.
3. 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.


Important
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](/guides/risk/maintaining_user_identity#claim-the-user).

### When to call

- When the user attempts the sensitive action (e.g., clicks login, clicks password reset, submits form).
- Before authentication occurs (for `login` and `password_reset`).
- Before the action is processed on your backend.


div
div
Integration validator: Clear user method was called
> 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.


### Where to add this code

- 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).


### When to call

- As soon as the user logs out.
- When you detect that the session has expired.


## Backend integration

> 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).


### Where to add this code

In your application backend (in the same flow that will later call trigger-action and action/result).

### What it does

1. Call the OAuth token endpoint with your client credentials.
2. Store the `access_token` from the response (you use it when calling trigger-action and action/result).


### When to call

- Before calling the trigger-action or action/result APIs.
- You can cache the token until it expires.


div
div
Integration validator: Recommendation API calls
div
div
Integration validator: Events are received for action types: login
div
details
summary
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](/guides/risk/recommendations) for a detailed explanation of each recommendation type and how to use them.

details
summary
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](/openapi/risk/client-actions.openapi/other/triggeranaction)). Mosaic returns a recommendation and an `action_token`. You then handle that recommendation and report the outcome (Step 6).

### Where to add this code

- In your backend endpoint that handles the sensitive action (e.g., your login endpoint).


### What it does

1. Use the `access_token` from Step 4.
2. Call **POST** `/risk/v1/action/trigger-action?get_recommendation=true` with `session_token`, `action_type`, `claimed_user_id`, `claimed_user_id_type`, and optional `correlation_id`. (Use the hashed value received from the frontend—do not send PII in plain text.)
3. Receive the recommendation and `action_token` (the latter is used in Step 6 when reporting the result).


### When to call

- 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).


div
div
Integration validator: Report action result is available
div
div
Integration validator: User ID is hashed
> Tell Mosaic how the action ended and, on success, link this device to the user so future risk checks can use that profile.


### Where to add this code

- 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_id` in the action/result request body when `result` is `success` (after the user has fully authenticated, including any 2FA/MFA).


### What it does

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:

1. If the recommendation is **DENY**: report result `failure` and return (e.g. respond to client with login denied).
2. If the recommendation is **CHALLENGE**: run your challenge (e.g. SMS OTP), then report result `success` or `failure` and optionally `challenge_type`; include `user_id` on success.
3. If the recommendation is **ALLOW** or **TRUST**: report result `success` with `user_id` (opaque identifier), then complete login.


Important
The `user_id` must be an opaque identifier (not email, phone, or other PII in plain text).

### When to call

- Immediately after getting the recommendation (Step 5).
- Only set `user_id` after the user is fully authenticated, including any 2FA/MFA that was required.


style

  /* Integration validator badges */
  .code-walkthrough .code-step-wrapper > div > p:first-child {
    font-size: 1.125rem;
  }
  .badge-wrapper {
    margin: 10px 0;
    justify-content: flex-start;
  }
  .badge {
    margin: 0 !important;
    font-weight: 600;
    font-size: 13px;
    color: rgb(110, 98, 4);
    text-align: left;
  }

  /* FAQ / accordion */
  .faq-wrapper details summary {
    color: #6982FF !important;
  }
  summary {
    list-style: none;
    cursor: pointer;
  }
  summary::before {
    content: "›";
    display: inline-block;
    margin-right: 0.1em;
    transform: rotate(0);
    width: 1.2em;
    text-align: center;
    letter-spacing: 0.1em;
    font-size: 1.2em;
  }
  details[open] summary::before {
    transform: rotate(90deg);
  }
  [data-component-name="Markdown/Markdown"] blockquote {
    border-left: 4px solid #BBC5FF !important;
    border-radius: 2px;
    background-color: #F1F2FF !important;
    padding: 20px 10px 15px 10px;
    margin: 10px 1px;
  }
  [data-component-name="Markdown/Markdown"] [data-component-name="Admonition/Admonition"][type="info"] {
    box-sizing: border-box;
    width: auto;
    max-width: var(--md-content-max-width);
    align-items: flex-start;
    gap: 12px;
    margin: 16px auto 24px !important;
    padding: 16px 18px;
    border-radius: 8px;
  }
  [data-component-name="Markdown/Markdown"] [data-component-name="Admonition/Admonition"][type="info"] svg {
    margin-top: 2px;
  }
  [data-component-name="Markdown/Markdown"] [data-component-name="Admonition/Admonition"][type="info"] > div {
    min-width: 0;
    gap: 4px;
  }
  [data-component-name="Markdown/Markdown"] [data-component-name="Admonition/Admonition"][type="info"] p {
    margin: 0;
    overflow-wrap: anywhere;
  }

  /* Walkthrough layout + inherited theme vars */
  .code-walkthrough {
    display: flex;
    width: 100%;
    max-width: 100%;
    overflow-x: clip;
    --code-step-vertical-line-bg-active: #BBC5FF;
    --code-step-vertical-line-bg-hover: #F6F9FF;
    --code-step-bg-hover: #F6F9FF;
    --layer-color: rgb(248, 248, 255);
    --layer-color-hover: rgb(248, 248, 255);
    --code-step-bg-active-hover: rgb(248, 248, 255);
  }
  .code-walkthrough > * {
    flex: 0 0 50%;
    max-width: 50%;
    min-width: 0;
  }

  @media (max-width: 996px) {
    html,
    body,
    [data-component-name="Markdown/Markdown"] {
      overflow-anchor: none;
    }
    .code-walkthrough {
      display: block;
    }
    .code-walkthrough > * {
      flex: 0 1 auto;
      max-width: 100%;
    }
    .code-walkthrough,
    .code-walkthrough * {
      overflow-anchor: none;
    }
    .code-walkthrough .code-step-wrapper,
    .code-walkthrough [data-component-name="Markdoc/CodeWalkthrough/CodeStep"] {
      cursor: default;
      pointer-events: none;
    }
    .code-walkthrough .code-step-wrapper a,
    .code-walkthrough .code-step-wrapper summary,
    .code-walkthrough .code-step-wrapper details,
    .code-walkthrough [data-component-name="Markdoc/CodeWalkthrough/CodeStep"] a,
    .code-walkthrough [data-component-name="Markdoc/CodeWalkthrough/CodeStep"] summary,
    .code-walkthrough [data-component-name="Markdoc/CodeWalkthrough/CodeStep"] details {
      pointer-events: auto;
    }
    .code-walkthrough .code-step-wrapper:focus,
    .code-walkthrough [data-component-name="Markdoc/CodeWalkthrough/CodeStep"]:focus {
      outline: none;
    }
    .code-walkthrough [data-highlighted="true"],
    .code-walkthrough [data-chunk-highlighted],
    .code-walkthrough [class*="chunk-highlight"],
    .code-walkthrough .highlighted-line,
    .code-walkthrough .line.highlighted,
    .code-walkthrough .line[data-highlight],
    .code-walkthrough .diff-add,
    .code-walkthrough .line[data-add],
    .code-walkthrough .shiki .highlight {
      background-color: transparent !important;
    }
    .code-walkthrough .shiki .highlight {
      padding: 0;
    }
    .code-walkthrough .greyed-out,
    .code-walkthrough [class*="greyed"] {
      opacity: 1 !important;
      filter: none !important;
    }
  }

  /* Highlighted lines/chunks */
  .code-walkthrough .line.highlighted,
  .shiki .line.highlighted,
  .shiki .line[data-highlight],
  .shiki .diff-add,
  .shiki .line[data-add] {
    background-color: #BBC5FF !important;
  }
  .shiki .highlight {
    background-color: #BBC5FF;
    padding: 0 4px;
  }

  /* Shiki background */
  .shiki {
    --shiki-light-bg: #1B1941;
    --shiki-dark-bg: #1B1941;
  }

  /* Walkthrough code palette */
  .code-walkthrough [data-component-name="CodeBlock/CodeBlockContainer"],
  .code-walkthrough .shiki {
    --code-block-bg-color: #1B1941;
    --code-block-text-color: rgb(22, 150, 50);
    --code-block-tokens-comment-color: #5468D4;
    --code-block-tokens-prolog-color: #5468D4;
    --code-block-tokens-doctype-color: #5468D4;
    --code-block-tokens-cdata-color: #5468D4;
    --code-block-tokens-string-color: rgb(158, 79, 30);
    --code-block-tokens-property-string-color: rgb(156, 144, 54);
    --code-block-tokens-selector-color: #E8DB7D;
    --code-block-tokens-attr-value-color: #E8DB7D;
    --code-block-tokens-keyword-color: rgb(0, 154, 174);
    --code-block-tokens-atrule-color: rgb(0, 154, 174);
    --code-block-tokens-number-color: #558C8C;
    --code-block-tokens-constant-color: rgb(136, 138, 1);
    --code-block-tokens-symbol-color: #558C8C;
    --code-block-tokens-boolean-color: rgb(42, 42, 88);
    --code-block-tokens-variable-color: #558C8C;
    --code-block-tokens-property-color: #558C8C;
    --code-block-tokens-tag-color: #69658C;
    --code-block-tokens-attr-name-color: #69658C;
    --code-block-tokens-builtin-color: #558C8C;
    --code-block-tokens-function-color: #E3E499;
    --code-block-tokens-class-name-color: #558C8C;
    --code-block-tokens-operator-color: #69658C;
    --code-block-tokens-entity-color: #69658C;
    --code-block-tokens-url-color: #558C8C;
    --code-block-tokens-regex-color: #E8DB7D;
    --code-block-tokens-regex-char-escape-color: #558C8C;
    --code-block-tokens-inserted-color: #558C8C;
    --code-block-tokens-deleted-color: #82204A;
    --code-block-tokens-important-color: #82204A;
    --code-block-tokens-invalid-color: #82204A;
    --code-block-tokens-message-error-color: #82204A;
    --code-block-tokens-unmatched-color: #82204A;
  }