diff --git a/packages/better-auth/src/api/routes/forget-password.test.ts b/packages/better-auth/src/api/routes/forget-password.test.ts index 35b4f5cc64..6e7ccf44fa 100644 --- a/packages/better-auth/src/api/routes/forget-password.test.ts +++ b/packages/better-auth/src/api/routes/forget-password.test.ts @@ -142,3 +142,93 @@ describe("forget password", async (it) => { expect(res2.error?.status).toBe(400); }); }); + +describe("revoke sessions on password reset", async (it) => { + const mockSendEmail = vi.fn(); + let token = ""; + + const { client, testUser, signInWithTestUser } = await getTestInstance( + { + emailAndPassword: { + enabled: true, + async sendResetPassword({ url }) { + token = url.split("?")[0].split("/").pop() || ""; + await mockSendEmail(); + }, + revokeSessionsOnPasswordReset: true, + }, + }, + { + testWith: "sqlite", + }, + ); + + it("should revoke other sessions when revokeSessionsOnPasswordReset is enabled", async () => { + const { headers } = await signInWithTestUser(); + + await client.forgetPassword({ + email: testUser.email, + redirectTo: "http://localhost:3000", + }); + + await client.resetPassword( + { + newPassword: "new-password", + }, + { + query: { + token, + }, + }, + ); + + const sessionAttempt = await client.getSession({ + fetchOptions: { + headers, + }, + }); + expect(sessionAttempt.data).toBeNull(); + }); + + it("should not revoke other sessions by default", async () => { + const { client, testUser, signInWithTestUser } = await getTestInstance( + { + emailAndPassword: { + enabled: true, + async sendResetPassword({ url }) { + token = url.split("?")[0].split("/").pop() || ""; + await mockSendEmail(); + }, + }, + }, + { + testWith: "sqlite", + }, + ); + + const { headers } = await signInWithTestUser(); + + await client.forgetPassword({ + email: testUser.email, + redirectTo: "http://localhost:3000", + }); + + await client.resetPassword( + { + newPassword: "new-password", + }, + { + query: { + token, + }, + }, + ); + + const sessionAttempt = await client.getSession({ + fetchOptions: { + headers, + }, + }); + expect(sessionAttempt.data?.user).toBeDefined(); + }); +}); diff --git a/packages/better-auth/src/api/routes/forget-password.ts b/packages/better-auth/src/api/routes/forget-password.ts index 1be49d465a..4ad6407d80 100644 --- a/packages/better-auth/src/api/routes/forget-password.ts +++ b/packages/better-auth/src/api/routes/forget-password.ts @@ -285,7 +285,9 @@ export const resetPassword = createAuthEndpoint( ctx, ); await ctx.context.internalAdapter.deleteVerificationValue(verification.id); - + if (ctx.context.options.emailAndPassword?.revokeSessionsOnPasswordReset) { + await ctx.context.internalAdapter.deleteSessions(userId); + } return ctx.json({ status: true, }); diff --git a/packages/better-auth/src/types/options.ts b/packages/better-auth/src/types/options.ts index 2b2b95c382..3828fc2f6a 100644 --- a/packages/better-auth/src/types/options.ts +++ b/packages/better-auth/src/types/options.ts @@ -239,6 +239,11 @@ export type BetterAuthOptions = { * Automatically sign in the user after sign up */ autoSignIn?: boolean; + /** + * Whether to revoke all other sessions when resetting password + * @default false + */ + revokeSessionsOnPasswordReset?: boolean; }; /** * list of social providers