diff --git a/packages/better-auth/src/plugins/username/index.ts b/packages/better-auth/src/plugins/username/index.ts index 269c7566..02f98b40 100644 --- a/packages/better-auth/src/plugins/username/index.ts +++ b/packages/better-auth/src/plugins/username/index.ts @@ -286,43 +286,6 @@ export const username = (options?: UsernameOptions) => { }); } - if ( - ctx.context.options?.emailAndPassword?.requireEmailVerification && - !user.emailVerified - ) { - if ( - !ctx.context.options?.emailVerification?.sendVerificationEmail - ) { - throw new APIError("FORBIDDEN", { - message: ERROR_CODES.EMAIL_NOT_VERIFIED, - }); - } - - if (ctx.context.options?.emailVerification?.sendOnSignIn) { - const token = await createEmailVerificationToken( - ctx.context.secret, - user.email, - undefined, - ctx.context.options.emailVerification?.expiresIn, - ); - const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${ - ctx.body.callbackURL || "/" - }`; - await ctx.context.options.emailVerification.sendVerificationEmail( - { - user: user, - url, - token, - }, - ctx.request, - ); - } - - throw new APIError("FORBIDDEN", { - message: ERROR_CODES.EMAIL_NOT_VERIFIED, - }); - } - const account = await ctx.context.adapter.findOne({ model: "account", where: [ @@ -360,6 +323,44 @@ export const username = (options?: UsernameOptions) => { message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD, }); } + + if ( + ctx.context.options?.emailAndPassword?.requireEmailVerification && + !user.emailVerified + ) { + if ( + !ctx.context.options?.emailVerification?.sendVerificationEmail + ) { + throw new APIError("FORBIDDEN", { + message: ERROR_CODES.EMAIL_NOT_VERIFIED, + }); + } + + if (ctx.context.options?.emailVerification?.sendOnSignIn) { + const token = await createEmailVerificationToken( + ctx.context.secret, + user.email, + undefined, + ctx.context.options.emailVerification?.expiresIn, + ); + const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${ + ctx.body.callbackURL || "/" + }`; + await ctx.context.options.emailVerification.sendVerificationEmail( + { + user: user, + url, + token, + }, + ctx.request, + ); + } + + throw new APIError("FORBIDDEN", { + message: ERROR_CODES.EMAIL_NOT_VERIFIED, + }); + } + const session = await ctx.context.internalAdapter.createSession( user.id, ctx, diff --git a/packages/better-auth/src/plugins/username/username.test.ts b/packages/better-auth/src/plugins/username/username.test.ts index d819ea47..13171eb9 100644 --- a/packages/better-auth/src/plugins/username/username.test.ts +++ b/packages/better-auth/src/plugins/username/username.test.ts @@ -533,3 +533,44 @@ describe("post normalization flow", async (it) => { expect(session?.user.displayUsername).toBe("Test Username"); }); }); + +describe("username email verification flow (no info leak)", async (it) => { + const { client } = await getTestInstance( + { + emailAndPassword: { enabled: true, requireEmailVerification: true }, + plugins: [username()], + }, + { + clientOptions: { + plugins: [usernameClient()], + }, + }, + ); + + it("returns INVALID_USERNAME_OR_PASSWORD for wrong password even if email is unverified", async () => { + await client.signUp.email({ + email: "unverified-user@example.com", + username: "unverified_user", + password: "correct-password", + name: "Unverified User", + }); + + const res = await client.signIn.username({ + username: "unverified_user", + password: "wrong-password", + }); + + expect(res.error?.status).toBe(401); + expect(res.error?.code).toBe("INVALID_USERNAME_OR_PASSWORD"); + }); + + it("returns EMAIL_NOT_VERIFIED only after a correct password for an unverified user", async () => { + const res = await client.signIn.username({ + username: "unverified_user", + password: "correct-password", + }); + + expect(res.error?.status).toBe(403); + expect(res.error?.code).toBe("EMAIL_NOT_VERIFIED"); + }); +});