From f2cf85d450fa39ccafd405a6d8a269e535c7b5c8 Mon Sep 17 00:00:00 2001 From: Bereket Engida <86073083+Bekacru@users.noreply.github.com> Date: Thu, 14 Nov 2024 20:58:53 +0300 Subject: [PATCH] fix: reset password email reset token expiration (#533) --- .../src/api/routes/forget-password.test.ts | 56 +++++++++++++++++++ .../src/api/routes/forget-password.ts | 15 +++-- .../plugins/phone-number/phone-number.test.ts | 1 - packages/better-auth/src/types/options.ts | 9 ++- 4 files changed, 71 insertions(+), 10 deletions(-) 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 75b0248d60..19e649f9f7 100644 --- a/packages/better-auth/src/api/routes/forget-password.test.ts +++ b/packages/better-auth/src/api/routes/forget-password.test.ts @@ -67,4 +67,60 @@ describe("forget password", async (it) => { expect(res.error?.status).toBe(400); }); + + it("should expire", async () => { + const { client, signInWithTestUser, testUser } = await getTestInstance({ + emailAndPassword: { + enabled: true, + async sendResetPassword({ token: _token }) { + token = _token; + await mockSendEmail(); + }, + resetPasswordTokenExpiresIn: 10, + }, + }); + const { headers } = await signInWithTestUser(); + await client.forgetPassword({ + email: testUser.email, + redirectTo: "/sign-in", + fetchOptions: { + headers, + }, + }); + vi.useFakeTimers(); + await vi.advanceTimersByTimeAsync(1000 * 9); + const callbackRes = await client.$fetch("/reset-password/:token", { + params: { + token, + }, + query: { + callbackURL: "/cb", + }, + onError(context) { + const location = context.response.headers.get("location"); + expect(location).not.toContain("error"); + expect(location).toContain("token"); + }, + }); + console.log({ callbackRes }); + const res = await client.resetPassword({ + newPassword: "new-password", + token, + }); + expect(res.data?.status).toBe(true); + await client.forgetPassword({ + email: testUser.email, + redirectTo: "/sign-in", + fetchOptions: { + headers, + }, + }); + vi.useFakeTimers(); + await vi.advanceTimersByTimeAsync(1000 * 11); + const res2 = await client.resetPassword({ + newPassword: "new-password", + token, + }); + expect(res2.error?.status).toBe(400); + }); }); diff --git a/packages/better-auth/src/api/routes/forget-password.ts b/packages/better-auth/src/api/routes/forget-password.ts index 3a15fa3713..89d3956169 100644 --- a/packages/better-auth/src/api/routes/forget-password.ts +++ b/packages/better-auth/src/api/routes/forget-password.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { createAuthEndpoint } from "../call"; import { APIError } from "better-call"; import type { AuthContext } from "../../init"; +import { getDate } from "../../utils/date"; function redirectError( ctx: AuthContext, @@ -75,11 +76,10 @@ export const forgetPassword = createAuthEndpoint( ); } const defaultExpiresIn = 60 * 60 * 1; - const expiresAt = new Date( - Date.now() + - 1000 * - (ctx.context.options.emailAndPassword.resetPasswordTokenExpiresIn || - defaultExpiresIn), + const expiresAt = getDate( + ctx.context.options.emailAndPassword.resetPasswordTokenExpiresIn || + defaultExpiresIn, + "sec", ); const verificationToken = ctx.context.uuid(); await ctx.context.internalAdapter.createVerificationValue({ @@ -92,6 +92,7 @@ export const forgetPassword = createAuthEndpoint( { user: user.user, url, + token: verificationToken, }, ctx.request, ); @@ -126,6 +127,7 @@ export const forgetPasswordCallback = createAuthEndpoint( redirectError(ctx.context, callbackURL, { error: "INVALID_TOKEN" }), ); } + throw ctx.redirect(redirectCallback(ctx.context, callbackURL, { token })); }, ); @@ -142,10 +144,12 @@ export const resetPassword = createAuthEndpoint( method: "POST", body: z.object({ newPassword: z.string(), + token: z.string().optional(), }), }, async (ctx) => { const token = + ctx.body.token || ctx.query?.token || (ctx.query?.currentURL ? new URL(ctx.query.currentURL).searchParams.get("token") @@ -159,7 +163,6 @@ export const resetPassword = createAuthEndpoint( const id = `reset-password:${token}`; const verification = await ctx.context.internalAdapter.findVerificationValue(id); - if (!verification || verification.expiresAt < new Date()) { throw new APIError("BAD_REQUEST", { message: "Invalid token", diff --git a/packages/better-auth/src/plugins/phone-number/phone-number.test.ts b/packages/better-auth/src/plugins/phone-number/phone-number.test.ts index 9ca391df06..62e8210c70 100644 --- a/packages/better-auth/src/plugins/phone-number/phone-number.test.ts +++ b/packages/better-auth/src/plugins/phone-number/phone-number.test.ts @@ -3,7 +3,6 @@ import { getTestInstance } from "../../test-utils/test-instance"; import { phoneNumber } from "."; import { createAuthClient } from "../../client"; import { phoneNumberClient } from "./client"; -import { changeEmail } from "../../api"; describe("phone-number", async (it) => { let otp = ""; diff --git a/packages/better-auth/src/types/options.ts b/packages/better-auth/src/types/options.ts index 91fec5ac9d..b08f4eb4c4 100644 --- a/packages/better-auth/src/types/options.ts +++ b/packages/better-auth/src/types/options.ts @@ -186,16 +186,19 @@ export interface BetterAuthOptions { * @param user the user to send the * reset password email to * @param url the url to send the reset password email to + * @param token the token to send to the user (could be used instead of sending the url + * if you need to redirect the user to custom route) */ - data: { user: User; url: string }, + data: { user: User; url: string; token: string }, /** * The request object */ request?: Request, ) => Promise; /** - * Number of seconds the reset password token is valid for. - * @default 1 hour + * Number of seconds the reset password token is + * valid for. + * @default 1 hour (60 * 60) */ resetPasswordTokenExpiresIn?: number; /**