# Account Takeover Prevention

### The Problem

Account takeover (ATO) is one of the most damaging forms of fraud. Attackers gain access to legitimate user accounts through stolen credentials, phishing, or credential stuffing - then drain funds, steal data, or make fraudulent purchases.

The challenge is twofold:

1. **Attackers have valid credentials** - Username and password checks pass
2. **Legitimate users hate friction** - Too much MFA drives customers away

Traditional approaches force a choice: either frustrate every user with constant verification, or leave accounts vulnerable to takeover.

#### Common Attack Vectors

| Attack Type             | Description                                                   | Scale                        |
| ----------------------- | ------------------------------------------------------------- | ---------------------------- |
| **Credential Stuffing** | Automated login attempts using leaked username/password pairs | Millions of attempts per day |
| **Phishing**            | Tricking users into revealing credentials                     | Targeted attacks             |
| **Session Hijacking**   | Stealing active session tokens                                | Individual accounts          |
| **SIM Swapping**        | Taking over phone numbers to bypass SMS MFA                   | High-value targets           |
| **Brute Force**         | Guessing passwords through repeated attempts                  | Automated attacks            |
| **Password Spraying**   | Trying common passwords across many accounts                  | Enterprise targets           |

#### The Core Problem

1. Attacker has stolen credentials
2. Enters correct username + password
3. Traditional system says "Credentials valid" → Access granted

{% hint style="warning" %}
**The credentials are correct.** How do you know it's not the real user?
{% endhint %}

***

### The Solution: Device-Based Recognition

Guardian Stack recognizes **the device**, not just the credentials. When the account owner logs in from their usual device, they sail through. When an attacker logs in with stolen credentials from a different device, additional verification is triggered.

**How It Works**

<figure><img src="/files/xsThA0vQu0pxlKfiGso6" alt=""><figcaption></figcaption></figure>

1. User enters login credentials
2. Guardian SDK silently collects device signals
3. Your backend fetches the Guardian event and checks:
   * Is this a device the user has logged in from before?
   * Is this a bot or automated browser?
   * Is the user hiding behind a VPN/proxy?
   * Does the location match the user's history?
4. Based on risk level → Allow, challenge with MFA, or block

**The Result**

* **Recognized device:** Instant login, no friction
* **New device:** Require email/SMS verification
* **Suspicious device:** Require strong MFA or block
* **Bot/attacker:** Block immediately

***

### Implementation Guide

#### Step 1: Frontend - Capture Device Signals at Login

Install the Guardian JS SDK:

```bash
npm install @guardianstack/guardian-js
```

Initialize Guardian and call `.get()` during login:

```typescript
import { loadAgent } from "@guardianstack/guardian-js";

// Initialize once when your app starts
const guardian = await loadAgent({
  siteKey: "YOUR_SITE_KEY",
});

async function handleLogin(email: string, password: string) {
  // 1. Get Guardian signals
  const response = await guardian.get();
  const requestId = response?.requestId;

  // 2. Submit login with Guardian requestId
  const result = await fetch("/api/login", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      email,
      password,
      guardianRequestId: requestId,
    }),
  });

  return result.json();
}
```

#### Step 2: Backend - Adaptive Authentication

Install the Guardian Server SDK:

```bash
npm install @guardianstack/guardianjs-server
```

Create your login endpoint with risk-based authentication:

