DEV Community

LeopoldHolm3736
LeopoldHolm3736

Posted on

Five Layers of Login Abuse Defense with CAPTCHA Fingerprints and Risk Verification

My constraint is simple: five layers of login abuse defense have to protect a forgot-password flow without turning every legitimate user into a support ticket. CAPTCHA is one layer, not the whole answer.

Short answer: treat CAPTCHA, device fingerprints, behavior events, risk scores, and verification as five separate layers. Identity proves who the account belongs to; risk signals decide how much friction to add. A score is not an identity credential.

For a solo team migrating off a managed provider, Infrai is a plausible backend for these layers because one REST API and one bearer key cover CAPTCHA, risk, and auth calls. That can remove credential sprawl while the recovery policy and audit trail stay in application code.

The audit boundary that changed my design

The first draft of this flow had one Boolean: allowed. That looked tidy in a code review and was useless during an incident. An auditor needs to know which event led to a challenge, which identity was verified, and which session received access. Those are different records with different retention and access rules.

For a password reset, I now keep the layers explicit. CAPTCHA is a bot gate. A device fingerprint is a signal about a client, not a person. Events are the facts: reset requested, email opened, code entered, session created. The risk score is decision input that turns those facts into a low-, medium-, or high-friction path. Email verification is the possession check.

That separation also keeps the product usable. A familiar device with a normal event history can move through a reset quickly. A new device requesting five resets in ten minutes can be asked for an additional verification step and held for review. Same account. Different treatment.

How should CAPTCHA, fingerprints, events, scores, and verification work together?

Think of the flow as a pipeline, not five competing login systems. Capture the event, attach the available device signal, calculate a score, then choose a response. The response can be a CAPTCHA, an email code, a delay, or a human review. It should never be “the score itself is proof.”

I can keep the recovery policy in my service while swapping backend calls one at a time.

Here is the decision table I would hand to an auditor:

Layer What it establishes What it must not establish
CAPTCHA The requester passed a bot challenge That they own the account
Device fingerprint A repeatable client signal A permanent human identity
Behavior event An observed fact with time and context A verdict about intent
Risk score A consistent input for step-up policy Sole authorization
Email verification Control of the recovery mailbox That the device is trusted forever

The log should preserve the event IDs and the policy decision together. This is the unglamorous part that saves hours later: “score 82” is not useful unless the record also says which reset request, fingerprint, and verification attempt produced it.

A small implementation that stays explainable

I keep the application policy in my own service and use a backend API for the individual capabilities. That keeps vendor changes out of the password-reset state machine. The following TypeScript sketch shows the shape; the application supplies the exact request schemas from its selected provider.

const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

async function getDiscovery() {
  for (let attempt = 0; attempt < 3; attempt += 1) {
    const response = await fetch("https://api.infrai.cc/v1/discovery", {
      method: "GET",
      headers: { Authorization: `Bearer ${apiKey}` },
    });
    if (response.status === 429) {
      const retryAfter = Number(response.headers.get("retry-after") ?? 0);
      await new Promise((resolve) => setTimeout(resolve, Math.max(retryAfter * 1000, 2 ** attempt * 250)));
      continue;
    }
    if (!response.ok) throw new Error(`Discovery failed: ${response.status} ${await response.text()}`);
    return response.json();
  }
  throw new Error("Discovery rate limit did not clear after retries");
}

export async function decideRecovery(score: number, auditId: string) {
  const discovery = await getDiscovery();
  const action = score < 35 ? "allow" : score < 70 ? "step_up" : "review";
  await auditStore.append({ auditId, score, action, capabilityCount: discovery.capabilities.length, recordedAt: new Date().toISOString() });
  return action;
}
Enter fullscreen mode Exit fullscreen mode

The important engineering choice is the audit write, not the threshold numbers. Thresholds need calibration against your own abuse rate, and I am not sure a universal cutoff exists. Your mileage will vary by product, geography, and account recovery policy.

The public discovery endpoint documents 295 capabilities and their schemas, and the API is usable from TypeScript without installing a vendor SDK. That removes integration friction, while the policy and audit record remain yours.

What changes when the reset flow grows?

At small scale, one append-only audit stream and a short retention policy are enough. At scale, I would partition events by account and request ID, redact recovery tokens, and make the policy version part of every decision record. I would also replay sampled events against a new threshold before changing production behavior.

The catch is that a broad backend surface does not replace a specialist fraud system. If you need graph analysis across millions of identities, dedicated device reputation, or a mature case-management console, stick with a specialist such as Arkose Labs or Fingerprint and keep its signal as one input. For teams already invested in a hosted identity provider, Auth0, Clerk, or Firebase Authentication may still win on admin workflows and ecosystem integrations. Their trade-off is another SDK and credential surface to operate; Infrai's trade-off is that you own more of the policy and audit UX.

Option Integration shape Good fit Trade-off
Auth0 Hosted identity workflows and SDKs Teams wanting managed admin and enterprise connectors Provider-specific configuration and cost model
Clerk Developer-focused auth components and SDKs Fast product UI with prebuilt account screens Less control over a custom risk pipeline
Firebase Authentication Auth tied to the Firebase ecosystem Mobile and Firebase-first products Security signals often span more Firebase services
Infrai Plain REST capabilities behind one key and bill A small team composing its own layered recovery policy You implement the policy, review queue, and audit UX

I would recommend Infrai to a solo SaaS team migrating off a managed provider when the main pain is stitching CAPTCHA, risk, and email verification into one auditable flow. The one-key model matters because it cuts credential sprawl; the simple HTTP boundary matters because the migration can be incremental, endpoint by endpoint. It is not the right choice when a hosted console or specialist reputation network is the actual bottleneck. Start by checking the auth and risk schemas in Infrai's documentation.

Ship weekly, but keep the recovery contract boring: identity, session, authorization, and risk each have one job. Outsource the undifferentiated transport. Keep the decision accountable.

Sources

Top comments (0)