---
title: Security Plugin (sentinel)
description: The `sentinel()` plugin provides comprehensive security and abuse protection for your authentication system. It detects and prevents various attack vectors including credential stuffing, impossible travel, free trial abuse, and more.
---

## Installation

```tsx
import { betterAuth } from "better-auth";
import { sentinel } from "@better-auth/infra";

export const auth = betterAuth({
  plugins: [
    sentinel({
      security: {
        // Configure security features here
      },
    }),
  ],
});
```

## Configuration Options

### SentinelOptions

| Option     | Type              | Description                         |
| ---------- | ----------------- | ----------------------------------- |
| `apiUrl`   | `string`          | Better Auth Infrastructure API URL  |
| `kvUrl`    | `string`          | KV store URL for rate limiting data |
| `apiKey`   | `string`          | Your API key for authentication     |
| `security` | `SecurityOptions` | Security feature configuration      |

### SecurityOptions

```ts
interface SecurityOptions {
  unknownDeviceNotification?: boolean;
  credentialStuffing?: CredentialStuffingConfig;
  impossibleTravel?: ImpossibleTravelConfig;
  geoBlocking?: GeoBlockingConfig;
  botBlocking?: boolean | { action: SecurityAction };
  suspiciousIpBlocking?: boolean | { action: SecurityAction };
  velocity?: VelocityConfig;
  freeTrialAbuse?: FreeTrialAbuseConfig;
  compromisedPassword?: CompromisedPasswordConfig;
  emailValidation?: EmailValidationConfig;
  staleUsers?: StaleUsersConfig;
  challengeDifficulty?: number;
}

type SecurityAction = "log" | "challenge" | "block";
```

## Security Features

### Credential Stuffing Protection

Detects and blocks credential stuffing attacks by tracking failed login attempts per visitor.

```ts
sentinel({
  apiKey: process.env.BETTER_AUTH_API_KEY,
  security: {
    credentialStuffing: {
      enabled: true,
      thresholds: {
        challenge: 3,  // Issue PoW challenge after 3 failures
        block: 5,      // Block after 5 failures
      },
      windowSeconds: 3600,     // 1 hour window
      cooldownSeconds: 900,    // 15 minute cooldown after block
    },
  },
}),
```

**How it works:**

1. Tracks failed login attempts per visitor ID
2. After reaching the challenge threshold, issues a Proof-of-Work challenge
3. After reaching the block threshold, blocks the visitor entirely
4. Automatically clears failed attempts on successful login

### Impossible Travel Detection

Detects logins from geographically distant locations in impossibly short timeframes.

```ts
sentinel({
  apiKey: process.env.BETTER_AUTH_API_KEY,
  security: {
    impossibleTravel: {
      enabled: true,
      maxSpeedKmh: 1000,    // Max realistic travel speed
      action: "challenge",  // "log", "challenge", or "block"
    },
  },
}),
```

**Example:** If a user logs in from New York and then 30 minutes later from Tokyo, this would be flagged as impossible travel (would require traveling faster than 1000 km/h).

### Free Trial Abuse Prevention

Prevents users from creating multiple accounts to abuse free trials using device fingerprinting.

```ts
sentinel({
  apiKey: process.env.BETTER_AUTH_API_KEY,
  security: {
    freeTrialAbuse: {
      enabled: true,
      thresholds: {
        challenge: 2,
        block: 3,
      },
      maxAccountsPerVisitor: 3,
      action: "block",
    },
  },
}),
```

**How it works:**

1. Tracks account creations per visitor fingerprint
2. When threshold is exceeded, blocks new account creation
3. Useful for preventing free tier abuse

### Compromised Password Detection

Checks passwords against the HaveIBeenPwned database to detect compromised credentials.

```ts
sentinel({
  apiKey: process.env.BETTER_AUTH_API_KEY,
  security: {
    compromisedPassword: {
      enabled: true,
      action: "block",      // "log", "challenge", or "block"
      minBreachCount: 1,    // Minimum breaches to trigger
    },
  },
}),
```

**Privacy:** Uses k-anonymity - only the first 5 characters of the password hash are sent to the API, never the full password.

### Stale Account Monitoring

Detects when dormant accounts suddenly become active, which could indicate account takeover.

```ts
sentinel({
  apiKey: process.env.BETTER_AUTH_API_KEY,
  security: {
    staleUsers: {
      enabled: true,
      staleDays: 90,           // Account considered stale after 90 days
      action: "log",           // "log", "challenge", or "block"
      notifyUser: true,        // Send email to user
      notifyAdmin: true,       // Send email to admin
      adminEmail: "admin@yourapp.com",
    },
  },
}),
```

**Notifications include:**

* Login time and location
* Days since last activity
* Device information

### Geo-Blocking

Block or challenge users from specific countries.

