OAuth Sign-Up Control#
Better Auth provides two complementary mechanisms to prevent new account creation during OAuth and Google One Tap flows while still allowing existing users to sign in. The check happens inside handleOAuthUserInfo() — the shared function invoked by both the standard OAuth callback and the One Tap plugin — immediately before the new-user registration branch .
disableSignUp — Hard Block on New Registrations#
Set disableSignUp: true on any provider in socialProviders to permanently reject OAuth sign-up for that provider. Existing users can still sign in; only the "no user found → create user" path is blocked.
socialProviders: {
google: {
clientId: "...",
clientSecret: "...",
disableSignUp: true,
},
}
How it works: The callback handler at /callback/:id passes provider.options?.disableSignUp directly to handleOAuthUserInfo as the disableSignUp flag . Inside that function, when isRegister = true (no existing user found) and disableSignUp is truthy, the function returns { error: "signup disabled", data: null } . The callback handler then redirects to the error URL with the signup_disabled error code .
Note on stateless mode: The official docs flag a known issue —
disableSignUpdoes not work correctly with stateless mode. The recommended workaround is adatabaseHooks.user.create.beforehook that throws anAPIErrorto reject the creation .
disableImplicitSignUp — Opt-In Registration#
disableImplicitSignUp: true is a softer control: it blocks sign-up unless the client explicitly passes requestSignUp: true when initiating the flow .
socialProviders: {
google: {
clientId: "...",
clientSecret: "...",
disableImplicitSignUp: true,
},
}
How it works: The parsed OAuth state carries a requestSignUp flag . The disableSignUp value passed to handleOAuthUserInfo is computed as (provider.disableImplicitSignUp && !requestSignUp) || provider.options?.disableSignUp . So when disableImplicitSignUp is set, sign-up is only blocked when the client has not explicitly requested it — allowing application-level UI to gate new registrations (e.g., an invite flow) while reusing the same OAuth provider config.
Client-side usage:
await authClient.signIn.social({
provider: "google",
requestSignUp: true, // omit this to block registration
});
One Tap: disableSignup#
The Google One Tap plugin exposes its own option — note the lowercase p: disableSignup (not disableSignUp). It maps directly onto the same disableSignUp parameter passed to handleOAuthUserInfo , so the enforcement path is identical.
import { oneTap } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
oneTap({ disableSignup: true }),
],
});
Unlike disableImplicitSignUp, the One Tap option has no requestSignUp escape hatch — it is a hard block.
Decision Summary#
| Scenario | Option | Escape hatch? |
|---|---|---|
| Block all new OAuth accounts for a provider | socialProviders.<id>.disableSignUp: true | No |
| Allow sign-up only when explicitly requested | socialProviders.<id>.disableImplicitSignUp: true | Yes — requestSignUp: true in client call |
| Block new accounts via One Tap | oneTap({ disableSignup: true }) | No |
Key Source Files#
| File | Role |
|---|---|
packages/better-auth/src/api/routes/callback.ts | OAuth callback handler; assembles the disableSignUp flag from provider options and state |
packages/better-auth/src/oauth2/link-account.ts | handleOAuthUserInfo() — enforces the block before user creation |
packages/better-auth/src/plugins/one-tap/index.ts | One Tap plugin; passes options.disableSignup to handleOAuthUserInfo |
| OAuth Provider Options docs | Reference for disableSignUp and disableImplicitSignUp |
signup_disabled error reference | Known issue with stateless mode and recommended workaround |