mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-25 00:22:43 -05:00
fix: reset password email reset token expiration (#533)
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
@@ -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<void>;
|
||||
/**
|
||||
* 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;
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user