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
Count the flops: For every IP, track how many times reCAPTCHA verification has failed.
Strike threshold: When failures hit, say, 5 attempts in an hour, block that IP for a day.
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(),
});
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
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));
}
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;
}
*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!!: đ¤
Top comments (9)
well explained
thanks!
you are welcome keep posting
super clean setup honestly. you ever wonder if keeping security simple like this is actually better than just stacking more and more layers?
Absolutely â simplicity in security is underrated.
Nice!
đđ
Thanks!
â¤ď¸âđĽâ¤ď¸âđĽ