fix: reset password email reset token expiration (#533)

This commit is contained in:
Bereket Engida
2024-11-14 20:58:53 +03:00
committed by GitHub
parent 958b8af41e
commit f2cf85d450
4 changed files with 71 additions and 10 deletions

View File

@@ -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);
});
});

View File

@@ -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",

View File

@@ -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 = "";

View File

@@ -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;
/**