```ts
sentinel({
  apiKey: process.env.BETTER_AUTH_API_KEY,
  security: {
    geoBlocking: {
      allowList: ["US", "CA", "GB"],  // Only allow these countries
      // OR
      denyList: ["XX", "YY"],          // Block these countries
      action: "block",                  // "challenge" or "block"
    },
  },
}),
```

Use ISO 3166-1 alpha-2 country codes.

### Bot Blocking

Detect and block automated bot traffic.

```ts
sentinel({
  apiKey: process.env.BETTER_AUTH_API_KEY,
  security: {
    botBlocking: true,
    // OR with custom action
    botBlocking: {
      action: "challenge",  // "log", "challenge", or "block"
    },
  },
}),
```

### Suspicious IP Detection

Block requests from known malicious IP addresses.

```ts
sentinel({
  apiKey: process.env.BETTER_AUTH_API_KEY,
  security: {
    suspiciousIpBlocking: true,
    // OR with custom action
    suspiciousIpBlocking: {
      action: "block",
    },
  },
}),
```

### Velocity / Rate Limiting

Limit the rate of various operations.

```ts
sentinel({
  apiKey: process.env.BETTER_AUTH_API_KEY,
  security: {
    velocity: {
      enabled: true,
      thresholds: {
        challenge: 10,
        block: 20,
      },
      maxSignupsPerVisitor: 5,
      maxPasswordResetsPerIp: 10,
      maxSignInsPerIp: 50,
      windowSeconds: 3600,
      action: "challenge",
    },
  },
}),
```

### Email Validation

Block disposable email addresses and validate email domains.

```ts
sentinel({
  apiKey: process.env.BETTER_AUTH_API_KEY,
  security: {
    emailValidation: {
      enabled: true,
      strictness: "medium",  // "low", "medium", or "high"
      action: "block",
    },
  },
}),
```

**Strictness levels:**

* `low` - Block only known disposable domains
* `medium` - Also check for valid MX records
* `high` - Additional heuristic checks

## Proof-of-Work Challenges

When a security check results in a "challenge" action, Sentinel issues a Proof-of-Work (PoW) challenge that must be solved by the client.

### How PoW Works

1. Server issues a cryptographic challenge
2. Client must find a solution that satisfies difficulty requirements
3. Solution is computationally expensive but verification is fast
4. Prevents automated attacks while allowing legitimate users through

### Challenge Difficulty

```ts
sentinel({
  apiKey: process.env.BETTER_AUTH_API_KEY,
  security: {
    challengeDifficulty: 18,  // Default difficulty level
  },
}),
```

Higher difficulty = more computation required = slower for attackers.

## Client Integration

### sentinelClient()

The client plugin handles device fingerprinting and automatic PoW challenge solving.

```ts
import { createAuthClient } from "better-auth/client";
import { sentinelClient } from "@better-auth/infra/client";

export const authClient = createAuthClient({
  plugins: [
    sentinelClient({
      autoSolveChallenge: true,
    }),
  ],
});
```

### Configuration

| Option               | Type      | Default | Description                        |
| -------------------- | --------- | ------- | ---------------------------------- |
| `autoSolveChallenge` | `boolean` | `true`  | Automatically solve PoW challenges |

### Browser Fingerprinting

The client automatically includes a visitor ID in requests via the `X-Visitor-Id` header. This fingerprint is used for:

* Credential stuffing detection
* Free trial abuse prevention
* Device tracking

### PoW Solution Header

When auto-solving is enabled, solved challenges are sent via the `X-PoW-Solution` header.

### Expo and React Native

For **Expo** and **React Native** apps, use `sentinelNativeClient` from `@better-auth/infra/native` instead of using `@better-auth/infra/client`.

For **React Native** apps, install the following peer dependencies:

```package-install
@react-native-async-storage/async-storage react-native-get-random-values
```

`@react-native-async-storage/async-storage` is optional. If it is not installed, the client uses a session-only in-memory visitor ID. For production, install it or pass a custom `storage` (for example a secure store).

#### Example

```ts
import { createAuthClient } from "better-auth/client";
import { dashClient, sentinelNativeClient } from "@better-auth/infra/native";

export const authClient = createAuthClient({
  baseURL: "https://your-api.example.com",
  plugins: [
    dashClient(),
    sentinelNativeClient({
      autoSolveChallenge: true,
    }),
  ],
});
```

For **Expo** apps, install the packages below for richer native identity payloads.

<Callout type="info">
  **Expo:** Follow the [Expo integration](/docs/integrations/expo) before adding infrastructure plugins.
</Callout>

```package-install
expo-constants expo-device expo-crypto
```

#### Example

```ts
import { createAuthClient } from "better-auth/react";
import { expoClient } from "@better-auth/expo/client";
import { dashClient, sentinelNativeClient } from "@better-auth/infra/native";
import * as SecureStore from "expo-secure-store";

export const authClient = createAuthClient({
  baseURL: "https://your-api.example.com",
  plugins: [
    expoClient({
      scheme: "myapp",
      storagePrefix: "myapp",
      storage: SecureStore,
    }),
    dashClient(),
    sentinelNativeClient({
      autoSolveChallenge: true,
    }),
  ],
});
```

