Session duplication bug when two-factor plugin enabled
I have a project that shows otp-modal if two-factor validation is active. The normal is that it creates a session after entering the correct otp code. But as soon as i enter the password the session is created and after i enter the otp code correctly another session is created. What is the solution for this?
actions/sign-in.ts -->
sign-in-form -->
"use server";
import { auth } from "@/lib/auth";
import { signInSchema } from "@/schemas";
import { APIError } from "better-auth/api";
import { z } from "zod";
export const signIn = async (values: z.infer<typeof signInSchema>) => {
const validatedData = signInSchema.parse(values);
try {
const response = await auth.api.signInEmail({
body: {
email: validatedData.email,
password: validatedData.password,
callbackURL: "/verify-email?success=true",
},
asResponse: true,
});
if (!response.ok) {
throw new Error("Failed to sign in");
}
const data = await response.json();
console.log("Sign-in Response:", data);
if (data.twoFactorRedirect) return { twoFactorRedirect: true };
return null;
} catch (error) {
if (error instanceof APIError) {
if (error.status === "UNAUTHORIZED") {
throw new Error("Invalid email or password");
}
if (error.status === "FORBIDDEN") {
throw new Error("Email not verified");
}
}
throw new Error("Something went wrong.");
}
};
"use server";
import { auth } from "@/lib/auth";
import { signInSchema } from "@/schemas";
import { APIError } from "better-auth/api";
import { z } from "zod";
export const signIn = async (values: z.infer<typeof signInSchema>) => {
const validatedData = signInSchema.parse(values);
try {
const response = await auth.api.signInEmail({
body: {
email: validatedData.email,
password: validatedData.password,
callbackURL: "/verify-email?success=true",
},
asResponse: true,
});
if (!response.ok) {
throw new Error("Failed to sign in");
}
const data = await response.json();
console.log("Sign-in Response:", data);
if (data.twoFactorRedirect) return { twoFactorRedirect: true };
return null;
} catch (error) {
if (error instanceof APIError) {
if (error.status === "UNAUTHORIZED") {
throw new Error("Invalid email or password");
}
if (error.status === "FORBIDDEN") {
throw new Error("Email not verified");
}
}
throw new Error("Something went wrong.");
}
};
import { twoFactor } from "@/lib/auth-client";
const onSubmit = (values: z.infer<typeof signInSchema>) => {
startTransition(() => {
signIn(values)
.then(async (data) => {
if (data?.twoFactorRedirect) {
await twoFactor.sendOtp();
otpModal.onOpen();
}
router.push("/dashboard");
})
.catch((error) => toast.error(error.message));
});
};
import { twoFactor } from "@/lib/auth-client";
const onSubmit = (values: z.infer<typeof signInSchema>) => {
startTransition(() => {
signIn(values)
.then(async (data) => {
if (data?.twoFactorRedirect) {
await twoFactor.sendOtp();
otpModal.onOpen();
}
router.push("/dashboard");
})
.catch((error) => toast.error(error.message));
});
};
4 Replies
otp-modal -->
any help please 😦
import { twoFactor } from "@/lib/auth-client";
const onSubmit = async (values: z.infer<typeof formSchema>) => {
const validatedData = formSchema.parse(values);
try {
await twoFactor.verifyOtp(
{
code: validatedData.code,
trustDevice: validatedData.trustDevice,
},
{
onSuccess() {
onClose();
router.push("/dashboard");
},
onError(ctx) {
setIsInvalidCode(true);
throw new Error(ctx.error.message);
},
}
);
} catch (error) {
toast.error((error as Error).message);
}
};
import { twoFactor } from "@/lib/auth-client";
const onSubmit = async (values: z.infer<typeof formSchema>) => {
const validatedData = formSchema.parse(values);
try {
await twoFactor.verifyOtp(
{
code: validatedData.code,
trustDevice: validatedData.trustDevice,
},
{
onSuccess() {
onClose();
router.push("/dashboard");
},
onError(ctx) {
setIsInvalidCode(true);
throw new Error(ctx.error.message);
},
}
);
} catch (error) {
toast.error((error as Error).message);
}
};
which version of better auth are you using?
also could you send me your auth config?
"better-auth": "^1.2.3"
I'm really tired of looking for a solution to this problem. another source code I looked at on the internet has the same problem. I don't think the problem is caused by my code.
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { db } from "./db";
import { nextCookies } from "better-auth/next-js";
import { sendEmail } from "@/actions/send-email";
import { twoFactor } from "better-auth/plugins";
export const auth = betterAuth({
appName: "Better Auth",
database: prismaAdapter(db, {
provider: "postgresql",
}),
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
sendResetPassword: async ({ user, url, token }) => {
const newToken = `reset-password:${token}`;
if (!user.emailVerified) {
const existingToken = await db.verification.findFirst({
where: {
identifier: newToken,
},
});
await db.verification.delete({
where: {
id: existingToken?.id,
},
});
throw new Error();
}
await sendEmail({
email: user.email,
subject: "Reset your password",
text: `Click the link to reset your password: ${url}`,
});
},
},
emailVerification: {
sendOnSignUp: true,
autoSignInAfterVerification: true,
sendVerificationEmail: async ({ user, url }) => {
await sendEmail({
email: user.email,
subject: "Verify your email address",
text: `Click the link to verify your email: ${url}`,
});
},
},
user: {
deleteUser: {
enabled: true,
},
},
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
},
plugins: [
twoFactor({
otpOptions: {
async sendOTP({ user, otp }) {
await sendEmail({
email: user.email,
subject: "2FA",
text: `Your OTP is ${otp}`,
});
},
},
skipVerificationOnEnable: true,
}),
nextCookies(),
],
});
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { db } from "./db";
import { nextCookies } from "better-auth/next-js";
import { sendEmail } from "@/actions/send-email";
import { twoFactor } from "better-auth/plugins";
export const auth = betterAuth({
appName: "Better Auth",
database: prismaAdapter(db, {
provider: "postgresql",
}),
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
sendResetPassword: async ({ user, url, token }) => {
const newToken = `reset-password:${token}`;
if (!user.emailVerified) {
const existingToken = await db.verification.findFirst({
where: {
identifier: newToken,
},
});
await db.verification.delete({
where: {
id: existingToken?.id,
},
});
throw new Error();
}
await sendEmail({
email: user.email,
subject: "Reset your password",
text: `Click the link to reset your password: ${url}`,
});
},
},
emailVerification: {
sendOnSignUp: true,
autoSignInAfterVerification: true,
sendVerificationEmail: async ({ user, url }) => {
await sendEmail({
email: user.email,
subject: "Verify your email address",
text: `Click the link to verify your email: ${url}`,
});
},
},
user: {
deleteUser: {
enabled: true,
},
},
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
},
plugins: [
twoFactor({
otpOptions: {
async sendOTP({ user, otp }) {
await sendEmail({
email: user.email,
subject: "2FA",
text: `Your OTP is ${otp}`,
});
},
},
skipVerificationOnEnable: true,
}),
nextCookies(),
],
});
It could be a bug from our side. we actively delete the first session created. I'm not sure why it's not the case on your side. But I'll check.