```typescript
import {
  createGuardianClient,
  isBot,
  isVPN,
  isTampering,
  isIncognito,
  isVirtualized,
} from "@guardianstack/guardianjs-server";

const guardianClient = createGuardianClient({
  secret: process.env.GUARDIAN_SECRET_KEY!,
});

app.post("/api/login", async (req, res) => {
  const { email, password, guardianRequestId } = req.body;

  // 1. Verify credentials first
  const user = await verifyCredentials(email, password);
  if (!user) {
    return res.status(401).json({ error: "Invalid credentials" });
  }

  // 2. Fetch Guardian event
  const event = await guardianClient.getEvent(guardianRequestId);
  const visitorId = event.identification.visitorId;

  // 3. Check for automation (credential stuffing bots)
  if (isBot(event) || isTampering(event)) {
    await logSecurityEvent("bot_login_attempt", { email, visitorId });
    return res.status(403).json({ error: "Access denied" });
  }

  // 4. Check if this is a recognized device for this user
  const knownDevice = await db.userDevices.findFirst({
    where: {
      userId: user.id,
      visitorId,
      isVerified: true,
    },
  });

  if (knownDevice) {
    // Known device — allow immediate access
    await updateLastSeen(knownDevice.id);
    const token = generateSessionToken(user);
    return res.json({ success: true, token });
  }

  // 5. New device — assess risk level
  const riskLevel = assessLoginRisk(event, user);

  if (riskLevel === "high") {
    // Block suspicious attempts
    await logSecurityEvent("high_risk_login_blocked", { email, visitorId });
    return res.status(403).json({
      error: "Login blocked",
      message: "Please contact support",
    });
  }

  if (riskLevel === "medium") {
    // Require MFA for suspicious logins
    const challengeToken = await createMfaChallenge(user, visitorId);
    return res.status(202).json({
      requiresMfa: true,
      challengeToken,
      methods: ["sms", "email", "totp"],
    });
  }

  // Low risk new device — require light verification
  const challengeToken = await createMfaChallenge(user, visitorId);
  return res.status(202).json({
    requiresMfa: true,
    challengeToken,
    methods: ["email"],
    message: "New device detected. Please verify your identity.",
  });
});
```

#### Step 3: Risk Assessment Function

```typescript
function assessLoginRisk(event: GuardianEvent, user: User): "low" | "medium" | "high" {
  // High-risk signals — block or require strong MFA
  if (isBot(event) || isTampering(event)) {
    return "high";
  }

  if (isVirtualized(event)) {
    return "high";
  }

  // Check velocity (credential stuffing indicator)
  const velocity = event.velocity;
  if (velocity?.["5m"] > 5 || velocity?.["1h"] > 20) {
    return "high";
  }

  // Medium-risk signals — require MFA
  if (isVPN(event) && event.vpn?.timezoneDifference > 4) {
    return "medium";
  }

  if (event.ipInfo?.is_datacenter) {
    return "medium";
  }

  // Geographic anomaly
  const deviceCountry = event.identification.location?.country_code;
  const userCountry = user.lastKnownCountry;
  if (deviceCountry && userCountry && deviceCountry !== userCountry) {
    return "medium";
  }

  // Low-risk signals
  if (isVPN(event) || isIncognito(event)) {
    return "low"; // Still require verification for new device
  }

  return "low";
}
```

#### Step 4: MFA Verification & Device Registration

```typescript
app.post("/api/verify-mfa", async (req, res) => {
  const { challengeToken, code, rememberDevice } = req.body;

  // 1. Verify the MFA code
  const challenge = await getMfaChallenge(challengeToken);
  if (!challenge || !verifyMfaCode(challenge, code)) {
    return res.status(401).json({ error: "Invalid code" });
  }

  const user = await db.users.findUnique({ where: { id: challenge.userId } });

  // 2. If user chose to remember this device, save it
  if (rememberDevice) {
    await db.userDevices.upsert({
      where: {
        userId_visitorId: {
          userId: user.id,
          visitorId: challenge.visitorId,
        },
      },
      create: {
        userId: user.id,
        visitorId: challenge.visitorId,
        isVerified: true,
        deviceName: challenge.deviceInfo?.browser || "Unknown device",
        firstSeenAt: new Date(),
        lastSeenAt: new Date(),
      },
      update: {
        isVerified: true,
        lastSeenAt: new Date(),
      },
    });
  }

  // 3. Complete login
  const token = generateSessionToken(user);
  await clearMfaChallenge(challengeToken);

  return res.json({
    success: true,
    token,
    deviceRemembered: rememberDevice,
  });
});
```

***

### Real-World Examples

#### Credential Stuffing Prevention

**Scenario:** Attackers use bots to test millions of stolen username/password combinations.

