Security Plugin (sentinel)
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
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
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;
emailNormalization?: { enabled?: boolean };
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.
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:
- Tracks failed login attempts per visitor ID
- After reaching the challenge threshold, issues a Proof-of-Work challenge
- After reaching the block threshold, blocks the visitor entirely
- Automatically clears failed attempts on successful login
Impossible Travel Detection
Detects logins from geographically distant locations in impossibly short timeframes.
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.
sentinel({
apiKey: process.env.BETTER_AUTH_API_KEY,
security: {
freeTrialAbuse: {
enabled: true,
thresholds: {
challenge: 2,
block: 3,
},
maxAccountsPerVisitor: 3,
action: "block",
},
},
}),How it works:
- Tracks account creations per visitor fingerprint
- When threshold is exceeded, blocks new account creation
- Useful for preventing free tier abuse
Compromised Password Detection
Checks passwords against the HaveIBeenPwned database to detect compromised credentials.
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.
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.
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.
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.
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.
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.
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 domainsmedium- Also check for valid MX recordshigh- Additional heuristic checks
Email normalization
Sentinel can normalize email addresses before sign-up and sign-in so aliases and provider quirks do not create duplicate accounts or mismatched logins. Normalization includes lowercasing, stripping plus-address tags on common providers (for example user+tag@gmail.com → user@gmail.com), removing dots in Gmail-style addresses, and mapping googlemail.com to gmail.com.
Use security.emailNormalization when you want to control this separately from disposable-domain validation (emailValidation):
sentinel({
apiKey: process.env.BETTER_AUTH_API_KEY,
security: {
emailValidation: {
enabled: false, // skip disposable / MX checks
},
emailNormalization: {
enabled: true, // still normalize for deduplication and consistent sign-in
},
},
}),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
- Server issues a cryptographic challenge
- Client must find a solution that satisfies difficulty requirements
- Solution is computationally expensive but verification is fast
- Prevents automated attacks while allowing legitimate users through
Challenge Difficulty
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.
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:
npm 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
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.
Expo: Follow the Expo integration before adding infrastructure plugins.
npm install expo-constants expo-device expo-cryptoExample
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
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
-
Start with logging - Set actions to "log" initially to understand your traffic patterns before blocking.
-
Tune thresholds - Every application is different. Monitor false positives and adjust thresholds accordingly.
-
Use challenges before blocks - Challenges allow legitimate users through while stopping automated attacks.
-
Enable client-side auto-solve - Use
sentinelClient({ autoSolveChallenge: true })in the browser, orsentinelNativeClient({ autoSolveChallenge: true })in Expo / React Native. -
Monitor security events - Regularly review the Security dashboard to identify attack patterns.
-
Set up admin notifications - Enable
notifyAdminfor 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:
- Verify the correct client plugin:
sentinelClient()for web (@better-auth/infra/client), orsentinelNativeClient()for Expo / React Native (@better-auth/infra/native) - Check that
autoSolveChallengeistrue - Ensure the client can reach the server
High False Positive Rate
If legitimate users are being blocked:
- Increase thresholds
- Change action from "block" to "challenge"
- Review security events to identify patterns