Autenticação multifator
Cadastrar um fator, o desafio que ele produz na entrada, e recuperação.
MFA tem duas metades que são fáceis de confundir. Cadastro é algo que um usuário autenticado faz na própria conta. O desafio é o que a entrada passa a produzir depois, para quem tem um fator.
Você constrói as duas como telas separadas, e só uma delas está no caminho da entrada.
O lado do desafio
Assim que um usuário tem um fator habilitado, o signIn para de devolver sessão
direto:
const { data, error } = await nulvless.auth.signIn({ email, password });
if (error) return showError();
if (data.challenge) return goToVerify(data.challenge);
// sem desafio — autenticadoA plataforma escolhe com qual fator desafiar, numa ordem de preferência fixa:
totp1ºUm app autenticador. Escolhido sempre que o usuário tem um.
sms_otp2ºUm código por SMS.
email_otp3ºUm código por e-mail.
recovery_codesúltimoSó quando nada mais está disponível.
challenge.preferred é o escolhido; challenge.factors lista as alternativas
que o usuário de fato possui. É desse segundo campo que se constrói um link de
"tentar de outra forma".
Trocando de método no meio do fluxo
Um usuário sem acesso ao autenticador não recomeça a entrada — o desafio já em andamento é movido para outro método:
await nulvless.auth.challenges.change({
token: challenge.token,
type: 'email_otp',
});O campo é type, não method.
Finalizando
A mesma chamada de todos os outros desafios, e é por isso que um componente só atende a todos:
const { data } = await nulvless.auth.verifyChallenge({
token: challenge.token,
code,
});Um código de recuperação vai no mesmo campo code — não existe chamada separada
para ele.
O lado do cadastro
O cadastro tem dois passos de propósito: o segredo é entregue primeiro, e o fator só passa a existir quando o usuário prova que o dispositivo dele gera o código certo.
Inicie
const { data } = await nulvless.auth.factors.enroll({
type: 'totp',
name: 'Meu autenticador',
});
// data.secret → entrada manual
// data.url → renderize como QR code
// data.token → leve para o enable()typeMFAMethodRequiredtotpsms_otpemail_otprecovery_codesnamestringOptionalAparece na lista de fatores do usuário.
noncestringOptionalNonce de sudo — veja abaixo.
secret e url só voltam para TOTP. O url é uma URL de provisionamento, então
você a transforma direto em QR code sem montar nada à mão.
Confirme
await nulvless.auth.factors.enable({
token: data.token,
code: primeiroCodigoDoApp,
});Até isso funcionar, o fator não existe. Um cadastro abandonado deixa a conta exatamente como estava — não há estado meio-habilitado para limpar.
Listando o que um usuário tem
const { data: factors } = await nulvless.auth.factors.list();Cada entrada traz id, type, name, status (configured ou
not_configured) e created_at. O id é o que o unenroll recebe:
const totp = factors.find((factor) => factor.type === 'totp');
if (totp?.id) await nulvless.auth.factors.unenroll({ id: totp.id });Um usuário pode ter vários apps autenticadores e fatores SMS — até 10 somados,
limite que cada ambiente pode reduzir nas configurações de entrada do console
(Fatores por usuário, na seção de MFA). Cada fator SMS tem seu próprio telefone, que pode
ser diferente do telefone do usuário. E-mail e códigos de recuperação continuam
um de cada. No login, um desafio totp aceita o código de qualquer app do
usuário, e challenges.change({ token, type: 'sms_otp', factor_id }) escolhe
qual telefone recebe o SMS.
Códigos de recuperação
const { data } = await nulvless.auth.factors.getRecoveryCodes();
// data.codes → string[]
await nulvless.auth.factors.deleteRecoveryCodes();Mostre uma vez, no cadastro, e faça o usuário confirmar que guardou. São o único
caminho de volta para quem perde o dispositivo — a alternativa é um fluxo de
suporte em que um admin remove o fator com
auth.admin.users.removeFactor, e a
plataforma não questiona essa chamada.
Tudo que é destrutivo exige sudo
enroll, unenroll, getRecoveryCodes e deleteRecoveryCodes são todos
protegidos por sudo. Só o enable não é — ele já está provando posse do
dispositivo.
O auth.sessions.sudo() envia o código nos dois casos. Gaste ele na sessão com
sudoVerify, ou só nesta chamada:
await nulvless.auth.factors.unenroll({ id, nonce: code });O nonce é esse código. Vai nos params, não nas options.
É esse o motivo do sudo na gestão de MFA: uma sessão roubada não pode remover silenciosamente o segundo fator que protege a conta. Veja Modo sudo.
Quando o ambiente exige MFA
Um ambiente pode ser configurado para MFA não ser opcional. Duas configurações importam:
As duas ficam no console, em Autenticações.
Exigir autenticação de dois fatores (MFA)Sessões que nunca usaram um segundo fator podem ser rejeitadas quando solicitado.
Métodos de autenticaçãoQuais tipos de fator este ambiente permite.
Exigir MFA não cadastra ninguém por você. O seu produto ainda precisa levar um
usuário sem fator até o cadastro — normalmente verificando factors.list() após
a entrada e roteando para uma tela de configuração quando vier vazio.