`disableSignUp` on a social provider is ignored for ID-token sign-in (`/sign-in/social` with `idToken`)
Is this suited for github?
- Yes, this is suited for github
Reproduction
Steps to reproduce:
- Configure a social provider with
disableSignUp: true(see the auth config below). - Call
POST /api/auth/sign-in/socialwith{ "provider": "google", "idToken": { "token": "<valid Google ID token for an email that has no account>" } }, which is whatauthClient.signIn.social({ provider: "google", idToken: { token } })sends. - 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:
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
{
"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)
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
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:
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 disabledand creates no user. - Existing users still sign in.
requestSignUp: trueno longer overridesdisableSignUp.
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.
Source: better-auth/better-auth