Plugins

Two Factor

Add TOTP and backup-code challenges after password or username sign-in.

The two-factor plugin adds a second step to credential sign-in. When a user with 2FA enabled signs in with email/password or username/password, Kernia withholds the session and returns { twoFactorRedirect: true }. The user must verify a TOTP code, an emailed OTP, or a backup code before a real session is issued.

Installation

Add the plugin to your server

Pass two_factor() to your Kernia config. The plugin requires pyotp (installed via the two-factor extra). Configuration — issuer, TOTP settings, and the optional OTP sender — lives under advanced["two-factor"].

auth.py
import os
from kernia import KerniaOptions
from kernia.auth import init
from kernia.plugins.two_factor import two_factor

async def send_otp(data: dict, ctx) -> None:
    # data = {"user": <user row>, "otp": "123456"}
    await email_client.send(
        to=data["user"]["email"],
        subject="Your verification code",
        html=f"<p>Your code is <strong>{data['otp']}</strong></p>",
    )

auth = init(KerniaOptions(
    database=adapter,
    secret=os.environ["KERNIA_SECRET"],
    base_url=os.environ["KERNIA_BASE_URL"],
    plugins=[two_factor()],
    advanced={
        "two-factor": {
            "issuer": "Acme",
            "otp_options": {"send_otp": send_otp},
            "totp_options": {"digits": 6, "period": 30},
        }
    },
))

Add the client plugin

Kernia speaks the Better Auth wire protocol, so the official JavaScript client works unchanged. Add the twoFactorClient plugin and handle the redirect that signals a second factor is required.

auth-client.ts
import { createAuthClient } from "better-auth/client";
import { twoFactorClient } from "better-auth/client/plugins";

export const authClient = createAuthClient({
  baseURL: "/api/auth",
  plugins: [
    twoFactorClient({
      onTwoFactorRedirect({ twoFactorMethods }) {
        // e.g. ["totp", "otp"] — route the user to your 2FA screen
        window.location.href = "/two-factor";
      },
    }),
  ],
});

Usage

Enable two factor

Enabling requires the user's password. Kernia returns a totpURI to render as a QR code in an authenticator app, plus backup codes.

const { data } = await authClient.twoFactor.enable({
  password: "the-password",
});
// data.totpURI -> render as QR; data.backupCodes -> show once

Verify during setup or sign-in

After credential sign-in returns twoFactorRedirect, collect the code and verify it. trustDevice skips the second factor on this browser for future sign-ins.

await authClient.twoFactor.verifyTotp({
  code: "123456",
  trustDevice: true,
});

Email OTP as a second factor

If you configured otp_options.send_otp, the user can request a one-time code instead of using their authenticator app.

await authClient.twoFactor.sendOtp();           // delivers via your send_otp
await authClient.twoFactor.verifyOtp({ code: "123456" });

Backup codes

// Regenerate (requires password) — show the new set once
await authClient.twoFactor.generateBackupCodes({ password: "the-password" });

// Use one to clear a 2FA challenge when the device is unavailable
await authClient.twoFactor.verifyBackupCode({ code: "a1b2-c3d4" });

Disable two factor

await authClient.twoFactor.disable({ password: "the-password" });

Options

Configure under advanced["two-factor"]:

OptionTypeDefaultDescription
issuerstrapp nameLabel shown in authenticator apps.
otp_options.send_otpasync ({user, otp}, ctx) -> NoneDelivers the email/SMS OTP second factor. Required to use the OTP flow.
totp_optionsdict{digits: 6, period: 30}TOTP algorithm settings (digits, period, disable).
skip_verification_on_enableboolFalseEnable 2FA immediately without a verification step.
trust_device_max_ageint2592000Trust-device cookie lifetime in seconds (30 days).
two_factor_cookie_max_ageint600Lifetime of the pending-challenge cookie in seconds.

Schema

The plugin adds a twoFactor table and supporting tables, and extends user:

twoFactor.id
stringrequired
Primary key.
twoFactor.userId
stringrequired
References user.id.
twoFactor.secret
stringrequired
TOTP secret (not returned).
twoFactor.backupCodes
stringrequired
Hashed backup codes (not returned).
twoFactor.verified
booleanrequired
Whether the factor is confirmed.
user.twoFactorEnabled
booleanrequired
Whether 2FA is active for the user.

It also adds twoFactorConfirmation and twoFactorBackupCode tables. Run kernia generate after enabling the plugin.

The sign-in gate is an after-hook on /sign-in/email, /sign-in/username, and /sign-in/phone-number: it deletes the provisional session and returns twoFactorRedirect. Always handle onTwoFactorRedirect on the client, or sign-in will appear to silently fail for 2FA users.