Korzen
Bot Protection

Better Auth

Better Auth で Bot Protection を使う:パスワードでもソーシャルプロバイダーでも、ユーザーを作成する前に問い合わせ、block なら新規登録を拒否する database hook。

Better Auth は自分のサーバーで動くので、訪問者を新規登録させるリクエストは自分のコードを通ります。databaseHooks.user.create.before フックは、メールとパスワードのフォームからでも、ソーシャルプロバイダーのコールバックからでも、Better Auth がユーザーを作成する前に実行され、リクエストのヘッダーを受け取ります。そこで Bot Protection に問い合わせ、block ならユーザーを拒否します。このガイドは Better Auth 1.x 向けです。

タグ

フレームワークのガイド(Next.js、Express)と同じく、新規登録ページにも入るようにページのレイアウトに入れます。

<script src="https://korzen.dev/v1/tag.js" data-site-key="YOUR_SITE_KEY" async></script>

呼び出し

クライアントは依存関係のない 1 ファイルです。/v1/decide に問い合わせ、設定した timeout で打ち切り、エラーのときは必ず訪問者を通します。Better Auth の設定ファイルの隣にダウンロードし、commit する前に中身を読んでください。

curl -o lib/bot-protection.js https://korzen.dev/bot-protection/sdk/korzen.js
curl -o lib/bot-protection.d.ts https://korzen.dev/bot-protection/sdk/korzen.d.ts # types, for TypeScript

次にフックと、判定の request_id を残すユーザーのフィールドです。

// lib/auth.js
import { betterAuth } from "better-auth";
import { APIError } from "better-auth/api";
import { createClient, fromHeaders, ipFrom } from "./bot-protection.js";

const korzen = createClient({ apiKey: process.env.KORZEN_API_KEY, url: "https://korzen.dev", timeoutMs: 200 });

export const auth = betterAuth({
  // your database, emailAndPassword, socialProviders ...
  user: {
    additionalFields: { korzenRequestId: { type: "string", required: false, input: false } },
  },
  databaseHooks: {
    user: {
      create: {
        before: async (user, ctx) => {
          if (!ctx?.headers) return; // a user your own code creates, outside a request
          const d = await korzen.decide(
            fromHeaders(ctx.headers, {
              ip: ipFrom.vercel, // your platform's: see "The visitor's IP"
              path: "/signup",
              method: ctx.request?.method ?? "POST",
            }),
          );
          if (d?.action === "block") {
            throw new APIError("BAD_REQUEST", { message: "We could not create your account." });
          }
          // "challenge": ask for a phone number, or another check, before the account does anything
          return { data: { ...user, korzenRequestId: d?.request_id ?? null } };
        },
      },
    },
  },
});

最後に npx auth@latest generate(Better Auth 自身の Kysely アダプターなら migrate)を一度実行し、データベースに新しい列を追加します。

訪問者の IP

フックは自分のサーバーで動くので、IP はプラットフォームのガイドのとおりです。Vercel では ipFrom.vercel、サーバーが Cloudflare からのトラフィックしか受け付けないなら ipFrom.cloudflare、自前のロードバランサーや nginx の背後では ipFrom.lastForwarded を使います。訪問者が書き込めるヘッダーを使うと、bot が自分の IP を選べてしまいます。

アクションを適用する

読むのは action だけです。block の場合は、その操作がほかの理由で失敗したときと同じエラーを返し、bot に何も悟らせないようにします。challenge の場合は、すでにある確認手段(CAPTCHA、メールや電話の確認コード)を使います。何もなければそのまま続けます。アカウントは monitor mode で始まり、その間 action は常に allow です。block mode をオンにするまで、訪問者にとっては何も変わりません。

request_id を保存する

すべての応答には request_id が含まれます。操作で作成されたもの(アカウントや注文)のカラムに保存するか、その ID と一緒にログに残してください。お問い合わせの際にその判定を特定でき、後からどの応答が偽アカウントを通したのかを調べられます。

手元のマシンでテストする

サーバーを起動し、localhost で新規登録ページを開きます。localhost のページは常にタグを受け付け、ドメインとして一覧に載ることはありません。ローカルでは IP が 127.0.0.1 または ::1 になりますが、問題ありません。自分のマシンの Docker でアプリを動かすと、代わりにブリッジのアドレス(172.17.0.1 など)になりますが、タグがそこであなたのページを見ているので、チャレンジページは localhost へ戻してくれます。

  1. .env に KORZEN_API_KEY を設定し、新規登録ページを開きます。数秒後、ブラウザーの開発者ツールに _kz Cookie が表示されます。
  2. 新規登録します。判定は dashboard の Decisions ページの /signup に表示され、新しいユーザーには korzenRequestId が入ります。
  3. fail-open を確認します。誤ったキーを設定してもう一度新規登録すると、呼び出しは 401 を返し、新規登録はそのまま完了します。

よくある間違い

  • /sign-up/email だけの hooks.before。Google や GitHub での新規登録はそこを通りません。database hook ならすべての新規ユーザーを見られます。
  • session.create でも問い合わせる。呼び出しのたびに判定 1 件として数えられます。ここでは新規アカウントごとに 1 回で十分です。ログインやチェックアウトはそれぞれのハンドラーで保護してください。
  • ブラウザーから /v1/decide を呼び出す。API key がすべての訪問者に渡ってしまいます。呼び出しはサーバーで行ってください。
  • タイムアウトがない、またはエラーで新規登録が止まる。200 以外の応答や、時間内に応答がない場合は、訪問者を通すという意味です。
  • ログに korzen: the visitor's IP sent to Bot Protection で始まる行が出る。クライアントは、IP があなた自身のネットワークのもの(ロードバランサー、プロキシ、コンテナのもの)である最初の判定で一度だけこれを書きます。Bot Protection はその判定を IP なしとして扱います。ip に訪問者のアドレスを設定してください(訪問者の IP)。自分のマシンの Docker ではブリッジのアドレスで、そこでは問題ありません。