Overriding TOTP Period Causes Unauthorized / Invalid Two-Factor Cookie Error

When I override the default TOTP period (e.g. from 30 seconds to 60 seconds) in the Better Auth 2FA TOTP plugin configuration. The attempt to verify totp code end up in resulting in Unauthorized response and an Invalid two-factor cookie message. If you dont override the default period. The code works fine. To Reproduce
export const auth = betterAuth({
plugins: [
twoFactor({
totpOptions: {
period: 60
},
},
],
})
export const auth = betterAuth({
plugins: [
twoFactor({
totpOptions: {
period: 60
},
},
],
})
I was using Sveltekit when i encountered this error
No description
5 Replies
Budi
Budi4mo ago
I'm having the same issue but haven't changed the defaults. Would you mind sharing your implementation @misterh? I'd be curious if you got it to work.
misterh
misterhOP4mo ago
Hi yes, mine started to work when I removed the custom totp period I can share my code and more details in couples hours when im home
Budi
Budi4mo ago
That would be terrific. Thank you @misterh.
misterh
misterhOP4mo ago
this is my auth.ts file and configuration
export const auth = betterAuth({
appName: "MyApp",
database: new Pool({
user: "appuser",
database: "appdb",
port: 5432,
host: "localhost",
password: "pass",
ssl: false
}),

secondaryStorage: {
get: async (key) => {
const value = await redis.get(key);
return value ? value : null;
},
set: async (key, value, ttl) => {
if (ttl) await redis.set(key, value, { EX: ttl });
// or for ioredis:
// if (ttl) await redis.set(key, value, 'EX', ttl)
else await redis.set(key, value);
},
delete: async (key) => {
await redis.del(key);
}
},

user: {
additionalFields: {
bio: {
type: "string",
required: false,
defaultValue: "",
}
}
},

emailAndPassword: {
enabled: true,
resetPasswordTokenExpiresIn: 900, // 15 minutes

sendResetPassword: async ({ user, url, token }) => {
await sendResetPasswordEmail({
email: user.email,
resetLink: `${url}`
})
}
},

emailVerification: {
sendVerificationEmail: async ({ user, url }) => {
await sendEmailVerificationMail({
email: user.email,
verificationLink: url
})
},

sendOnSignUp: true
},

socialProviders: {
google: {
clientId: GOOGLE_CLIENT_ID,
clientSecret: GOOGLE_CLIENT_SECRET
}
},
// session: {
// cookieCache: {
// enabled: true,
// maxAge: 10 * 60, // Cache duration in seconds
// },
// },
account: {
accountLinking: {
enabled: true,
trustedProviders: ["google", "email-password"],
}
},

plugins: [
twoFactor({
// skipVerificationOnEnable: true,

otpOptions: {
period: 90,

async sendOTP({ user, otp }) {
await sendOtpCode({
email: user.email,
otp
})
}
}
})
]
})
export const auth = betterAuth({
appName: "MyApp",
database: new Pool({
user: "appuser",
database: "appdb",
port: 5432,
host: "localhost",
password: "pass",
ssl: false
}),

secondaryStorage: {
get: async (key) => {
const value = await redis.get(key);
return value ? value : null;
},
set: async (key, value, ttl) => {
if (ttl) await redis.set(key, value, { EX: ttl });
// or for ioredis:
// if (ttl) await redis.set(key, value, 'EX', ttl)
else await redis.set(key, value);
},
delete: async (key) => {
await redis.del(key);
}
},

user: {
additionalFields: {
bio: {
type: "string",
required: false,
defaultValue: "",
}
}
},

emailAndPassword: {
enabled: true,
resetPasswordTokenExpiresIn: 900, // 15 minutes

sendResetPassword: async ({ user, url, token }) => {
await sendResetPasswordEmail({
email: user.email,
resetLink: `${url}`
})
}
},

emailVerification: {
sendVerificationEmail: async ({ user, url }) => {
await sendEmailVerificationMail({
email: user.email,
verificationLink: url
})
},

sendOnSignUp: true
},

socialProviders: {
google: {
clientId: GOOGLE_CLIENT_ID,
clientSecret: GOOGLE_CLIENT_SECRET
}
},
// session: {
// cookieCache: {
// enabled: true,
// maxAge: 10 * 60, // Cache duration in seconds
// },
// },
account: {
accountLinking: {
enabled: true,
trustedProviders: ["google", "email-password"],
}
},

plugins: [
twoFactor({
// skipVerificationOnEnable: true,

otpOptions: {
period: 90,

async sendOTP({ user, otp }) {
await sendOtpCode({
email: user.email,
otp
})
}
}
})
]
})
async function verifyPassword() {
isLoading = true;
error = null;

try {
if (is2faEnabled) {
await authClient.twoFactor.disable(
{
password: password
},
{
onSuccess: () => {
isLoading = false;
onConfirm(password);
toast.success('Two-factor authentication disabled successfully');
},
onError: (ctx) => {
isLoading = false;
error = ctx.error.message;
toast.error(`Failed to disable two-factor authentication. ${ctx.error.message}`);
}
}
);
} else {
console.log('Enable 2FA');
await authClient.twoFactor.enable(
{
password: password
},
{
onSuccess: (ctx) => {
isLoading = false;
totpUri = ctx.data.totpURI;
step = 'verify';
},
onError: (ctx) => {
isLoading = false;
error = ctx.error.message;
toast.error(`Failed to enable two-factor authentication. ${ctx.error.message}`);
}
}
);
}
} catch (err) {
toast.error(`Failed to ${is2faEnabled ? 'disable' : 'enable'} two-factor authentication`);
isLoading = false;
}
}
async function verifyPassword() {
isLoading = true;
error = null;

try {
if (is2faEnabled) {
await authClient.twoFactor.disable(
{
password: password
},
{
onSuccess: () => {
isLoading = false;
onConfirm(password);
toast.success('Two-factor authentication disabled successfully');
},
onError: (ctx) => {
isLoading = false;
error = ctx.error.message;
toast.error(`Failed to disable two-factor authentication. ${ctx.error.message}`);
}
}
);
} else {
console.log('Enable 2FA');
await authClient.twoFactor.enable(
{
password: password
},
{
onSuccess: (ctx) => {
isLoading = false;
totpUri = ctx.data.totpURI;
step = 'verify';
},
onError: (ctx) => {
isLoading = false;
error = ctx.error.message;
toast.error(`Failed to enable two-factor authentication. ${ctx.error.message}`);
}
}
);
}
} catch (err) {
toast.error(`Failed to ${is2faEnabled ? 'disable' : 'enable'} two-factor authentication`);
isLoading = false;
}
}
to enable the totp you need to call authClient.twoFactor.enable, its need user account password. and if successfully, it will generate the totp uri, secret and backcode and will add them to the db. you can convert the totp uri into a qr code and let user scan and add it to their authentication app, then prompt user to get the code and call authClient.twoFactor.verifyTotp and then will enable the totp and 2fa auth for user
async function verifyCode() {
isLoading = true;
error = null;

try {
await authClient.twoFactor.verifyTotp({
code
}, {
onSuccess: () => {
isLoading = false;
onConfirm(code);
toast.success('Two-factor authentication enabled successfully');
},
onError: (ctx) => {
isLoading = false;
error = ctx.error.message;
toast.error(`Failed to enable two-factor authentication. ${ctx.error.message}`);
}
})

} catch {
isLoading = false;
error = 'Invalid code';
toast.error('Invalid code');
}
}
async function verifyCode() {
isLoading = true;
error = null;

try {
await authClient.twoFactor.verifyTotp({
code
}, {
onSuccess: () => {
isLoading = false;
onConfirm(code);
toast.success('Two-factor authentication enabled successfully');
},
onError: (ctx) => {
isLoading = false;
error = ctx.error.message;
toast.error(`Failed to enable two-factor authentication. ${ctx.error.message}`);
}
})

} catch {
isLoading = false;
error = 'Invalid code';
toast.error('Invalid code');
}
}
What the exact error are you getting @Budi To enable two-factor authentication (2FA), first call authClient.twoFactor.enable({ password: "userPassword" }). This will generate a TOTP URI and save the secret and backup codes to the database, but it does not activate 2FA yet. Next, prompt the user to scan the TOTP URI with an authenticator app like Google Authenticator or Authy. Once the user has added the code to their app, ask them to enter the 6-digit code generated by the app. Then call authClient.twoFactor.verifyTotp({ code: "123456" }). If the verification is successful, 2FA will be fully enabled for the user.
Budi
Budi4mo ago
Thanks for sharing this. I see you're using the client-side Auth client. I'm trying to do this server-side. I think the issue is that the correct cookies aren't being transmitted from the server-side calls to ensure the verifyTOTP call works. I discovered the issue was that I wasn't passing headers to my server signInEmail API call. Therefore it couldn't receive and set the cookie header.

Did you find this page helpful?