DEV Community

Cover image for Boost Your App Security with reCAPTCHA and IP-Based Fraud Detection 🛡
Ali nazari
Ali nazari

Posted on

Boost Your App Security with reCAPTCHA and IP-Based Fraud Detection 🛡

Hey folks!

If you’re running any public-facing web app—think login screens, sign-up pages, and password reset forms—you’ve probably already slapped on reCAPTCHA to fend off bots.

But what happens when a sneaky attacker just keeps spamming wrong tokens or dodges your checks?

That’s where throwing in some fraud detection around reCAPTCHA can really save the day.

Why Bother with Fraud Detection on Top of reCAPTCHA?

  • Glitches Happen: Maybe a user’s connection hiccups or your server hiccups—single reCAPTCHA failures shouldn’t ban them for good.

  • Bot Farms Are Sneaky: Even if they solve the widget once, a bot network can rotate IPs and hammer your endpoints nonstop.

  • Layered Defense: reCAPTCHA is great, but count-and-block on repeated failures makes your security way tighter.

The Gist

  1. Count the flops: For every IP, track how many times reCAPTCHA verification has failed.

  2. Strike threshold: When failures hit, say, 5 attempts in an hour, block that IP for a day.

  3. Forgive on success: One valid reCAPTCHA resets the counter—your users aren’t punished forever.

Code Walkthrough

We’ll break the implementation into four bite-sized parts:

Google Response Schema

Validate exactly what Google sends back so we never trust untyped JSON.

import { z } from "zod";

const SiteVerify = z.object({
  success:      z.boolean(),
  "error-codes": z.array(z.string()).optional(),
});
Enter fullscreen mode Exit fullscreen mode

Redis Setup & Configuration

Define your Redis keys and tuning parameters for failure counts and block windows.

import Redis from "ioredis";

const redis       = new Redis();
const FAIL_KEY    = (ip: string) => `recap:fail:${ip}`;
const BLOCK_KEY   = (ip: string) => `recap:block:${ip}`;

// Tweak these to suit your traffic and risk tolerance
const MAX_FAILS   = 5;            // block after 5 fails
const FAIL_WINDOW = 60 * 60;      // 1-hour rolling window
const BLOCK_TIME  = 24 * 60 * 60; // block duration: 24 hours
Enter fullscreen mode Exit fullscreen mode

Helper Functions

Encapsulate common operations: check if blocked, record a failure, and reset on success.

// 3a) Quick block check
async function isBlocked(ip: string): Promise<boolean> {
  return (await redis.exists(BLOCK_KEY(ip))) === 1;
}

// 3b) Record a failure and block if threshold exceeded
async function recordFailure(ip: string): Promise<void> {
  const key = FAIL_KEY(ip);
  const count = await redis.incr(key);
  if (count === 1) await redis.expire(key, FAIL_WINDOW);
  if (count >= MAX_FAILS) {
    await redis.set(BLOCK_KEY(ip), "1", "EX", BLOCK_TIME);
    console.warn(`🔒 IP ${ip} blocked for ${BLOCK_TIME}s after ${count} fails`);
  }
}

// 3c) Reset on successful verify
async function resetFailures(ip: string): Promise<void> {
  await redis.del(FAIL_KEY(ip));
}

Enter fullscreen mode Exit fullscreen mode

The Main Verification Function

Bring it all together in one function you can plug into any endpoint.

import fetch from "node-fetch";
import env from "../env";

export async function verifyWithGoogleWithFraud(
  token:    string,
  remoteIp: string
): Promise<boolean> {
  // a) Block check
  if (await isBlocked(remoteIp)) return false;

  // b) Call Google’s siteverify API
  const params = new URLSearchParams({
    secret:   env.RECAPTCHA_SECRET_KEY,
    response: token,
  });
  params.append("remoteip", remoteIp);

  let data: unknown;
  try {
    const res = await fetch(
      "https://www.google.com/recaptcha/api/siteverify",
      { method: "POST", body: params }
    );
    data = await res.json();
  } catch (err) {
    console.error("[recap] network oops:", err);
    await recordFailure(remoteIp);
    return false;
  }

  // c) Parse + validate schema
  const parsed = SiteVerify.safeParse(data);
  if (!parsed.success) {
    console.error("[recap] bad response:", parsed.error);
    await recordFailure(remoteIp);
    return false;
  }

  // d) Check success flag
  if (!parsed.data.success) {
    console.warn("[recap] token rejected:", parsed.data["error-codes"]);
    await recordFailure(remoteIp);
    return false;
  }

  // e) Success → reset failures
  await resetFailures(remoteIp);
  return true;
}

Enter fullscreen mode Exit fullscreen mode

*What’s cool about this? *

  • Dead simple: no extra classes or complicated wiring—just plain functions you can test easily.

  • Zero false positives: only blocks after repeated fails, and resets on success.

  • Tunable: tweak your MAX_FAILS, FAIL_WINDOW, and BLOCK_TIME to your heart’s content.

  • Easy logging: every block, fail, and success logs meaningful info.

pairing reCAPTCHA with a quick Redis counter gives you a neat, lightweight fraud-detection layer on top of Google’s anti-bot service.

It’s a tiny bit of extra code, but it pays off by turning one-off failures into friendly warnings while still walling off real abuse. Give it a spin and let me know how it goes!


Let's connect!!: 🤝

LinkedIn
GitHub

Top comments (9)

Collapse
 
shifa_2 profile image
Shifa

well explained

Collapse
 
silentwatcher_95 profile image
Ali nazari

thanks!

Collapse
 
shifa_2 profile image
Shifa

you are welcome keep posting

Collapse
 
nevodavid profile image
Nevo David

super clean setup honestly. you ever wonder if keeping security simple like this is actually better than just stacking more and more layers?

Collapse
 
silentwatcher_95 profile image
Ali nazari

Absolutely — simplicity in security is underrated.

Collapse
 
northstar_toolkit profile image
Northstar Toolkit

Nice!

Collapse
 
silentwatcher_95 profile image
Ali nazari

💚💚

Collapse
 
lovit_js profile image
Lovit

Thanks!

Collapse
 
silentwatcher_95 profile image
Ali nazari

❤️‍🔥❤️‍🔥