Korzen
Bot Protection

Clerk

Clerk と Bot Protection を組み合わせます。新しいアカウントの最初のページで 1 回判定し、block なら ban して、アプリが確認する印をセッション token に付けます。

Clerk の新規登録はブラウザーから Clerk へ直接送られ、Clerk にはユーザーを作成する前に動くフックがありません。そこで Bot Protection の判定は作成の直後に行います。新しいアカウントが最初に到達するページで 1 回だけ問い合わせ、block なら ban し、確認済みの印を付けます。それまでは、アプリ側でそのアカウントに何もさせません。これは Clerk 自身が紹介している オンボーディングのパターンを Next.js で実装したものです。

タグ

Next.js ガイドと同じく、ルートレイアウトの <ClerkProvider> の内側に置きます。decide 関数もそのガイドのものを使います。

呼び出し

Clerk の dashboard の Sessions で、ユーザーの public metadata をセッション token に追加します。これでサーバーは API を呼ばずに印を読み取れます。

{
  "metadata": "{{user.public_metadata}}"
}

1 回だけ問い合わせてアカウントに印を付ける server action です。

// app/signup/check/actions.js
"use server";
import { auth, clerkClient } from "@clerk/nextjs/server";
import { decide } from "@/lib/korzen"; // from the Next.js guide

// check asks Bot Protection once about a new account, and marks it checked.
export async function check() {
  const { userId } = await auth();
  if (!userId) return "signed-out";
  const client = await clerkClient();
  const user = await client.users.getUser(userId);
  if (user.publicMetadata.korzen) return "done";
  let d = null;
  if (Date.now() - user.createdAt < 24 * 3600_000) {
    // accounts made before you added Bot Protection are marked without a decision
    d = await decide("/signup");
    if (d?.action === "block") {
      await client.users.banUser(userId); // ends its sessions; it cannot sign in again
      return "blocked";
    }
    // "challenge": ask for a phone number, or another check, before going on
  }
  await client.users.updateUserMetadata(userId, {
    publicMetadata: { korzen: { request_id: d?.request_id ?? null } },
  });
  return "done";
}

新しいアカウントが最初に開くページです。NEXT_PUBLIC_CLERK_SIGN_UP_FORCE_REDIRECT_URL=/signup/check でここへ送ります。

// app/signup/check/page.js
"use client";
import { useUser } from "@clerk/nextjs";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { check } from "./actions";

export default function Check() {
  const { isLoaded, user } = useUser();
  const router = useRouter();
  useEffect(() => {
    if (!isLoaded) return;
    (async () => {
      await window.korzen?.ready();
      if ((await check()) !== "done") return router.push("/sign-in");
      await user?.reload(); // a new session token, with the mark in it
      router.push("/dashboard");
    })();
  }, [isLoaded]);
  return <p>One moment…</p>;
}

印のないアカウントをこのページへ戻す middleware です。Clerk はこれを利便性のためのものと位置づけています。費用が発生したりメッセージを送ったりする route handler や server action では、sessionClaims?.metadata?.korzen をもう一度確認してください。

// middleware.js (proxy.js on Next.js 16)
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";

const isOpen = createRouteMatcher(["/", "/sign-in(.*)", "/sign-up(.*)", "/signup/check"]);

export default clerkMiddleware(async (auth, req) => {
  const { userId, sessionClaims } = await auth();
  if (userId && !sessionClaims?.metadata?.korzen && !isOpen(req)) {
    return NextResponse.redirect(new URL("/signup/check", req.url));
  }
});

// keep the config.matcher your Clerk set-up already has

訪問者の IP

判定は server action の中で行われるため、IP は Next.js ガイドの説明どおりです。新規登録時の Clerk 自身の bot 対策はオンのままでかまいません。Clerk のものはフォームを守り、Bot Protection はそのフォームで作られたアカウントを守ります。

アクションを適用する

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

request_id を保存する

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

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

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

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

よくある間違い

  • 印を middleware でしか確認しない。bot は API ルートを直接呼び出します。重要な箇所ではクレームを確認してください。
  • 印を付けた後に user.reload() を呼ばない。セッション token は最大 1 分間古い metadata を保持するため、middleware がアカウントを送り返してしまいます。
  • 訪問のたびに問い合わせる。呼び出しはすべて判定としてカウントされます。印があれば 1 アカウントにつき 1 回で済みます。
  • ブラウザーから /v1/decide を呼び出す。API key がすべての訪問者に渡ってしまいます。呼び出しはサーバーで行ってください。
  • タイムアウトがない、またはエラーで新規登録が止まる。200 以外の応答や、時間内に応答がない場合は、訪問者を通すという意味です。