[GH-ISSUE #5616] Automatic IP Detection in Captcha Plugin #27626

Closed
opened 2026-04-17 18:43:59 -05:00 by GiteaMirror · 3 comments
Owner

Originally created by @issam-seghir on GitHub (Oct 27, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/5616

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Summary

The captcha plugin should automatically extract the user's IP address from request headers (similar to how rate limiting works) instead of requiring client-side IP detection and manual header passing.

Current Behavior

Currently, the captcha plugin requires developers to manually pass the user's IP via the x-captcha-user-remote-ip header:

// captcha/index.ts (line 23-24)
const remoteUserIP =
    request.headers.get("x-captcha-user-remote-ip") ?? undefined;

This forces a client-side workaround:

// Client-side (insecure approach)
const response = await fetch("/api/auth/get-ip"); // Extra API call needed
const { ip } = await response.json();

await authClient.signIn.email({
    email: "user@example.com",
    password: "password",
    fetchOptions: {
        headers: {
            "x-captcha-response": captchaToken,
            "x-captcha-user-remote-ip": ip, // Manually passed from client
        },
    },
});

Problems with Current Approach

  1. Security Risk: IP detection happens client-side, can be manipulated
  2. Extra API Calls: Requires separate endpoint to fetch IP (/api/auth/get-ip)
  3. Increased Complexity: IP detection should be in server side.
  4. Inconsistency: Rate limiting auto-detects IP, but captcha doesn't

Proposed Solution

The captcha plugin should automatically extract IP using the existing getIp utility function, respecting the advanced.ipAddress configuration:

Implementation Suggestion

// captcha/index.ts
import { getIp } from "../../utils/get-request-ip";

export const captcha = (options: CaptchaOptions) =>
    ({
        id: "captcha",
        onRequest: async (request, ctx) => {
            try {
                // ... existing code ...

                const captchaResponse = request.headers.get("x-captcha-response");
                
                // NEW: Auto-detect IP from request headers using Better Auth's config
                const remoteUserIP = getIp(request, ctx.options);

                if (!captchaResponse) {
                    return middlewareResponse({
                        message: EXTERNAL_ERROR_CODES.MISSING_RESPONSE,
                        status: 400,
                    });
                }

                // Rest of verification logic...
            }
        },
    }) satisfies BetterAuthPlugin;

Configuration Example

// auth.ts
export const auth = betterAuth({
    advanced: {
        ipAddress: {
            // Same config used by rate limiting
            ipAddressHeaders: ["cf-connecting-ip", "x-forwarded-for", "x-real-ip"],
            disableIpTracking: false,
        },
    },
    plugins: [
        captcha({
            provider: "google-recaptcha",
            secretKey: process.env.RECAPTCHA_SECRET_KEY!,
        }),
    ],
});

Benefits

  1. Secure: IP extracted server-side from trusted headers
  2. Consistent: Same IP detection logic as rate limiting
  3. Simple: No client-side IP detection code needed
  4. Performant: No extra API calls
  5. Configurable: Respects existing ipAddressHeaders config
  6. Better Fraud Detection: Google reCAPTCHA/Cloudflare get accurate IPs

Better Auth already has the infrastructure:

  1. IP Detection Utility (utils/get-request-ip.ts):

    export function getIp(req: Request | Headers, options: BetterAuthOptions): string | null {
        if (options.advanced?.ipAddress?.disableIpTracking) {
            return null;
        }
        const ipHeaders = options.advanced?.ipAddress?.ipAddressHeaders || ["x-forwarded-for"];
        // ... extraction logic
    }
    
  2. Rate Limiting (already uses this pattern):

    // Rate limiting auto-detects IP
    const userIp = getIp(request, ctx.options);
    
  3. Captcha Verification Handlers (already accept remoteIP):

    • googleRecaptcha({ ..., remoteIP })
    • cloudflareTurnstile({ ..., remoteIP })
    • hCaptcha({ ..., remoteIP })
    • captchaFox({ ..., remoteIP })

📖 Documentation Update

The docs would be simplified from:

Before (Current):

// 1. Create API route to get IP
// 2. Fetch IP on client
// 3. Pass IP in header
await authClient.signIn.email({
    fetchOptions: {
        headers: {
            "x-captcha-response": token,
            "x-captcha-user-remote-ip": userIp, // Manual
        },
    },
});

After (Proposed):

// Just pass the captcha token - IP auto-detected server-side
await authClient.signIn.email({
    fetchOptions: {
        headers: {
            "x-captcha-response": token,
        },
    },
});

Current vs. Expected behavior

..

What version of Better Auth are you using?

1.3.10

System info

{
  "system": {
    "platform": "win32",
    "arch": "x64",
    "version": "Windows 11 Pro",
    "release": "10.0.26100",
    "cpuCount": 12,
    "cpuModel": "AMD Ryzen 5 PRO 4650G with Radeon Graphics",
    "totalMemory": "31.30 GB",
    "freeMemory": "4.72 GB"
  },
  "node": {
    "version": "v22.17.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "10.9.2"
  },
  "frameworks": [
    {
      "name": "next",
      "version": "15.5.6"
    },
    {
      "name": "react",
      "version": "19.2.0"
    }
  ],
  "databases": [
    {
      "name": "pg",
      "version": "^8.16.3"
    },
    {
      "name": "@prisma/client",
      "version": "^6.18.0"
    }
  ],
  "betterAuth": {
    "version": "^1.3.10",
    "config": null
  }
}

Which area(s) are affected? (Select all that apply)

Backend, Client, Documentation, Package

Auth config (if applicable)

import { betterAuth } from "better-auth"
export const auth = betterAuth({
  emailAndPassword: {  
    enabled: true
  },
});

Additional context

No response

Originally created by @issam-seghir on GitHub (Oct 27, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/5616 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce ## Summary The captcha plugin should automatically extract the user's IP address from request headers (similar to how rate limiting works) instead of requiring client-side IP detection and manual header passing. ## Current Behavior Currently, the captcha plugin requires developers to manually pass the user's IP via the `x-captcha-user-remote-ip` header: ```ts // captcha/index.ts (line 23-24) const remoteUserIP = request.headers.get("x-captcha-user-remote-ip") ?? undefined; ``` This forces a **client-side workaround**: ```ts // Client-side (insecure approach) const response = await fetch("/api/auth/get-ip"); // Extra API call needed const { ip } = await response.json(); await authClient.signIn.email({ email: "user@example.com", password: "password", fetchOptions: { headers: { "x-captcha-response": captchaToken, "x-captcha-user-remote-ip": ip, // Manually passed from client }, }, }); ``` ## ❌ Problems with Current Approach 1. **Security Risk**: IP detection happens client-side, can be manipulated 2. **Extra API Calls**: Requires separate endpoint to fetch IP (`/api/auth/get-ip`) 3. **Increased Complexity**: IP detection should be in server side. 4. **Inconsistency**: Rate limiting auto-detects IP, but captcha doesn't ## ✅ Proposed Solution The captcha plugin should **automatically extract IP** using the existing `getIp` utility function, respecting the `advanced.ipAddress` configuration: ### Implementation Suggestion ```ts // captcha/index.ts import { getIp } from "../../utils/get-request-ip"; export const captcha = (options: CaptchaOptions) => ({ id: "captcha", onRequest: async (request, ctx) => { try { // ... existing code ... const captchaResponse = request.headers.get("x-captcha-response"); // NEW: Auto-detect IP from request headers using Better Auth's config const remoteUserIP = getIp(request, ctx.options); if (!captchaResponse) { return middlewareResponse({ message: EXTERNAL_ERROR_CODES.MISSING_RESPONSE, status: 400, }); } // Rest of verification logic... } }, }) satisfies BetterAuthPlugin; ``` ### Configuration Example ```ts // auth.ts export const auth = betterAuth({ advanced: { ipAddress: { // Same config used by rate limiting ipAddressHeaders: ["cf-connecting-ip", "x-forwarded-for", "x-real-ip"], disableIpTracking: false, }, }, plugins: [ captcha({ provider: "google-recaptcha", secretKey: process.env.RECAPTCHA_SECRET_KEY!, }), ], }); ``` ## Benefits 1. **Secure**: IP extracted server-side from trusted headers 2. **Consistent**: Same IP detection logic as rate limiting 3. **Simple**: No client-side IP detection code needed 4. **Performant**: No extra API calls 5. **Configurable**: Respects existing `ipAddressHeaders` config 6. **Better Fraud Detection**: Google reCAPTCHA/Cloudflare get accurate IPs ## Related Code Better Auth already has the infrastructure: 1. **IP Detection Utility** (`utils/get-request-ip.ts`): ```ts export function getIp(req: Request | Headers, options: BetterAuthOptions): string | null { if (options.advanced?.ipAddress?.disableIpTracking) { return null; } const ipHeaders = options.advanced?.ipAddress?.ipAddressHeaders || ["x-forwarded-for"]; // ... extraction logic } ``` 2. **Rate Limiting** (already uses this pattern): ```ts // Rate limiting auto-detects IP const userIp = getIp(request, ctx.options); ``` 3. **Captcha Verification Handlers** (already accept `remoteIP`): - `googleRecaptcha({ ..., remoteIP })` - `cloudflareTurnstile({ ..., remoteIP })` - `hCaptcha({ ..., remoteIP })` - `captchaFox({ ..., remoteIP })` ## 📖 Documentation Update The docs would be simplified from: **Before (Current)**: ```ts // 1. Create API route to get IP // 2. Fetch IP on client // 3. Pass IP in header await authClient.signIn.email({ fetchOptions: { headers: { "x-captcha-response": token, "x-captcha-user-remote-ip": userIp, // Manual }, }, }); ``` **After (Proposed)**: ```ts // Just pass the captcha token - IP auto-detected server-side await authClient.signIn.email({ fetchOptions: { headers: { "x-captcha-response": token, }, }, }); ``` ### Current vs. Expected behavior .. ### What version of Better Auth are you using? 1.3.10 ### System info ```bash { "system": { "platform": "win32", "arch": "x64", "version": "Windows 11 Pro", "release": "10.0.26100", "cpuCount": 12, "cpuModel": "AMD Ryzen 5 PRO 4650G with Radeon Graphics", "totalMemory": "31.30 GB", "freeMemory": "4.72 GB" }, "node": { "version": "v22.17.0", "env": "development" }, "packageManager": { "name": "npm", "version": "10.9.2" }, "frameworks": [ { "name": "next", "version": "15.5.6" }, { "name": "react", "version": "19.2.0" } ], "databases": [ { "name": "pg", "version": "^8.16.3" }, { "name": "@prisma/client", "version": "^6.18.0" } ], "betterAuth": { "version": "^1.3.10", "config": null } } ``` ### Which area(s) are affected? (Select all that apply) Backend, Client, Documentation, Package ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth" export const auth = betterAuth({ emailAndPassword: { enabled: true }, }); ``` ### Additional context _No response_
GiteaMirror added the lockedenhancement labels 2026-04-17 18:43:59 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Oct 27, 2025):

You're right—currently, the captcha plugin requires the client to manually pass the user's IP via the x-captcha-user-remote-ip header, which is inconsistent with how rate limiting works. Rate limiting already uses the getIp utility to extract the IP server-side from trusted headers, respecting your advanced.ipAddress config. The captcha plugin does not use getIp at all; it just reads the manual header and passes it to the verification handler, which is both less secure and more complex for developers to implement source.

There are multiple issues and discussions confirming that this manual approach causes confusion, security risks, and extra work for users source source source. There is also consensus that the captcha plugin should adopt the same automatic server-side IP detection logic as rate limiting, using the getIp utility and your existing config source.

Your proposed solution—to have the captcha plugin call getIp(request, ctx.options) and pass the result as remoteIP—would make the plugin more secure, consistent, and easier to use. This change is not yet implemented as of v1.3.10, but the infrastructure is already in place and the change would align with how rate limiting works source.

Updating the captcha plugin to use getIp would also simplify the documentation and remove the need for client-side IP detection or extra API calls. This is a well-supported and actionable improvement.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

<!-- gh-comment-id:3452063618 --> @dosubot[bot] commented on GitHub (Oct 27, 2025): <!-- Answer --> You're right—currently, the captcha plugin requires the client to manually pass the user's IP via the `x-captcha-user-remote-ip` header, which is inconsistent with how rate limiting works. Rate limiting already uses the `getIp` utility to extract the IP server-side from trusted headers, respecting your `advanced.ipAddress` config. The captcha plugin does not use `getIp` at all; it just reads the manual header and passes it to the verification handler, which is both less secure and more complex for developers to implement [source](https://github.com/better-auth/better-auth/blob/97cd4a205957002568915b2f8b85c2b9c8de76d9/packages/better-auth/src/plugins/captcha/index.ts). There are multiple issues and discussions confirming that this manual approach causes confusion, security risks, and extra work for users [source](https://github.com/better-auth/better-auth/issues/3803) [source](https://github.com/better-auth/better-auth/issues/3804) [source](https://github.com/better-auth/better-auth/issues/3805). There is also consensus that the captcha plugin should adopt the same automatic server-side IP detection logic as rate limiting, using the `getIp` utility and your existing config [source](https://github.com/better-auth/better-auth/discussions/3865). Your proposed solution—to have the captcha plugin call `getIp(request, ctx.options)` and pass the result as `remoteIP`—would make the plugin more secure, consistent, and easier to use. This change is not yet implemented as of v1.3.10, but the infrastructure is already in place and the change would align with how rate limiting works [source](https://github.com/better-auth/better-auth/blob/97cd4a205957002568915b2f8b85c2b9c8de76d9/packages/better-auth/src/utils/get-request-ip.ts). Updating the captcha plugin to use `getIp` would also simplify the documentation and remove the need for client-side IP detection or extra API calls. This is a well-supported and actionable improvement. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/8a88ad29-1cf8-497f-a553-3cd29cc47cfa?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/8a88ad29-1cf8-497f-a553-3cd29cc47cfa?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/8a88ad29-1cf8-497f-a553-3cd29cc47cfa?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/8a88ad29-1cf8-497f-a553-3cd29cc47cfa?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/8a88ad29-1cf8-497f-a553-3cd29cc47cfa?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/8a88ad29-1cf8-497f-a553-3cd29cc47cfa?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/8a88ad29-1cf8-497f-a553-3cd29cc47cfa?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/5616)
Author
Owner

@GautamBytes commented on GitHub (Oct 30, 2025):

Will work on it!

<!-- gh-comment-id:3467941175 --> @GautamBytes commented on GitHub (Oct 30, 2025): Will work on it!
Author
Owner

@better-auth-agent[bot] commented on GitHub (Oct 30, 2025):

Based on the context, the recommended next steps are as follows:

• Update the captcha plugin so that it no longer relies on a client-side workaround. Instead of requiring the manual passing of the IP via the header, incorporate the getIp utility (which is already used by rate limiting) to extract the IP server-side using the trusted headers defined in your advanced.ipAddress configuration [1].

• Remove the extra API call that fetches the IP from a dedicated endpoint and modify both the plugin code and its associated documentation so that developers only need to pass the captcha token from the client [1].

• Verify that the change keeps the behavior consistent with the rate limiting logic; the server should automatically detect the IP from headers like "cf-connecting-ip", "x-forwarded-for", or "x-real-ip" as configured in your auth setup [1].

• Finally, update tests to confirm that the header extraction works correctly in production-like environments and ensure that the improved flow does not introduce any security risks.

These steps will make the captcha verification process more secure, performant, and less complex from a client-development standpoint.

DiagramDiscordGitHub

Diagram Join Star

<!-- gh-comment-id:3467943386 --> @better-auth-agent[bot] commented on GitHub (Oct 30, 2025): Based on the context, the recommended next steps are as follows: • Update the captcha plugin so that it no longer relies on a client-side workaround. Instead of requiring the manual passing of the IP via the header, incorporate the getIp utility (which is already used by rate limiting) to extract the IP server-side using the trusted headers defined in your advanced.ipAddress configuration [[1]](https://github.com/better-auth/better-auth/issues/5616). • Remove the extra API call that fetches the IP from a dedicated endpoint and modify both the plugin code and its associated documentation so that developers only need to pass the captcha token from the client [[1]](https://github.com/better-auth/better-auth/issues/5616). • Verify that the change keeps the behavior consistent with the rate limiting logic; the server should automatically detect the IP from headers like "cf-connecting-ip", "x-forwarded-for", or "x-real-ip" as configured in your auth setup [[1]](https://github.com/better-auth/better-auth/issues/5616). • Finally, update tests to confirm that the header extraction works correctly in production-like environments and ensure that the improved flow does not introduce any security risks. These steps will make the captcha verification process more secure, performant, and less complex from a client-development standpoint. <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) • [Discord](https://discord.gg/better-auth) • [GitHub](https://github.com/better-auth/better-auth) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/better-auth) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/better-auth/better-auth)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#27626