diff --git a/.changeset/fix-social-id-token-disable-sign-up.md b/.changeset/fix-social-id-token-disable-sign-up.md new file mode 100644 index 0000000000..b0d6cf11dd --- /dev/null +++ b/.changeset/fix-social-id-token-disable-sign-up.md @@ -0,0 +1,5 @@ +--- +"better-auth": patch +--- + +Respect social provider `disableSignUp` when signing in with an ID token. diff --git a/packages/better-auth/src/api/routes/sign-in.ts b/packages/better-auth/src/api/routes/sign-in.ts index 0cbde53740..9c41399533 100644 --- a/packages/better-auth/src/api/routes/sign-in.ts +++ b/packages/better-auth/src/api/routes/sign-in.ts @@ -342,6 +342,7 @@ export const signInSocial = () => callbackURL: c.body.callbackURL, disableSignUp: (provider.disableImplicitSignUp && !c.body.requestSignUp) || + provider.options?.disableSignUp || provider.disableSignUp, source: { method: "oauth", diff --git a/packages/better-auth/src/social.test.ts b/packages/better-auth/src/social.test.ts index 89b831e876..1440689fe2 100644 --- a/packages/better-auth/src/social.test.ts +++ b/packages/better-auth/src/social.test.ts @@ -696,6 +696,132 @@ describe("Disable implicit signup", async () => { }); describe("Disable signup", async () => { + /** + * @see https://github.com/better-auth/better-auth/issues/11480 + */ + describe("ID-token sign-in", () => { + const googleIdTokenOptions = (email: string) => { + const profile: GoogleProfile = { + aud: "test", + azp: "test", + email, + email_verified: true, + exp: 2_000_000_000, + family_name: "User", + given_name: "Google", + iat: 1_900_000_000, + iss: "https://accounts.google.com", + name: "Google User", + picture: "https://example.com/avatar.png", + sub: "google-id-token-user", + }; + return { + clientId: "test", + clientSecret: "test", + verifyIdToken: async () => true, + getUserInfo: async () => ({ + user: { email, name: profile.name, emailVerified: true }, + data: profile, + }), + }; + }; + + it.each([ + { requestSignUp: undefined, scenario: "without requestSignUp" }, + { requestSignUp: true, scenario: "with requestSignUp" }, + ])("rejects ID-token sign-in for a new user $scenario", async ({ + requestSignUp, + }) => { + const email = "new-id-token-user@example.com"; + const { client, auth } = await getTestInstance( + { + socialProviders: { + google: { + ...googleIdTokenOptions(email), + disableSignUp: true, + }, + }, + }, + { disableTestUser: true }, + ); + + const response = await client.signIn.social({ + provider: "google", + idToken: { token: "a.b.c" }, + requestSignUp, + }); + + const user = await (await auth.$context).adapter.findOne({ + model: "user", + where: [{ field: "email", value: email }], + }); + expect(user).toBeNull(); + expect(response.error).toMatchObject({ + status: 401, + code: "OAUTH_LINK_ERROR", + message: "signup disabled", + }); + }); + + it("allows an existing ID-token user to sign in when provider sign-up is disabled", async () => { + const email = "existing-id-token-user@example.com"; + const { client, auth } = await getTestInstance( + { socialProviders: { google: googleIdTokenOptions(email) } }, + { disableTestUser: true }, + ); + const signIn = () => + client.signIn.social({ + provider: "google", + idToken: { token: "a.b.c" }, + }); + + const firstResponse = await signIn(); + expect(firstResponse.error).toBeNull(); + + const provider = (await auth.$context).socialProviders.find( + (provider) => provider.id === "google", + ); + expect(provider).toBeDefined(); + provider!.options!.disableSignUp = true; + + const response = await signIn(); + expect(response.error).toBeNull(); + expect(response.data).toMatchObject({ + redirect: false, + user: { email }, + }); + }); + + it("keeps the top-level provider sign-up restriction for ID-token sign-in", async () => { + const { client, auth } = await getTestInstance( + { + socialProviders: { + google: { + ...googleIdTokenOptions("top-level-id-token-user@example.com"), + disableSignUp: false, + }, + }, + }, + { disableTestUser: true }, + ); + const provider = (await auth.$context).socialProviders.find( + (provider) => provider.id === "google", + ); + expect(provider).toBeDefined(); + provider!.disableSignUp = true; + + const response = await client.signIn.social({ + provider: "google", + idToken: { token: "a.b.c" }, + }); + expect(response.error).toMatchObject({ + status: 401, + code: "OAUTH_LINK_ERROR", + message: "signup disabled", + }); + }); + }); + it("Should not create user when sign up is disabled", async () => { const headers = new Headers(); const { client, cookieSetter } = await getTestInstance({