BA
Better Auth•2w ago
stu

Invalid token on resetPassword

I'm creating a flow for when a signed out user forgets their password, they can reset it.
export const auth = betterAuth({
database: {
dialect,
type: "postgres",
},
emailAndPassword: {
enabled: true,
sendResetPassword: async (
{ user, token, url }: { user: User; token: string; url: string },
_
) => {
await sendResetPasswordEmail({
email: user.email,
url,
});
},
resetPasswordTokenExpiresIn: 3600, // 1 hour
},
trustedOrigins: ["http://localhost:4321"],
});
export const auth = betterAuth({
database: {
dialect,
type: "postgres",
},
emailAndPassword: {
enabled: true,
sendResetPassword: async (
{ user, token, url }: { user: User; token: string; url: string },
_
) => {
await sendResetPasswordEmail({
email: user.email,
url,
});
},
resetPasswordTokenExpiresIn: 3600, // 1 hour
},
trustedOrigins: ["http://localhost:4321"],
});
It's sending me an email with a URL like http://localhost:4321/reset-password/<TOKEN>. Great! But when I try and use the token to reset the password, I always get a BAD_REQUEST with error INVALID_TOKEN. I'm trying to resetPassword server-side, and the code looks like this:
const result = await auth.api.resetPassword({
body: {
newPassword: password,
token,
},
});
const result = await auth.api.resetPassword({
body: {
newPassword: password,
token,
},
});
9 Replies
stu
stuOP•2w ago
Oh cool, the team is on top of it. Thanks! Was going crazy thinking I misconfigured something haha
SoSweetHam
SoSweetHam•2w ago
hehe same 😄 oh wait a sec, you are getting a pre-cursory step wrong if it's a bad token issue, reset password still won't work until the fix in that pr though
stu
stuOP•2w ago
Yeah, not sure if I'm misunderstanding the flow or something. Using the exact token that is generated.
SoSweetHam
SoSweetHam•2w ago
how are you getting the token? on the client
stu
stuOP•2w ago
I'm doing it server side, simply const { token } = Astro.params; That parses the token from the URL, structured like /reset-password/<token>. It looks to be logging the same value that's generated in sendResetPassword.
import { auth } from "../../../lib/auth";

const { token } = Astro.params;

if (!token) {
return Astro.redirect("/signin");
}

if (Astro.request.method === "POST") {

const data = await Astro.request.formData();
const password = data.get("password")?.toString();

if (!password || password?.length < 6) {
return Astro.redirect("/reset-password?error=1");
}

try {

const result = await auth.api.resetPassword({
body: {
newPassword: password,
token,
},
});

Astro.redirect("/signin");
} catch (error) {
return Astro.redirect("/reset-password?error=1");
}
}
import { auth } from "../../../lib/auth";

const { token } = Astro.params;

if (!token) {
return Astro.redirect("/signin");
}

if (Astro.request.method === "POST") {

const data = await Astro.request.formData();
const password = data.get("password")?.toString();

if (!password || password?.length < 6) {
return Astro.redirect("/reset-password?error=1");
}

try {

const result = await auth.api.resetPassword({
body: {
newPassword: password,
token,
},
});

Astro.redirect("/signin");
} catch (error) {
return Astro.redirect("/reset-password?error=1");
}
}
Digging into this more, it's a bit odd.
const ctx = await auth.$context;
const verificationVal = await ctx.internalAdapter.findVerificationValue(
`reset-password:${token}`
);
const ctx = await auth.$context;
const verificationVal = await ctx.internalAdapter.findVerificationValue(
`reset-password:${token}`
);
If I add this to my Astro page on load, it finds the correct row with the matching user id. I use the same token in the resetPassword body, which fails.
SoSweetHam
SoSweetHam•2w ago
this token is different, the token is generated by the page which is the sort of magic link you get in the email that link then sends you to the callback url which would be your frontend page where you have a form that asks for a new password
stu
stuOP•2w ago
Thanks for the response, I'm not fully following the flow here. Let's say the token generated and in sendResetPassword is ABC. The URL generated is /reset-password/ABC?callbackURL=/dashboard (maybe my use of callbackURL is incorrect?). By clicking this link the email, it navigates to https://localhost:4321/reset-password/ABC?callbackURL=/dashboard, where I have a form to set a new password. When submitting the form, we parse the token ABC. And then, for testing, checking
ctx.internalAdapter.findVerificationValue(
`reset-password:${token}`
);
ctx.internalAdapter.findVerificationValue(
`reset-password:${token}`
);
, which is finding a row with the correct user. But then passing this same token to resetPassword, I'm getting an INVALID_TOKEN error.

Did you find this page helpful?