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.
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.
- Set
LOGINLLAMA_API_KEYto the test key in your development environment. - Sign up in your Clerk dev instance as
[email protected]. - 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.