```typescript
app.post("/api/login", async (req, res) => {
  const event = await guardianClient.getEvent(req.body.guardianRequestId);
  const visitorId = event.identification.visitorId;

  // Check for bot signals
  if (isBot(event)) {
    // Don't reveal detection — just fail silently
    await logSecurityEvent("credential_stuffing_blocked", { visitorId });
    
    // Add artificial delay to slow down attacks
    await sleep(2000);
    
    return res.status(401).json({ error: "Invalid credentials" });
  }

  // Check login velocity for this device
  const velocity = event.velocity;
  if (velocity["5m"] > 10 || velocity["1h"] > 50) {
    await logSecurityEvent("high_velocity_login", { visitorId, velocity });
    
    return res.status(429).json({
      error: "Too many attempts",
      retryAfter: 3600,
    });
  }

  // Check failed login attempts from this device
  const recentFailures = await db.loginAttempts.count({
    where: {
      visitorId,
      success: false,
      createdAt: { gte: subMinutes(new Date(), 15) },
    },
  });

  if (recentFailures >= 5) {
    return res.status(429).json({
      error: "Too many failed attempts",
      retryAfter: 900,
    });
  }

  // Continue with normal login...
});
```

#### Impossible Travel Detection

**Scenario:** User logs in from New York, then 10 minutes later from Tokyo — physically impossible.

```typescript
app.post("/api/login", async (req, res) => {
  const event = await guardianClient.getEvent(req.body.guardianRequestId);
  const user = await verifyCredentials(req.body.email, req.body.password);
  
  if (!user) {
    return res.status(401).json({ error: "Invalid credentials" });
  }

  const currentLocation = event.identification.location;
  const lastLogin = await db.loginHistory.findFirst({
    where: { userId: user.id },
    orderBy: { createdAt: "desc" },
  });

  if (lastLogin && currentLocation) {
    const timeSinceLastLogin = Date.now() - lastLogin.createdAt.getTime();
    const hoursSinceLastLogin = timeSinceLastLogin / (1000 * 60 * 60);

    const distance = calculateDistance(
      lastLogin.latitude,
      lastLogin.longitude,
      currentLocation.latitude,
      currentLocation.longitude
    );

    // Impossible if distance > 500 miles per hour
    const requiredHours = distance / 500;
    
    if (hoursSinceLastLogin < requiredHours && !isVPN(event)) {
      await logSecurityEvent("impossible_travel", {
        userId: user.id,
        from: lastLogin.city,
        to: currentLocation.city,
      });

      return res.status(202).json({
        requiresMfa: true,
        reason: "unusual_location",
        message: "We noticed a login from a new location. Please verify your identity.",
      });
    }
  }

  // Continue with login...
});
```

#### Session Anomaly Detection

**Scenario:** Active session suddenly changes device fingerprint — possible session hijacking.

```typescript
async function validateSession(req, res, next) {
  const sessionToken = req.headers.authorization?.split(" ")[1];
  const guardianRequestId = req.headers["x-guardian-request-id"];

  const session = await getSession(sessionToken);
  if (!session) {
    return res.status(401).json({ error: "Invalid session" });
  }

  if (guardianRequestId) {
    const event = await guardianClient.getEvent(guardianRequestId);
    const currentVisitorId = event.identification.visitorId;

    if (session.visitorId && session.visitorId !== currentVisitorId) {
      // Device changed mid-session — possible hijacking
      await logSecurityEvent("session_device_mismatch", {
        userId: session.userId,
        originalDevice: session.visitorId,
        currentDevice: currentVisitorId,
      });

      await invalidateSession(sessionToken);
      
      return res.status(401).json({
        error: "Session expired",
        message: "Please log in again",
      });
    }
  }

  req.user = session.user;
  next();
}
```

#### Adaptive MFA for Sensitive Actions

**Scenario:** Normal browsing needs no MFA. Changing password or transferring funds requires verification even on known devices.

```typescript
async function requireElevatedAuth(req, res, next) {
  const guardianRequestId = req.headers["x-guardian-request-id"];
  const event = await guardianClient.getEvent(guardianRequestId);
  const visitorId = event.identification.visitorId;

  // Check if user recently completed MFA
  const recentMfa = await db.mfaCompletions.findFirst({
    where: {
      userId: req.user.id,
      visitorId,
      createdAt: { gte: subMinutes(new Date(), 10) },
    },
  });

  if (recentMfa) {
    return next();
  }

  const riskLevel = assessLoginRisk(event, req.user);

  if (riskLevel === "high") {
    return res.status(202).json({
      requiresMfa: true,
      methods: ["totp"],
      reason: "sensitive_action",
    });
  }

  return res.status(202).json({
    requiresMfa: true,
    methods: ["totp", "email"],
    reason: "sensitive_action",
  });
}

app.post("/api/change-password", requireElevatedAuth, changePasswordHandler);
app.post("/api/transfer-funds", requireElevatedAuth, transferFundsHandler);
```

