#11480·better-auth

`disableSignUp` on a social provider is ignored for ID-token sign-in (`/sign-in/social` with `idToken`)

Author: vytautas-idealinkCreated Sep 30, 2026Updated Sep 30, 2026

Is this suited for github?

  • Yes, this is suited for github

Reproduction

Steps to reproduce:

  1. Configure a social provider with disableSignUp: true (see the auth config below).
  2. Call POST /api/auth/sign-in/social with { "provider": "google", "idToken": { "token": "<valid Google ID token for an email that has no account>" } }, which is what authClient.signIn.social({ provider: "google", idToken: { token } }) sends.
  3. The response is 200, a new user is created, and a session token is returned.

Self-contained script with no network. It stubs only the provider's verifyIdToken and getUserInfo, which take no part in the sign-up decision. After a clean npm install [email protected], it prints 200 1:

javascript
import { betterAuth } from "better-auth";
import { memoryAdapter } from "better-auth/adapters/memory";

const db = { user: [], session: [], account: [], verification: [] };
const auth = betterAuth({
  baseURL: "http://localhost:3000",
  secret: "a-test-secret-that-is-long-enough-1234567890",
  database: memoryAdapter(db),
  socialProviders: {
    google: {
      clientId: "test",
      clientSecret: "test",
      disableSignUp: true,
      // Stand-ins for Google's key check and token decode, so no network is needed.
      verifyIdToken: async () => true,
      getUserInfo: async () => ({
        user: { id: "sub-1", email: "[email protected]", name: "New", emailVerified: true },
        data: { sub: "sub-1", email: "[email protected]" },
      }),
    },
  },
});

const res = await auth.handler(
  new Request("http://localhost:3000/api/auth/sign-in/social", {
    method: "POST",
    headers: { "content-type": "application/json", origin: "http://localhost:3000" },
    body: JSON.stringify({ provider: "google", idToken: { token: "a.b.c" } }),
  }),
);
console.log(res.status, db.user.length); // expected: 401 0 (sign-up is disabled)

Current vs. Expected behavior

Current: HTTP 200. A user is created for the unknown email, and the response carries a session token. disableSignUp: true has no effect on this path.

Expected: the request is refused with signup disabled (401 OAUTH_LINK_ERROR, the answer this branch already gives for disableImplicitSignUp), and no user is created. That is what the redirect flow (/callback/:id) already does with the same config.

Signing in an existing user is unaffected, and should stay that way: the flag is only read when no user matches.

What version of Better Auth are you using?

1.7.6 (also 1.7.5; the same line is on main at 4186e36)

System info

bash
{
  "system": {
    "platform": "linux",
    "arch": "x64",
    "version": "#1 SMP PREEMPT_DYNAMIC @0",
    "release": "6.18.44-fc-v50",
    "cpuCount": 4,
    "cpuModel": "Intel(R) Xeon(R) Processor @ 2.80GHz",
    "totalMemory": "15.72 GB",
    "freeMemory": "14.73 GB"
  },
  "node": { "version": "v24.21.0", "env": "development" },
  "packageManager": { "name": "npm", "version": "11.19.0" },
  "frameworks": null,
  "databases": null,
  "betterAuth": { "version": "1.7.6", "config": null }
}

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

Backend

Auth config (if applicable)

typescript
import { betterAuth } from "better-auth"
export const auth = betterAuth({
  emailAndPassword: {  
    enabled: trueimport { betterAuth } from "better-auth"
export const auth = betterAuth({
  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
      disableSignUp: true,
    },
  },
});
  },
});

Additional context

I tested the reproduction against the latest release (1.7.6).

Root cause. The ID-token branch of /sign-in/social reads a top-level provider.disableSignUp: https://github.com/better-auth/better-auth/blob/229a02a652185ed32e87eab0c77c09d58532e0f1/packages/better-auth/src/api/routes/sign-in.ts#L343-L345

typescript
disableSignUp:
  (provider.disableImplicitSignUp && !c.body.requestSignUp) ||
  provider.disableSignUp,

Built-in social providers never set that property. google(options) returns the config as provider.options, and createAuthContext copies only disableImplicitSignUp onto the provider: https://github.com/better-auth/better-auth/blob/229a02a652185ed32e87eab0c77c09d58532e0f1/packages/better-auth/src/context/create-context.ts#L243-L246

The callback route reads the flag from options: https://github.com/better-auth/better-auth/blob/229a02a652185ed32e87eab0c77c09d58532e0f1/packages/better-auth/src/api/routes/callback.ts#L297-L299

The wrong read type-checks because OAuthProvider declares an optional top-level disableSignUp. Generic-oauth providers set the flag in both places, so they are unaffected. The same line has been there since at least v1.3.0.

Suggested fix, one line, matching callback.ts:

diff
 disableSignUp:
   (provider.disableImplicitSignUp && !c.body.requestSignUp) ||
-  provider.disableSignUp,
+  provider.options?.disableSignUp,

With this change applied to the 1.7.6 build:

  • The reproduction returns 401 signup disabled and creates no user.
  • Existing users still sign in.
  • requestSignUp: true no longer overrides disableSignUp.

A regression test that uses idToken, placed next to describe("Disable signup") in packages/better-auth/src/social.test.ts, would cover this; that describe block only tests the redirect callback today.

disableImplicitSignUp is no workaround here. It is enforced on this path, but a client lifts it by sending requestSignUp: true in the same body.

Related: #10478 was the same gap in One Tap, fixed by #10479. That fix did not touch /sign-in/social.