Korzen
Bot Protection

Better Auth

Bot Protection com Better Auth: um database hook que pergunta antes de criar qualquer usuário, com senha ou por um provedor social, e recusa o cadastro em block.

O Better Auth roda no seu próprio servidor, então a requisição que cadastra um visitante passa pelo seu código. Um hook databaseHooks.user.create.before roda antes de o Better Auth criar qualquer usuário, seja pelo formulário de email e senha, seja pelo callback de um provedor social, e recebe os cabeçalhos da requisição: ele pergunta ao Bot Protection ali e recusa o usuário em block. Este guia é para o Better Auth 1.x.

A tag

Como no guia do seu framework (Next.js, Express), no layout das suas páginas, para que a página de cadastro a tenha:

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

A chamada

O cliente é um arquivo só, sem dependências: ele pergunta ao /v1/decide, desiste no seu timeout e deixa o visitante passar em qualquer erro. Baixe-o ao lado da sua configuração do Better Auth e leia-o antes do 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

Depois, o hook, e um campo no usuário que guarda o request_id da decisão:

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

Depois, npx auth@latest generate (ou migrate, com o adapter Kysely do próprio Better Auth) uma vez, para o banco ter a nova coluna.

O IP do visitante

O hook roda no seu servidor, então o IP é o que o guia da sua plataforma diz: ipFrom.vercel na Vercel, ipFrom.cloudflare se o seu servidor recebe tráfego apenas do Cloudflare, ipFrom.lastForwarded atrás do seu próprio load balancer ou nginx. Um cabeçalho que o visitante pode escrever deixa o bot escolher o próprio IP.

Siga a action

Leia só a action. Em block, responda com o mesmo erro que a ação mostra quando falha por qualquer outro motivo, para o bot não aprender nada. Em challenge, use a verificação que você já tem (um CAPTCHA, um código por email ou telefone); sem nenhuma, siga em frente. A conta começa em monitor mode, em que a action é sempre allow: nada muda para os visitantes até você ligar o block mode.

Guarde o request_id

Toda resposta tem um request_id. Guarde-o junto do que a ação criou, numa coluna da conta ou do pedido, ou numa linha de log com o id deles. Com ele, você acha a decisão quando falar com a gente e descobre depois qual resposta deixou passar uma conta falsa.

Teste na sua máquina

Rode o seu servidor e abra a página de cadastro em localhost. Páginas em localhost sempre aceitam a tag e nunca entram na lista de domínios. Localmente o IP é 127.0.0.1 ou ::1, e tudo bem. No Docker na sua máquina, o app vê em vez disso o endereço da bridge, como 172.17.0.1: a página de desafio ainda traz você de volta para localhost, porque a tag viu sua página lá.

  1. Defina KORZEN_API_KEY em .env e abra a página de cadastro. Nas ferramentas de desenvolvedor do navegador, o cookie _kz aparece depois de alguns segundos.
  2. Cadastre-se. A decisão aparece na página Decisions do dashboard, em /signup, e o novo usuário tem o seu korzenRequestId.
  3. Confira o fail open: coloque uma chave errada e cadastre-se de novo. A chamada responde 401, e o cadastro passa.

Erros comuns

  • Um hooks.before só em /sign-up/email. Cadastros com Google ou GitHub nunca passam por ele; o database hook vê todo usuário novo.
  • Perguntar também em session.create. Cada chamada conta como uma decisão: aqui basta uma por conta nova; proteja o login e o checkout nos handlers deles.
  • Chamar /v1/decide do navegador. A API key ficaria exposta para todo visitante; a chamada sai do seu servidor.
  • Sem timeout, ou um erro que trava o cadastro. Se a resposta não for 200, ou não chegar a tempo, deixe o visitante passar.
  • Uma linha no seu log que começa com korzen: the visitor's IP sent to Bot Protection. O cliente a escreve uma vez, na primeira decisão cujo IP é da sua própria rede (de um load balancer, de um proxy, de um contêiner), que o Bot Protection julga como sem IP: defina ip como o endereço do visitante (o IP do visitante). No Docker na sua máquina, é o endereço da bridge, e ali não faz mal.