From 01bd8c1b760fca32ff5f87ddd5b2cc3774a2deb1 Mon Sep 17 00:00:00 2001 From: Bereket Engida <86073083+Bekacru@users.noreply.github.com> Date: Sat, 17 Jan 2026 20:11:47 -0800 Subject: [PATCH] feat(admin): make password field optional on create user (#7441) Co-authored-by: Cursor Agent --- .../src/plugins/admin/admin.test.ts | 33 +++++++++++++++++++ .../better-auth/src/plugins/admin/routes.ts | 26 +++++++++------ 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/packages/better-auth/src/plugins/admin/admin.test.ts b/packages/better-auth/src/plugins/admin/admin.test.ts index a0e4dd98b9..3627779687 100644 --- a/packages/better-auth/src/plugins/admin/admin.test.ts +++ b/packages/better-auth/src/plugins/admin/admin.test.ts @@ -173,6 +173,39 @@ describe("Admin plugin", async () => { expect(newUser?.role).toBe("user"); }); + it("should allow admin to create users without password", async () => { + const res = await client.admin.createUser( + { + name: "Passwordless User", + email: "passwordless@email.com", + role: "user", + }, + { + headers: adminHeaders, + }, + ); + expect(res.data?.user?.email).toBe("passwordless@email.com"); + expect(res.data?.user?.name).toBe("Passwordless User"); + expect(res.data?.user?.role).toBe("user"); + + // User should not be able to sign in with password since no credential account exists + const signInRes = await client.signIn.email({ + email: "passwordless@email.com", + password: "anypassword", + }); + expect(signInRes.error).toBeDefined(); + + // Clean up + await client.admin.removeUser( + { + userId: res.data?.user?.id || "", + }, + { + headers: adminHeaders, + }, + ); + }); + it("should allow admin to create user with multiple roles", async () => { const res = await client.admin.createUser( { diff --git a/packages/better-auth/src/plugins/admin/routes.ts b/packages/better-auth/src/plugins/admin/routes.ts index 3da3833876..209ec28750 100644 --- a/packages/better-auth/src/plugins/admin/routes.ts +++ b/packages/better-auth/src/plugins/admin/routes.ts @@ -236,8 +236,9 @@ const createUserBodySchema = z.object({ email: z.string().meta({ description: "The email of the user", }), - password: z.string().meta({ - description: "The password of the user", + password: z.string().optional().meta({ + description: + "The password of the user. If not provided, the user will be created without a credential account (useful for magic link or social login only users).", }), name: z.string().meta({ description: "The name of the user", @@ -313,7 +314,7 @@ export const createUser = (opts: O) => $Infer: { body: {} as { email: string; - password: string; + password?: string | undefined; name: string; role?: | (InferAdminRolesFromOption | InferAdminRolesFromOption[]) @@ -374,13 +375,18 @@ export const createUser = (opts: O) => message: ADMIN_ERROR_CODES.FAILED_TO_CREATE_USER, }); } - const hashedPassword = await ctx.context.password.hash(ctx.body.password); - await ctx.context.internalAdapter.linkAccount({ - accountId: user.id, - providerId: "credential", - password: hashedPassword, - userId: user.id, - }); + // Only create credential account if password is provided + if (ctx.body.password) { + const hashedPassword = await ctx.context.password.hash( + ctx.body.password, + ); + await ctx.context.internalAdapter.linkAccount({ + accountId: user.id, + providerId: "credential", + password: hashedPassword, + userId: user.id, + }); + } return ctx.json({ user: parseUserOutput(ctx.context.options, user) as UserWithRole, });