Korzen
Bot Protection

Clerk

Bot Protection con Clerk: una decisione sulla prima pagina di un nuovo account, un ban su block, e un segno nel session token che la tua app controlla.

La registrazione di Clerk va dal browser a Clerk, e Clerk non ha nessun hook che giri prima di creare un utente. Quindi Bot Protection decide subito dopo: la prima pagina che un nuovo account raggiunge lo valuta una volta, lo banna su block e lo segna come verificato; fino ad allora, la tua app non gli lascia fare niente. È lo stesso pattern di onboarding di Clerk, con Next.js.

Il tag

Come nella guida Next.js, nel root layout, dentro <ClerkProvider>. Anche la funzione decide è quella della guida.

La chiamata

Nella dashboard di Clerk, in Sessions, aggiungi i public metadata dell'utente al session token, così il tuo server legge il segno senza una chiamata API:

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

La server action che chiede una volta e segna l'account:

// 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";
}

La pagina su cui arrivano i nuovi account. Mandaceli con 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>;
}

Il middleware che rimanda lì un account non segnato. Clerk lo considera una comodità: in una route handler o server action che spende soldi o invia messaggi, controlla di nuovo 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

L'IP del visitatore

La decisione gira nella tua server action, quindi per l'IP vale quanto dice la guida Next.js. La bot protection di Clerk sulla registrazione può restare attiva: protegge il form di Clerk, e Bot Protection l'account che ne è nato.

Applica l'azione

Leggi solo action. Su block, rispondi con lo stesso errore che l'azione mostra quando fallisce per qualsiasi altro motivo, così il bot non impara niente. Su challenge, usa la verifica che hai già (un CAPTCHA, un codice via email o telefono); se non ne hai, prosegui. L'account parte in monitor mode, dove action è sempre allow: per i visitatori non cambia niente finché non attivi il block mode.

Conserva il request_id

Ogni risposta ha un request_id. Conservalo insieme a ciò che l'azione ha creato, in una colonna dell'account o dell'ordine, o in una riga di log con il loro id. Identifica quella decisione quando ci chiedi informazioni, e ti permette di scoprire in seguito quale risposta ha fatto entrare un account falso.

Provalo sulla tua macchina

Avvia next dev con le chiavi di sviluppo di Clerk e apri http://localhost:3000. Le pagine su localhost accettano sempre il tag e non compaiono mai tra i domini. In locale l'IP è 127.0.0.1 o ::1, e va bene così. In Docker sulla tua macchina, l'app vede invece l'indirizzo del bridge, come 172.17.0.1: la pagina di challenge ti riporta comunque su localhost, perché il tag ha visto lì la tua pagina.

  1. Imposta KORZEN_API_KEY in .env.local e apri la pagina di registrazione. Negli strumenti per sviluppatori del browser, il cookie _kz compare dopo qualche secondo.
  2. Registrati. La decisione compare nella pagina Decisioni della dashboard, sotto il path che hai inviato.
  3. Verifica il fail-open: imposta una chiave sbagliata e registrati di nuovo. La chiamata risponde 401, e la registrazione va a buon fine.

Errori comuni

  • Il segno controllato solo nel middleware. Un bot chiama direttamente le tue API route: controlla il claim dove conta.
  • Nessun user.reload() dopo il segno. Il session token mantiene i vecchi metadata fino a un minuto, e il middleware rimanda indietro l'account.
  • Chiedere di nuovo a ogni visita. Ogni chiamata conta come decisione: il segno fa sì che avvenga una volta per account.
  • Chiamare /v1/decide dal browser. L'API key arriverebbe a ogni visitatore; la chiamata va fatta dal tuo server.
  • Nessun timeout, o un errore che blocca la registrazione. Qualsiasi risposta diversa da 200, o nessuna risposta in tempo, significa: lascia passare il visitatore.