Suspicious Login Detection for Clerk

Score every Clerk sign-in for risk from a session.created webhook, and revoke the risky sessions within seconds. One package, three environment variables.

guidesclerk

Clerk checks the password. It does not ask whether the login looks right. A correct password from a Tor exit node, a country the user has never been to, or a bot that has just tried four thousand other accounts still gets a session.

Clerk sends a session.created webhook each time someone signs in. That is where to ask LoginLlama for a risk score. The webhook arrives a second or two after the session exists, so instead of refusing the sign-in you revoke the session, and the user is signed out on their next request.

What you need

  • A Clerk app, and your Clerk secret key.
  • A LoginLlama API key. New accounts get 1,000 free checks.

Install the package

npm install clerk-loginllama

In Next.js App Router, make app/api/webhooks/clerk/route.ts:

import { loginLlamaWebhook } from 'clerk-loginllama';

export const POST = loginLlamaWebhook();

It takes a standard Request, so it mounts anywhere that has one. In Hono:

const handler = loginLlamaWebhook();
app.post('/webhooks/clerk', (c) => handler(c.req.raw));

If you protect routes with clerkMiddleware, leave the webhook path public. Clerk calls it without a session.

Point Clerk at it

In the Clerk dashboard, open Webhooks, add an endpoint at your URL, and subscribe it to session.created. Copy the signing secret it shows you.

Then set three variables:

LOGINLLAMA_API_KEY=your-loginllama-key
CLERK_SECRET_KEY=sk_live_...
CLERK_WEBHOOK_SIGNING_SECRET=whsec_...

That's the whole setup.

How it works

When the webhook arrives, the handler checks Clerk's signature and turns away anything unsigned. The event names the session, but Clerk fills in where the sign-in came from on the session it serves from its API, so the handler fetches that and the user. It sends LoginLlama the user ID, primary email, IP address and browser.

LoginLlama returns a score from 0 to 10 with reason codes such as known_tor_exit_node, known_vpn, impossible_travel_detected, new_login_location and known_bot. At 8 or above the handler revokes the session.

Each sign-in also teaches LoginLlama which devices and places are normal for that user, so the next score is better. It shows up in the dashboard with its score and reasons, and LoginLlama can email the user about a login from somewhere new.

Two choices are yours to change.

The threshold. LoginLlama calls anything above 5 risky. The package revokes at 8, so you only act on the clearest cases at first. Pass loginLlamaWebhook({ blockAt: 6 }) once you have watched real scores for a week.

Failing open. If LoginLlama is down or takes longer than two seconds, the session stays. A fraud check should never lock your users out because of an outage on our side or yours.

Why a webhook and not a check before sign-in

Clerk runs the sign-in form on its own servers, so your code is not in the path before the session exists. The webhook is the earliest point you get. The risky user holds a session for the second or two it takes, which is why this works best alongside Clerk's own bot protection rather than in place of it.

If you need a hard block before the session, put LoginLlama behind your own sign-in endpoint with the Node SDK instead.

Test it without a real attack

Every team has a test key on the API keys page. Test checks are free, and they take their answer from tags in the email address rather than from the real IP address.

  1. Set LOGINLLAMA_API_KEY to the test key in your development environment.
  2. Sign up in your Clerk dev instance as [email protected].
  3. Sign in. The webhook fires, LoginLlama answers 9, and the session is revoked.

[email protected] returns that reason code instead. The full list of tags is in the test mode docs.

Or write it yourself

The package is about 90 lines and MIT licensed, on GitHub. If you'd rather own the code, copy it into your webhook route. It needs @clerk/backend and fetch, nothing else.

Create a free account to get an API key.