Radon
Auth Methods

2FA / TOTP

Add an authenticator-app second factor (Google Authenticator, 1Password, Authy) on top of any primary sign-in method. A Radon Pro feature.

Radon Pro

2FA/TOTP is a Pro feature and requires a license. It also requires an encryption key (RADON_ENCRYPTION_KEY) because a TOTP secret must be stored reversibly. See Radon Pro.

TOTP adds a second factor — the rotating 6-digit code from an authenticator app — on top of any primary method (email code, password, OAuth, …). It's compatible with Google Authenticator, 1Password, Authy, and anything else that speaks the standard, and passes the RFC 6238 test vectors.

1. Enable the provider

# TOTP secrets are reversible, so they're encrypted at rest.
RADON_ENCRYPTION_KEY=$(openssl rand -hex 32)
lib/auth.ts
export const auth = new Radon({
  adapter: postgresAdapter(pool),
  session: { secret: process.env.RADON_SECRET! },
  licenseKey: process.env.RADON_LICENSE_KEY,
  encryptionKey: process.env.RADON_ENCRYPTION_KEY, // or set the env var and omit this
  appName: "Acme",
  providers: {
    totp: { issuer: "Acme" }, // issuer shown in the authenticator app
  },
});

await auth.init();
OptionTypeDescription
issuerstringName shown in the authenticator app. Defaults to appName.
windownumberDrift steps tolerated on verify. Default 1 (±30s).
totpTotpOptionsDigits / period / algorithm overrides (defaults are standard).

Enrollment fails without an encryption key

A TOTP secret has to be read back to verify codes, so it's encrypted, not hashed. If no RADON_ENCRYPTION_KEY is configured, beginEnrollment throws EncryptionRequiredError.

2. Enroll the user (two steps)

Step 1 — show the QR
const { secret, uri } = await auth.totp.beginEnrollment(userId);
// Render `uri` as a QR code for the user to scan.
// `secret` is the base32 string for manual entry.
Step 2 — confirm they scanned it
// The user enters the current 6-digit code from their app:
const { recoveryCodes } = await auth.totp.confirmEnrollment(userId, codeFromApp);
// 2FA is now ENABLED. Show `recoveryCodes` to the user ONCE — only hashes are stored.

3. Verify at login

After the primary factor succeeds, require the TOTP code as the second step:

const ok = await auth.totp.verify(userId, codeFromApp); // → boolean
if (!ok) throw new Error("Invalid 2FA code");

// Lost their device? Accept a one-time recovery code instead:
const recovered = await auth.totp.verifyRecoveryCode(userId, recoveryCode); // consumes it

Radon verifies the factor; you gate the session

verify returns a boolean — it's up to your flow to only issue the session cookie after both factors pass. TOTP layers on top of a primary method; it doesn't replace it.

Managing 2FA

await auth.totp.isEnabled(userId);                // boolean
await auth.totp.regenerateRecoveryCodes(userId);  // returns a fresh set, invalidates the old
await auth.totp.disable(userId);                  // turn 2FA off and wipe all TOTP state

Next steps

On this page