Auth0 runs an Action before it creates a user who signs up with a password or a passwordless code (a Database or Passwordless connection), and that Action can call Bot Protection and refuse the sign-up. The sign-up page is Auth0's, on Auth0's domain, so the Action has no _kz cookie: the decision stands on the IP and the User-Agent.
The tag
Put the tag on your own pages, where people arrive before they sign up and where they sign in and pay, so those decisions have the token:
<script src="https://korzen.dev/v1/tag.js" data-site-key="YOUR_SITE_KEY" async></script>
The call
The Action. Auth0 gives it 20 seconds in all; the call keeps to your timeout, and anything else lets the sign-up through:
// Auth0 Dashboard: Actions, Triggers, pre-user-registration; a new Action. // Add the secret KORZEN_API_KEY to the Action. exports.onExecutePreUserRegistration = async (event, api) => { let d = null; try { const res = await fetch("https://korzen.dev/v1/decide", { method: "POST", headers: { Authorization: `Bearer ${event.secrets.KORZEN_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ ip: event.request.ip, user_agent: event.request.user_agent, ja4: event.security_context?.ja4 || undefined, // behind Cloudflare only: left out without it headers: { "accept-language": event.request.language }, path: "/signup", method: "POST", }), signal: AbortSignal.timeout(200), }); if (res.ok) d = await res.json(); } catch {} // fail open if (d?.action === "block") { api.access.deny("korzen_block", "We could not create your account."); return; } // "challenge": Auth0's own CAPTCHA or MFA, if your plan has them if (d?.request_id) api.user.setAppMetadata("korzen_request_id", d.request_id); };
Auth0 gives the Action the browser's language and User-Agent but none of the request's other headers, so the call forwards accept-language alone: the headers one browser build sends alike, which Bot Protection reads in your own server's calls, are missing here.
On a tenant whose traffic comes through Cloudflare (Auth0's Enterprise plans), the event also has security_context.ja4: the JA4 of the visitor's TLS handshake with Auth0's edge, the same form Bot Protection reads from Cloudflare in front of your own server. The call sends it as ja4 when it is there; without it, it is left out.
The visitor's IP
event.request.ip is the address that reached Auth0, already the visitor's: nothing to set up. On a custom domain behind your own proxy, see Auth0's custom domain settings for the visitor's IP.
Auth0's decisions come without the tag's token. Once Bot Protection has seen your tag on other pages, a missing token adds a little to the score (no_token), never enough for bot on its own.
Apply the action
Read only action. On block, answer with the same error the action shows when it fails for any other reason, so a bot learns nothing. On challenge, use the check you already have (a CAPTCHA, an email or phone code); with none, go on. The account starts in monitor mode, where action is always allow: nothing changes for visitors until you turn on block mode.
Keep the request_id
Every answer has a request_id. Keep it with what the action created, in a column of the account or the order, or in a log line with their id. It names that decision when you ask us about it, and lets you find out later which answer let a fake account in.
Test it on your machine
Use your development tenant. In the Action editor, Test runs it with a sample event; then sign up on your login page, and the decision shows on the dashboard's Decisions page under /signup. Check fail open with a wrong KORZEN_API_KEY secret: the sign-up goes through.
Common mistakes
- Social sign-ups. Google, GitHub and the like do not run the pre-user-registration trigger; a post-login Action can ask Bot Protection about them on their first sign-in.
- Calling
/v1/decidefrom the browser. The API key would reach every visitor; the call belongs on your server. - No timeout, or an error that stops the sign-up. Any answer other than
200, and no answer in time, means: let the visitor through.