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)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();| Option | Type | Description |
|---|---|---|
issuer | string | Name shown in the authenticator app. Defaults to appName. |
window | number | Drift steps tolerated on verify. Default 1 (±30s). |
totp | TotpOptions | Digits / 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)
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.// 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 itRadon 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 stateNext steps
Passkeys / WebAuthn
Phishing-resistant biometric sign-in with passkeys. Radon runs the server-side ceremonies; your frontend calls the browser WebAuthn APIs. A Radon Pro feature.
Phone / SMS OTP
Passwordless sign-in with a one-time code over SMS. The same flow as email codes, delivered by Twilio (or any SMS sender). A Radon Pro feature.