Korzen
Bot Protection

Better Auth

Bot Protection mit Better Auth: ein Database Hook, der fragt, bevor ein Nutzer angelegt wird, mit Passwort oder über einen Social Provider, und die Registrierung bei block ablehnt.

Better Auth läuft auf Ihrem eigenen Server, also geht die Anfrage, die einen Besucher registriert, durch Ihren Code. Ein databaseHooks.user.create.before-Hook läuft, bevor Better Auth einen Nutzer anlegt, ob aus dem Formular mit E-Mail und Passwort oder aus dem Callback eines Social Providers, und bekommt die Header der Anfrage: Dort fragt er Bot Protection und lehnt den Nutzer bei block ab. Diese Anleitung gilt für Better Auth 1.x.

Der Tag

Wie in der Anleitung Ihres Frameworks (Next.js, Express), im Layout Ihrer Seiten, damit die Registrierungsseite ihn hat:

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

Der Aufruf

Der Client ist eine einzige Datei ohne Abhängigkeiten: Er fragt /v1/decide, gibt bei Ihrem Timeout auf und lässt den Besucher bei jedem Fehler durch. Laden Sie ihn neben Ihre Better-Auth-Konfiguration und lesen Sie ihn, bevor Sie ihn committen:

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

Dann der Hook, und ein Feld am Nutzer, das die request_id der Entscheidung aufbewahrt:

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

Dann einmal npx auth@latest generate (oder migrate, mit dem eigenen Kysely-Adapter von Better Auth), damit die Datenbank die neue Spalte hat.

Die IP des Besuchers

Der Hook läuft auf Ihrem Server, also ist die IP die, die die Anleitung Ihrer Plattform nennt: ipFrom.vercel auf Vercel, ipFrom.cloudflare, wenn Ihr Server Traffic ausschließlich von Cloudflare annimmt, ipFrom.lastForwarded hinter Ihrem eigenen Load Balancer oder nginx. Ein Header, den der Besucher schreiben kann, lässt einen Bot seine IP selbst wählen.

Die Aktion anwenden

Lesen Sie nur action. Bei block antworten Sie mit demselben Fehler, den die Aktion zeigt, wenn sie aus einem anderen Grund scheitert, damit ein Bot nichts lernt. Bei challenge nutzen Sie die Prüfung, die Sie schon haben (ein CAPTCHA, ein Code per E-Mail oder Telefon); ohne eine machen Sie weiter. Das Konto beginnt im Monitor-Modus, in dem action immer allow ist: Für Besucher ändert sich nichts, bis Sie den Block-Modus einschalten.

Die request_id aufbewahren

Jede Antwort hat eine request_id. Bewahren Sie sie bei dem auf, was die Aktion erstellt hat, in einer Spalte des Kontos oder der Bestellung, oder in einer Logzeile mit deren ID. Sie benennt diese Entscheidung, wenn Sie uns dazu etwas fragen, und lässt Sie später herausfinden, welche Antwort ein Fake-Konto durchgelassen hat.

Auf Ihrem Rechner testen

Starten Sie Ihren Server und öffnen Sie die Registrierungsseite auf localhost. Seiten auf localhost akzeptieren den Tag immer und erscheinen nie als Domain. Lokal ist die IP 127.0.0.1 oder ::1, und das ist in Ordnung. Läuft die App auf Ihrem Rechner in Docker, sieht sie stattdessen die Adresse der Bridge, etwa 172.17.0.1: Die Challenge-Seite bringt Sie trotzdem zu localhost zurück, denn der Tag hat Ihre Seite dort gesehen.

  1. Setzen Sie KORZEN_API_KEY in .env und öffnen Sie die Registrierungsseite. In den Entwicklertools des Browsers erscheint nach ein paar Sekunden das _kz-Cookie.
  2. Registrieren Sie sich. Die Entscheidung erscheint auf der Seite Decisions des Dashboards unter /signup, und der neue Nutzer hat seine korzenRequestId.
  3. Prüfen Sie fail-open: Setzen Sie einen falschen Schlüssel und registrieren Sie sich erneut. Der Aufruf antwortet 401, und die Registrierung geht durch.

Häufige Fehler

  • Ein hooks.before nur auf /sign-up/email. Registrierungen mit Google oder GitHub erreichen ihn nie; der Database Hook sieht jeden neuen Nutzer.
  • Auch in session.create fragen. Jeder Aufruf zählt als Entscheidung: Hier genügt eine pro neuem Konto; schützen Sie Login und Checkout in ihren eigenen Handlern.
  • /v1/decide aus dem Browser aufrufen. Der API key würde jeden Besucher erreichen; der Aufruf gehört auf Ihren Server.
  • Kein Timeout, oder ein Fehler, der die Registrierung stoppt. Jede andere Antwort als 200, und keine Antwort rechtzeitig, bedeutet: den Besucher durchlassen.
  • Eine Zeile in Ihrem Log, die mit korzen: the visitor's IP sent to Bot Protection beginnt. Der Client schreibt sie einmal, bei der ersten Entscheidung, deren IP aus Ihrem eigenen Netz stammt (die eines Load Balancers, eines Proxys, eines Containers), was Bot Protection wie keine IP wertet: Setzen Sie ip auf die Adresse des Besuchers (die IP des Besuchers). In Docker auf Ihrem Rechner ist es die Adresse der Bridge, und dort harmlos.