***

### Database Schema Example

```sql
-- Track devices per user
CREATE TABLE user_devices (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id) ON DELETE CASCADE,
  visitor_id VARCHAR(255) NOT NULL,
  is_verified BOOLEAN DEFAULT false,
  device_name VARCHAR(255),
  first_seen_at TIMESTAMP DEFAULT NOW(),
  last_seen_at TIMESTAMP DEFAULT NOW(),
  
  UNIQUE(user_id, visitor_id)
);

CREATE INDEX idx_user_devices_lookup 
  ON user_devices(user_id, visitor_id);

-- Login history for anomaly detection
CREATE TABLE login_history (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id),
  visitor_id VARCHAR(255),
  ip_address INET,
  country VARCHAR(2),
  city VARCHAR(100),
  latitude DECIMAL(10, 8),
  longitude DECIMAL(11, 8),
  success BOOLEAN,
  risk_level VARCHAR(20),
  created_at TIMESTAMP DEFAULT NOW()
);

-- MFA challenges
CREATE TABLE mfa_challenges (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id),
  visitor_id VARCHAR(255),
  challenge_token VARCHAR(255) UNIQUE,
  method VARCHAR(50),
  code_hash VARCHAR(255),
  expires_at TIMESTAMP,
  completed_at TIMESTAMP,
  created_at TIMESTAMP DEFAULT NOW()
);

-- Security events for monitoring
CREATE TABLE security_events (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  event_type VARCHAR(100) NOT NULL,
  user_id UUID,
  visitor_id VARCHAR(255),
  ip_address INET,
  metadata JSONB,
  created_at TIMESTAMP DEFAULT NOW()
);
```

***

### User Experience: Device Management

Let users see and manage their recognized devices:

```typescript
app.get("/api/my-devices", async (req, res) => {
  const devices = await db.userDevices.findMany({
    where: { userId: req.user.id },
    orderBy: { lastSeenAt: "desc" },
    select: {
      id: true,
      deviceName: true,
      isVerified: true,
      firstSeenAt: true,
      lastSeenAt: true,
    },
  });

  return res.json({ devices });
});

app.delete("/api/my-devices/:deviceId", async (req, res) => {
  await db.userDevices.deleteMany({
    where: {
      id: req.params.deviceId,
      userId: req.user.id,
    },
  });

  return res.json({ success: true });
});

app.post("/api/logout-all-devices", requireElevatedAuth, async (req, res) => {
  await db.userDevices.updateMany({
    where: { userId: req.user.id },
    data: { isVerified: false },
  });

  await db.sessions.deleteMany({
    where: { userId: req.user.id },
  });

  return res.json({ success: true });
});
```

***

### Best Practices

#### Do

* **Remember verified devices** to reduce friction for legitimate users
* **Require MFA on new devices** even with correct credentials
* **Use risk-based authentication** — adapt requirements to threat level
* **Log all login attempts** with device fingerprints for forensics
* **Let users manage devices** — view and revoke recognized devices
* **Monitor for anomalies** — impossible travel, device changes, velocity spikes

#### Don't

* **Don't rely solely on passwords** — they're often compromised
* **Don't trust SMS alone** — vulnerable to SIM swapping
* **Don't block VPNs outright** — many legitimate users use them
* **Don't reveal why login failed** — helps attackers refine attacks
* **Don't skip MFA for "trusted" IPs** — IPs are easily spoofed

***

### Conclusion

Account takeover attacks succeed because they have valid credentials. Traditional systems can't distinguish between the real user and an attacker with stolen passwords.

Guardian Stack solves this by adding a device layer to authentication. The result:

* **Recognized users** log in instantly without friction
* **New devices** require verification — even with correct password
* **Suspicious devices** are blocked or challenged with strong MFA
* **Attackers** can't bypass protection just by having credentials

The key insight: Passwords can be stolen. Devices can't be cloned. Make the device part of your authentication.

***

{% hint style="success" %}
**Get Started:** [Sign up for Guardian Stack](https://dashboard.guardianstack.ai/) and protect your users today.
{% endhint %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.guardianstack.ai/documentation/protect-your-implementation/account-takeover-prevention.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
