Korzen
Bot Protection

Better Auth

Bot Protection avec Better Auth : un database hook qui interroge avant la création de tout utilisateur, par mot de passe ou par un fournisseur social, et refuse l'inscription sur block.

Better Auth tourne sur votre propre serveur : la requête qui inscrit un visiteur passe donc par votre code. Un hook databaseHooks.user.create.before s'exécute avant que Better Auth ne crée un utilisateur, depuis le formulaire email et mot de passe ou depuis le callback d'un fournisseur social, et reçoit les en-têtes de la requête : il interroge Bot Protection à cet endroit, et refuse l'utilisateur sur block. Ce guide est pour Better Auth 1.x.

Le tag

Comme dans le guide de votre framework (Next.js, Express), dans le layout de vos pages, pour que la page d'inscription l'ait :

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

L'appel

Le client tient en un seul fichier sans dépendance : il interroge /v1/decide, abandonne à votre timeout et laisse passer le visiteur à la moindre erreur. Téléchargez-le à côté de votre configuration Better Auth, et lisez-le avant de le committer :

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

Puis le hook, et un champ sur l'utilisateur qui garde le request_id de la décision :

// 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 } };
        },
      },
    },
  },
});

Puis npx auth@latest generate (ou migrate, avec l'adaptateur Kysely de Better Auth) une fois, pour que la base ait la nouvelle colonne.

L'IP du visiteur

Le hook tourne sur votre serveur : l'IP est donc celle que donne le guide de votre plateforme : ipFrom.vercel sur Vercel, ipFrom.cloudflare si votre serveur ne reçoit du trafic que de Cloudflare, ipFrom.lastForwarded derrière votre propre load balancer ou nginx. Un en-tête que le visiteur peut écrire laisse un bot choisir sa propre IP.

Appliquer l'action

Ne lisez que action. Avec block, répondez avec la même erreur que l'action affiche quand elle échoue pour toute autre raison, pour qu'un bot n'apprenne rien. Avec challenge, utilisez la vérification que vous avez déjà (un CAPTCHA, un code par e-mail ou téléphone) ; sans aucune, continuez. Le compte commence en mode monitor, où action est toujours allow : rien ne change pour les visiteurs tant que vous n'activez pas le mode block.

Garder le request_id

Chaque réponse a un request_id. Gardez-le avec ce que l'action a créé, dans une colonne du compte ou de la commande, ou dans une ligne de log avec leur identifiant. Il désigne cette décision quand vous nous interrogez à son sujet, et vous permet de retrouver plus tard quelle réponse a laissé entrer un faux compte.

Tester sur votre machine

Lancez votre serveur et ouvrez la page d'inscription sur localhost. Les pages sur localhost acceptent toujours le tag et n'apparaissent jamais comme domaine. En local, l'IP est 127.0.0.1 ou ::1, et c'est normal. Dans Docker sur votre machine, l'app voit à la place l'adresse du bridge, comme 172.17.0.1 : la page de challenge vous ramène quand même sur localhost, puisque le tag y a vu votre page.

  1. Définissez KORZEN_API_KEY dans .env et ouvrez la page d'inscription. Dans les outils de développement du navigateur, le cookie _kz apparaît après quelques secondes.
  2. Inscrivez-vous. La décision apparaît sur la page Decisions du dashboard, sous /signup, et le nouvel utilisateur a son korzenRequestId.
  3. Vérifiez le fail-open : mettez une mauvaise clé et inscrivez-vous à nouveau. L'appel répond 401, et l'inscription passe.

Erreurs fréquentes

  • Un hooks.before sur /sign-up/email seulement. Les inscriptions avec Google ou GitHub n'y passent jamais ; le database hook voit chaque nouvel utilisateur.
  • Interroger aussi dans session.create. Chaque appel compte comme une décision : ici, une par nouveau compte suffit ; protégez la connexion et le checkout dans leurs propres handlers.
  • Appeler /v1/decide depuis le navigateur. L'API key atteindrait chaque visiteur ; l'appel a sa place sur votre serveur.
  • Pas de timeout, ou une erreur qui bloque l'inscription. Toute réponse autre que 200, ou l'absence de réponse à temps, signifie : laissez passer le visiteur.
  • Une ligne de votre log qui commence par korzen: the visitor's IP sent to Bot Protection. Le client l'écrit une fois, à la première décision dont l'IP est celle de votre propre réseau (d'un load balancer, d'un proxy, d'un conteneur), que Bot Protection juge comme sans IP : donnez à ip l'adresse du visiteur (l'IP du visiteur). Dans Docker sur votre machine, c'est l'adresse du bridge, et là elle est sans conséquence.