auth.factors
Enrolling multi-factor methods and managing recovery codes.
Factors are what turn a sign-in into a two-step sign-in. This namespace is the
management half — a signed-in user setting up or removing their own methods.
The other half, the challenge that appears at sign-in afterwards, is
auth.challenges.
Every call here except enable is sudo-protected under manage_mfa. That is
deliberate: a stolen session must not be able to remove the second factor that
would have stopped it.
list
list() → AuthMethod[]
The factors configured for the current user.
const { data: factors } = await nulvless.auth.factors.list();idstringOptionalWhat unenroll takes.
typeMFAMethodRequiredtotp, sms_otp, email_otp or recovery_codes.
namestringOptionalWhatever the user called it at enrolment — "Work phone", "1Password".
statusAuthFactorStatusRequiredconfiguredSet up and usable.not_configuredAvailable on this environment, not set up.created_atstringOptionalAn empty result after sign-in is the signal to walk the user through enrolment, which is how you satisfy an environment that requires MFA.
enroll
enroll({ type, name, phone, nonce }) → FactorEnrollment
Begins setting up a factor. Nothing is active yet.
const { data } = await nulvless.auth.factors.enroll({
type: 'totp',
name: 'My authenticator',
nonce: code,
});
// data.url → render as a QR code
// data.secret → manual entrytypeMFAMethodRequiredtotpAn authenticator app. Returns a secret and a provisioning URL.sms_otpCodes by SMS.email_otpCodes by email.recovery_codesThe printable fallback set.namestringOptionalShown in the user's factor list. Worth collecting — a person with two authenticators cannot tell them apart otherwise.
phonePhoneOptionalsms_otp only. The number that receives the codes — it can differ from the
user's own phone. Defaults to the user's phone. The same number cannot be an
SMS factor twice (mfa_phone_already_enrolled).
noncestringOptionalSudo confirmation.
A user holds up to 10 authenticator apps and SMS factors combined
(mfa_factors_limit_reached beyond that; the environment can lower it), and a single email factor and set of recovery codes.
Returns FactorEnrollment:
tokenstringRequiredShort-lived. Carry it to enable.
secretstringOptionalTOTP only. The shared secret, for users who type it in by hand.
urlstringOptionalTOTP only. A provisioning URL — render it as a QR code directly rather than assembling one from the secret.
enable
enable({ token, code }) → null
Activates a pending enrolment by proving the device works.
await nulvless.auth.factors.enable({
token: data.token,
code: firstCodeFromTheApp,
});tokenstringRequiredFrom enroll.
codestringRequiredThe first code the user's device produces.
Until this succeeds the factor does not exist. An abandoned enrolment leaves the account exactly as it was — there is no half-enabled state to clean up, and no risk of locking someone out with a device that was never verified.
The only call in this namespace that does not need sudo. It is already proving possession of the device, which is what sudo would have asked for.
unenroll
unenroll({ id, nonce }) → AuthMethod
Removes a factor. Returns the one removed.
const { data: factors } = await nulvless.auth.factors.list();
const totp = factors?.find((factor) => factor.type === 'totp');
if (totp?.id) await nulvless.auth.factors.unenroll({ id: totp.id, nonce: code });idstringRequiredFrom list().
noncestringOptionalOn an environment that requires MFA, removing the last factor leaves the account unable to complete a sign-in. Warn before the last one, or pair the removal with enrolling a replacement.
getRecoveryCodes
getRecoveryCodes({ nonce }) → { codes }
Returns the user's recovery codes.
const { data } = await nulvless.auth.factors.getRecoveryCodes({ nonce: code });
// data.codes → string[]noncestringOptionalSudo confirmation.
Show them once, at enrolment, and make the user acknowledge that they were
saved. They are the only self-service way back in for someone who loses their
device — the alternative is a support path where an admin removes the factor
with auth.admin.users.removeFactor,
and the platform will not second-guess that call.
A recovery code is submitted in the ordinary code field of
verifyChallenge — there is no separate call
for redeeming one.
deleteRecoveryCodes
deleteRecoveryCodes({ nonce }) → null
Invalidates the current set.
await nulvless.auth.factors.deleteRecoveryCodes({ nonce: code });noncestringOptionalWhat a "regenerate my codes" button does before issuing a fresh set. On its own it removes a fallback rather than adding one, so it is rarely the whole interaction.