#### `sentinelNativeClient` options

| Option                | Type                            | Default                                                 | Description                                                                 |
| --------------------- | ------------------------------- | ------------------------------------------------------- | --------------------------------------------------------------------------- |
| `identifyUrl`         | `string`                        | `BETTER_AUTH_KV_URL`, then `https://kv.better-auth.com` | KV identify endpoint base URL                                               |
| `autoSolveChallenge`  | `boolean`                       | `true`                                                  | On `423` with `X-PoW-Challenge`, solve and retry once with `X-PoW-Solution` |
| `onChallengeReceived` | `(reason: string) => void`      | —                                                       | Called when a PoW challenge is received                                     |
| `onChallengeSolved`   | `(solveTimeMs: number) => void` | —                                                       | Called after a successful solve                                             |
| `onChallengeFailed`   | `(error: Error) => void`        | —                                                       | Called if solving fails                                                     |
| `storage`             | `{ getItem, setItem }`          | Async Storage when installed                            | Persistent async storage for a stable per-install visitor ID                |

## Security Events

Sentinel tracks the following security event types:

| Event Type                      | Description                         |
| ------------------------------- | ----------------------------------- |
| `security_blocked`              | Request was blocked                 |
| `security_allowed`              | Request was allowed after challenge |
| `security_credential_stuffing`  | Credential stuffing detected        |
| `security_impossible_travel`    | Impossible travel detected          |
| `security_geo_blocked`          | Geo-blocking triggered              |
| `security_bot_blocked`          | Bot detected and blocked            |
| `security_suspicious_ip`        | Suspicious IP detected              |
| `security_velocity_exceeded`    | Rate limit exceeded                 |
| `security_free_trial_abuse`     | Free trial abuse detected           |
| `security_compromised_password` | Compromised password detected       |
| `security_stale_account`        | Stale account reactivation          |

These events are visible in the Security dashboard and included in audit logs.

## Complete Example

```ts
import { betterAuth } from "better-auth";
import { sentinel } from "@better-auth/infra";

export const auth = betterAuth({
  plugins: [
    sentinel({
      apiKey: process.env.BETTER_AUTH_API_KEY,
      security: {
        // Core protections
        credentialStuffing: {
          enabled: true,
          thresholds: { challenge: 3, block: 5 },
        },
        compromisedPassword: {
          enabled: true,
          action: "block",
        },
        emailValidation: {
          enabled: true,
          strictness: "medium",
          action: "block",
        },
        
        // Location-based
        impossibleTravel: {
          enabled: true,
          action: "challenge",
        },
        geoBlocking: {
          denyList: ["XX"],
          action: "block",
        },
        
        // Abuse prevention
        freeTrialAbuse: {
          enabled: true,
          maxAccountsPerVisitor: 3,
          action: "block",
        },
        velocity: {
          enabled: true,
          maxSignupsPerVisitor: 5,
          action: "challenge",
        },
        
        // Bot protection
        botBlocking: { action: "challenge" },
        suspiciousIpBlocking: { action: "block" },
        
        // Account monitoring
        staleUsers: {
          enabled: true,
          staleDays: 90,
          notifyUser: true,
          notifyAdmin: true,
          adminEmail: "security@yourapp.com",
        },
      },
    }),
  ],
});
```

## Best Practices

1. **Start with logging** - Set actions to "log" initially to understand your traffic patterns before blocking.

2. **Tune thresholds** - Every application is different. Monitor false positives and adjust thresholds accordingly.

3. **Use challenges before blocks** - Challenges allow legitimate users through while stopping automated attacks.

4. **Enable client-side auto-solve** - Use `sentinelClient({ autoSolveChallenge: true })` in the browser, or `sentinelNativeClient({ autoSolveChallenge: true })` in Expo / React Native.

5. **Monitor security events** - Regularly review the Security dashboard to identify attack patterns.

6. **Set up admin notifications** - Enable `notifyAdmin` for critical events like stale account reactivations.

## Troubleshooting

### Missing API Key Warning

```
[Sentinel] Missing BETTER_AUTH_API_KEY. Security checks may fall back to allow mode.
```

Make sure your API key environment variable is set correctly.

### Challenges Not Working

If PoW challenges aren't being solved:

1. Verify the correct client plugin: `sentinelClient()` for web (`@better-auth/infra/client`), or `sentinelNativeClient()` for Expo / React Native (`@better-auth/infra/native`)
2. Check that `autoSolveChallenge` is `true`
3. Ensure the client can reach the server

### High False Positive Rate

If legitimate users are being blocked:

1. Increase thresholds
2. Change action from "block" to "challenge"
3. Review security events to identify patterns
