[GH-ISSUE #1684] Google Recaptcha's Site Verify Request is faulty (+fix) #8865

Closed
opened 2026-04-13 04:06:32 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @JakobsCode on GitHub (Mar 4, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/1684

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Add Captcha Plugin
  2. Create Client Component
  3. Use react-google-recaptcha for Client Google Recaptcha v2.
  4. Verify Captcha and use "Login Function" where the Captcha is checked.
  5. Server will tell you the Client Response is wrong but verifying the Client Token with the private Server Key via the same Google Endpoint tells you its a correct Response:

Server Response (better-auth):
POST /api/auth/sign-in/email 403 in 415ms
│ POST https://www.google.com/recaptcha/api/siteverify 200 in 52ms (cache skip)
│ │ Cache skipped reason: (auto no cache)

I extracted the response that that request got:

{"response":{"data":{"success":false,"error-codes":["invalid-input-response"]},"error":null}

The Request has to be formatted incorrectly!

Using the Google API with the same Client Token and same private Server Key:
Google Response:

{
  "success": true,
  "challenge_ts": "2025-03-04T21:15:01Z",
  "hostname": "localhost"
}

Current vs. Expected behavior

Shoud Verify.

Change the Function googleReCAPTCHA:

const googleReCAPTCHA = async ({
  siteVerifyURL,
  captchaResponse,
  secretKey
}) => {
  const response = await betterFetch(
    siteVerifyURL + "?secret=" + secretKey + "&response=" + captchaResponse,
    {
      method: "POST",
      // headers: { "Content-Type": "any" },
      // params: {      secret: secretKey,
      //      response: captchaResponse}
      // body: JSON.stringify({
      //   secret: secretKey,
      //   response: captchaResponse
      // })
    }
  );
  if (!response.data || response.error) {
    return middlewareResponse({
      message: CAPTCHA_ERROR_CODES.SERVICE_UNAVAILABLE,
      status: 503
    });
  }
  if (!response.data.success) {
    return middlewareResponse({
      message: CAPTCHA_ERROR_CODES.VERIFICATION_FAILED,
      status: 403
    });
  }
  return void 0;
};

This Code works using the params instead of the json body!

What version of Better Auth are you using?

1.2.2

Provide environment information

Windows 11
Webstrom
Edge

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

Backend

Auth config (if applicable)

import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import * as betterAuthSchema from "@/database/schema/better-auth";
import * as ordersSchema from "@/database/schema/order";
import { Pool } from "pg";
import { drizzle } from "drizzle-orm/node-postgres";
import { sendVerificationEmail, sendResetPassword } from "@/email/auth/actions";
import { admin, organization, captcha } from "better-auth/plugins";

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

export const db = drizzle(pool, {
  schema: { ...betterAuthSchema, ...ordersSchema },
});

export const auth = betterAuth({
  database: drizzleAdapter(db, {
    provider: "pg",
  }),
  trustedOrigins:
    (process.env.BETTER_AUTH_TRUSTED_ORIGINS && [
      process.env.BETTER_AUTH_TRUSTED_ORIGINS,
    ]) ||
    undefined,
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: true,
    sendResetPassword: async ({ user, url, token }) => {
      await sendResetPassword({ url, email: user.email });
    },
  },

  emailVerification: {
    sendOnSignUp: true,
    autoSignInAfterVerification: true,
    sendVerificationEmail: async ({ user, url, token }, request) => {
      await sendVerificationEmail({ url, email: user.email });
    },
  },

  plugins: [
    admin({
      defaultRole: "user",
      adminRole: ["admin"],
      defaultBanReason: "Breach of the terms of use",
      defaultBanExpiresIn: 60 * 60 * 24 * 356, // 1 Jahr
      impersonationSessionDuration: 60 * 60 * 24, // 1 Tag
    }),
    organization({
      allowUserToCreateOrganization: async (user) => {
        return true;
      },
    }),
    captcha({
      provider: "google-recaptcha",
      secretKey: process.env.RECAPTCHA_PRIVATE_KEY!,
      endpoints: ["/sign-up", "/sign-in", "/forget-password"],
    }),
  ],
});

Additional context

I could fix the bug by using params, could be the JSON.stringify({....}) but i dont know.

Originally created by @JakobsCode on GitHub (Mar 4, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/1684 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Add Captcha Plugin 2. Create Client Component 3. Use react-google-recaptcha for Client Google Recaptcha v2. 4. Verify Captcha and use "Login Function" where the Captcha is checked. 5. Server will tell you the Client Response is wrong but verifying the Client Token with the private Server Key via the same Google Endpoint tells you its a correct Response: Server Response (better-auth): POST /api/auth/sign-in/email 403 in 415ms │ POST https://www.google.com/recaptcha/api/siteverify 200 in 52ms (cache skip) │ │ Cache skipped reason: (auto no cache) I extracted the response that that request got: ```json {"response":{"data":{"success":false,"error-codes":["invalid-input-response"]},"error":null} ``` The Request has to be formatted incorrectly! Using the Google API with the **same** Client Token and **same** private Server Key: Google Response: ```json { "success": true, "challenge_ts": "2025-03-04T21:15:01Z", "hostname": "localhost" } ``` ### Current vs. Expected behavior Shoud Verify. Change the Function googleReCAPTCHA: ```typescript const googleReCAPTCHA = async ({ siteVerifyURL, captchaResponse, secretKey }) => { const response = await betterFetch( siteVerifyURL + "?secret=" + secretKey + "&response=" + captchaResponse, { method: "POST", // headers: { "Content-Type": "any" }, // params: { secret: secretKey, // response: captchaResponse} // body: JSON.stringify({ // secret: secretKey, // response: captchaResponse // }) } ); if (!response.data || response.error) { return middlewareResponse({ message: CAPTCHA_ERROR_CODES.SERVICE_UNAVAILABLE, status: 503 }); } if (!response.data.success) { return middlewareResponse({ message: CAPTCHA_ERROR_CODES.VERIFICATION_FAILED, status: 403 }); } return void 0; }; ``` This Code works using the params instead of the json body! ### What version of Better Auth are you using? 1.2.2 ### Provide environment information ```bash Windows 11 Webstrom Edge ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import * as betterAuthSchema from "@/database/schema/better-auth"; import * as ordersSchema from "@/database/schema/order"; import { Pool } from "pg"; import { drizzle } from "drizzle-orm/node-postgres"; import { sendVerificationEmail, sendResetPassword } from "@/email/auth/actions"; import { admin, organization, captcha } from "better-auth/plugins"; const pool = new Pool({ connectionString: process.env.DATABASE_URL, }); export const db = drizzle(pool, { schema: { ...betterAuthSchema, ...ordersSchema }, }); export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "pg", }), trustedOrigins: (process.env.BETTER_AUTH_TRUSTED_ORIGINS && [ process.env.BETTER_AUTH_TRUSTED_ORIGINS, ]) || undefined, emailAndPassword: { enabled: true, requireEmailVerification: true, sendResetPassword: async ({ user, url, token }) => { await sendResetPassword({ url, email: user.email }); }, }, emailVerification: { sendOnSignUp: true, autoSignInAfterVerification: true, sendVerificationEmail: async ({ user, url, token }, request) => { await sendVerificationEmail({ url, email: user.email }); }, }, plugins: [ admin({ defaultRole: "user", adminRole: ["admin"], defaultBanReason: "Breach of the terms of use", defaultBanExpiresIn: 60 * 60 * 24 * 356, // 1 Jahr impersonationSessionDuration: 60 * 60 * 24, // 1 Tag }), organization({ allowUserToCreateOrganization: async (user) => { return true; }, }), captcha({ provider: "google-recaptcha", secretKey: process.env.RECAPTCHA_PRIVATE_KEY!, endpoints: ["/sign-up", "/sign-in", "/forget-password"], }), ], }); ``` ### Additional context I could fix the bug by using params, could be the JSON.stringify({....}) but i dont know.
GiteaMirror added the lockedbug labels 2026-04-13 04:06:32 -05:00
Author
Owner

@0scrm commented on GitHub (Mar 16, 2025):

@JakobsCode Thanks for reporting the issue, this has been fixed in #1836 :)

<!-- gh-comment-id:2727330586 --> @0scrm commented on GitHub (Mar 16, 2025): @JakobsCode Thanks for reporting the issue, this has been fixed in #1836 :)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#8865