---
title: "Bot Protection"
url: "https://effect-auth.itsbroly.com/authentication/abuse-protection/bot-protection/"
description: "Verify endpoint-local browser challenges with explicit outage policy."
---



`BotChallengeVerifier` is the replaceable provider port. `BotProtectionDecision` selects skip/verify, expected action, hostname allowlist, and outage behavior.

:::caution[Provider installation is not enforcement]
Named guards skip bot verification by default. Providing `CloudflareTurnstile.layer(...)` does not modify `CoreAuthHttpApiLive` or standard operations.
:::

## Protect an endpoint [#protect-an-endpoint]

`.withPolicy(...)` creates an alternative guard; invoke it from an app-owned primitive handler. Exporting it alone changes nothing.

```ts title="src/server/password-sign-in-guard.ts"
import { BotProtection } from "@effect-auth/core/AbuseProtection";
import {
  PasswordGuards,
  type PasswordSignInGuardRequest,
} from "@effect-auth/core/HttpApi";

const guard = PasswordGuards.signIn.withPolicy({
  botProtection: BotProtection.verify({
    action: "password-sign-in",
    allowedHostnames: ["app.example.com"],
    outageMode: "fail-closed",
  }),
});

export const guardPasswordSignIn = (request: PasswordSignInGuardRequest) =>
  guard(request);
```

The result is redacted domain input after standard rate limiting, bot verification, and login enrichment. This seam is for a custom primitive handler, which must also preserve authentication continuations, public error mapping, session/cookie commitment, and auditing. There is currently no policy injection option for the standard password preset.

The browser must render/execute the Turnstile widget with the exact action and send `{ botChallenge: { token } }`. Host allowlists contain hostnames only, without scheme, port, or path; matching is normalized to lowercase.

| Guard family     | Supported operations                      |
| ---------------- | ----------------------------------------- |
| `PasswordGuards` | Sign in/up, reset start/verify            |
| `EmailGuards`    | Verification, OTP, email auth, Magic Link |
| `IdentityGuards` | Authenticated availability                |

Combined email start verifies one `email-auth` proof before creating OTP and Magic Link challenges. Do not verify the token again in both child flows.

## Configure Turnstile on Cloudflare [#configure-turnstile-on-cloudflare]

Bind the secret with Alchemy v2:

```ts title="alchemy.run.ts"
import * as Cloudflare from "alchemy/Cloudflare";
import * as Config from "effect/Config";

const AuthWorker = Cloudflare.Worker("AuthWorker", {
  main: "./src/workers/auth-backend.ts",
  env: {
    TURNSTILE_SECRET: Config.redacted("TURNSTILE_SECRET"),
  },
});
```

Create the runtime Layer from the Worker binding:

```ts title="src/server/turnstile.live.ts"
import * as CloudflareTurnstile from "@effect-auth/core/CloudflareTurnstile";
import * as Redacted from "effect/Redacted";

export interface BotEnv {
  readonly TURNSTILE_SECRET: string;
}

export const makeBotVerifierLive = (env: BotEnv) =>
  CloudflareTurnstile.layer({
    secret: Redacted.make(env.TURNSTILE_SECRET),
    timeoutMs: 2_000,
    retry: false,
  });
```

The adapter calls Cloudflare's fixed HTTPS `siteverify` endpoint, validates action/hostname, bounds token and response sizes, redacts the secret, rejects redirects, and applies a timeout. Retry is off by default; when enabled, one retry reuses an idempotency key.

## Request ordering [#request-ordering]

```mermaid title="A named password guard rejects cheap abuse before authentication work"
flowchart LR
  D[Decode and origin] --> L[Rate limit]
  L --> B[Bot verification]
  B --> E[Login enrichment]
  E --> A[Authentication]
```

Email-verification start is different: validate session, apply IP/user limit, resolve identity, apply email/principal/challenge limits, then verify the optional proof. Identity availability validates the session before its user-keyed limit. Trusted subjects never come from browser payloads.

| Outage mode   | Retryable provider outage | Invalid/missing proof | Configuration failure |
| ------------- | ------------------------- | --------------------- | --------------------- |
| `fail-open`   | Continue                  | Reject                | Fail safely           |
| `fail-closed` | Fail safely               | Reject                | Fail safely           |

Fail-open applies only to errors tagged retryable. HTTP guards map invalid proof to `request_rejected` and provider/configuration failure to `internal_error`; never expose provider codes or payloads.

Test exact action/hostname, missing proof, rate-limit short-circuit, malformed/oversized response, timeout, outage modes, combined-email single verification, and deployed edge metadata trust. Never log proof tokens, secrets, remote IPs, or provider payloads.

