BA
Better Auth•2mo ago
ragnar

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 -->
"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.");
}
};
sign-in-form -->
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
ragnar
ragnarOP•2mo ago
otp-modal -->
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);
}
};
any help please 😦
bekacru
bekacru•2mo ago
which version of better auth are you using? also could you send me your auth config?
ragnar
ragnarOP•2mo ago
"better-auth": "^1.2.3"
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(),
],
});
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.
bekacru
bekacru•2mo ago
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.

Did you find this page helpful?