Implementar TOTP no Node.js adiciona um segundo fator baseado em códigos temporários gerados por aplicativos autenticadores. O servidor e o dispositivo compartilham um segredo, e ambos calculam um código curto a partir do horário atual.
TOTP melhora a segurança contra senhas vazadas, mas não é resistente a phishing em tempo real. A implementação precisa proteger o segredo, limitar tentativas, lidar com diferença de relógio, oferecer recovery codes e auditar ativação e remoção.
Neste guia, você aprenderá a gerar segredos, criar URI otpauth, exibir QR Code, verificar códigos, cifrar o segredo, evitar replay, implementar recuperação, exigir step-up e testar o fluxo.
O que é TOTP?
TOTP significa Time-Based One-Time Password. O RFC 6238 define o algoritmo. O RFC 4226 define HOTP, base do TOTP.
Para tokens de autenticação, consulte JWT Seguro no Node.js. Para auditoria, veja Logs de Auditoria no Node.js.
Como o código é gerado?
O autenticador calcula um contador com base no horário:
counter = floor(unixTime / period)O valor padrão comum é 30 segundos. O contador é usado com HMAC e um segredo compartilhado.
Parâmetros comuns
- algoritmo HMAC, geralmente SHA-1 por compatibilidade;
- 6 dígitos;
- período de 30 segundos;
- pequena janela para diferença de relógio.
Não mude parâmetros sem confirmar suporte dos autenticadores.
Biblioteca
Use uma biblioteca TOTP mantida e auditada. Evite implementar truncamento dinâmico e Base32 manualmente em produção.
Gerando um segredo
const secret = authenticator.generateSecret();O segredo precisa ter entropia suficiente e ser gerado por fonte criptograficamente segura.
URI otpauth
const uri = authenticator.keyuri(
user.email,
'Minha Aplicação',
secret
);O formato costuma ser:
otpauth://totp/Minha%20Aplicação:user@example.com?secret=...&issuer=Minha%20AplicaçãoQR Code
O frontend pode mostrar o URI como QR Code. O conteúdo contém o segredo e deve ser tratado como credencial.
Não registre a URI
Logs, analytics, ferramentas de sessão e prints de suporte não devem capturar o QR Code ou o valor secret.
Fluxo de ativação
- Usuário autenticado solicita ativação.
- Servidor exige confirmação recente da senha ou passkey.
- Servidor gera segredo pendente.
- Usuário escaneia o QR Code.
- Usuário informa um código válido.
- Servidor ativa TOTP.
- Servidor gera recovery codes.
- Evento é auditado.
Segredo pendente
Não marque MFA como ativo antes de verificar um código. Armazene estado:
totp_status: 'pending' | 'active' | 'disabled'Expiração do setup
Um segredo pendente deve expirar após alguns minutos ou horas. Se o usuário reiniciar o fluxo, revogue o segredo anterior.
Verificando o código
const valid = authenticator.verify({
token: submittedCode,
secret
});Normalize apenas espaços permitidos e rejeite valores que não possuem o número esperado de dígitos.
Comparação segura
A biblioteca deve realizar a verificação adequadamente. Não converta códigos para número, pois zeros iniciais são significativos.
Janela de tempo
Uma janela de um passo anterior e posterior tolera relógios levemente diferentes. Janelas grandes aumentam a quantidade de códigos aceitos.
Sincronização do servidor
Use NTP ou serviço equivalente. Se o relógio do servidor diverge, todos os usuários podem falhar.
Evitar replay
Um código permanece válido durante a janela. Para operações críticas, registre o último contador aceito e rejeite reutilização:
if (counter <= user.lastTotpCounter) {
throw new InvalidCodeError();
}Atualize o contador atomicamente.
Concorrência
Duas requisições podem enviar o mesmo código ao mesmo tempo. Use transação ou update condicional para garantir uma única aceitação.
Rate limiting
Um código de seis dígitos possui espaço limitado. Restrinja tentativas por usuário, sessão e IP confiável.
Consulte Rate Limiting no Node.js.
Bloqueio progressivo
Depois de falhas, introduza atraso e solicite novo login. Evite bloquear permanentemente a conta por ação de um atacante.
Mensagem de erro
{
"code": "INVALID_MFA_CODE",
"message": "Código inválido ou expirado"
}Não informe se o código pertence à janela anterior ou se o relógio está quase correto.
Armazenando o segredo
O servidor precisa recuperar o segredo para verificar códigos, portanto hash não é suficiente. Use criptografia com chave gerenciada.
Criptografia de envelope
Gere uma chave de dados, cifre o segredo e proteja a chave de dados com KMS ou HSM. Armazene ciphertext, nonce, algoritmo e versão da chave.
Chave fora do banco
Se banco e chave ficam juntos, o benefício da criptografia diminui. Use Secret, KMS ou serviço de chaves com acesso restrito.
Rotação
Versione a chave de criptografia e recifre segredos gradualmente. A rotação do segredo TOTP exige novo enrollment do usuário.
Backups
Backups contêm segredos cifrados. Proteja chaves, acesso e retenção.
Recovery codes
Gere códigos aleatórios de uso único:
const codes = Array.from({ length: 10 }, () =>
randomBytes(10).toString('base64url')
);Mostre apenas uma vez.
Hash dos recovery codes
Diferentemente do segredo TOTP, recovery codes podem ser armazenados com hash forte, pois são comparados como senhas.
Uso único
Ao usar um recovery code, marque como consumido na mesma transação que cria a sessão.
Regeneração
Gerar novos recovery codes invalida todos os anteriores e exige autenticação forte.
Removendo TOTP
Exija senha recente, passkey ou outro fator. Não permita remover MFA apenas com a sessão antiga.
Conta comprometida
Se um atacante possui a sessão, ele pode tentar substituir o TOTP. Envie notificação e aplique período de segurança para mudanças críticas.
Step-up authentication
Mesmo em uma sessão autenticada, ações como exportar dados ou alterar cobrança podem exigir um código recente.
if (Date.now() - session.mfaVerifiedAt > 10 * 60 * 1000) {
throw new MfaRequiredError();
}Nível de autenticação
Registre na sessão se o usuário entrou apenas com senha ou com MFA. Políticas ABAC podem exigir nível forte.
Veja ABAC no Node.js.
Trusted devices
“Lembrar dispositivo” reduz fricção, mas cria outro token duradouro. Proteja com cookie HttpOnly, Secure, rotação e revogação.
Não use fingerprint invasivo
Características do navegador mudam e podem afetar privacidade. Um token aleatório de dispositivo é mais previsível.
Phishing
TOTP pode ser digitado em um site falso e usado imediatamente pelo atacante. Para resistência a phishing, prefira WebAuthn e passkeys.
Fallback
Oferecer SMS como fallback pode reduzir a segurança. Defina opções de recuperação compatíveis com o risco.
Suporte
Atendentes não devem visualizar o segredo nem desativar MFA sem verificação rigorosa e auditoria.
Multi-tenancy
Uma identidade pode usar o mesmo fator em vários tenants se a conta é global. Decisões de step-up continuam ligadas à sessão e organização.
Sessões
Após verificar TOTP, rotacione o ID de sessão para impedir fixation. O artigo sobre Sessões Seguras aprofunda o tema.
JWT
Um claim como amr pode indicar fatores usados:
{
"amr": ["pwd", "otp"],
"auth_time": 1787918400
}Valide assinatura, issuer e audience.
Auditoria
Registre:
- setup iniciado;
- ativação concluída;
- verificação falha em volume anormal;
- recovery code usado;
- códigos regenerados;
- TOTP removido;
- step-up concluído;
- mudança feita por suporte.
Nunca registre o código ou segredo.
Métricas
Monitore taxa de sucesso, falhas, bloqueios, uso de recovery code e remoções. Não use user ID como label.
Alertas
Muitas falhas distribuídas, remoção de MFA e uso de recovery code após mudança de senha podem indicar comprometimento.
Testes unitários
Use um relógio injetável para testar passos de tempo sem depender do horário real.
Teste de janela
test('aceita o passo anterior permitido', () => {
clock.set('2026-08-28T14:00:30Z');
const code = generateAt('2026-08-28T14:00:00Z');
assert.equal(verify(code, secret), true);
});Teste de replay
Envie o mesmo código duas vezes em paralelo e confirme apenas uma aceitação.
Teste de rate limit
Exceda o número de tentativas e confirme bloqueio temporário sem revelar estado da conta.
Teste de criptografia
Confirme que o banco não contém o segredo em texto claro e que uma chave antiga ainda pode ser lida durante rotação.
Teste de recovery code
Use um código, tente reutilizá-lo e confirme rejeição.
Teste de remoção
Uma sessão sem step-up recente não deve remover TOTP.
Erros comuns
- Segredo em texto claro: vazamento do banco compromete MFA.
- QR Code em logs: o segredo é exposto.
- Sem rate limit: códigos podem ser adivinhados.
- Janela grande: mais códigos permanecem válidos.
- Sem proteção contra replay: o mesmo código é reutilizado.
- Recovery codes sem hash: backup revela acesso.
- Remoção sem step-up: sessão roubada desativa MFA.
Boas práticas
- Use biblioteca confiável.
- Gere segredo criptográfico.
- Cifre com chave externa.
- Verifique antes de ativar.
- Use janela pequena.
- Limite tentativas.
- Bloqueie replay.
- Forneça recovery codes com hash.
- Exija step-up para mudanças.
- Audite sem registrar segredos.
Conclusão
Implementar TOTP no Node.js adiciona um segundo fator compatível com aplicativos autenticadores e reduz o impacto de senhas vazadas.
A segurança depende de proteger o segredo, limitar tentativas, controlar replay e oferecer recuperação segura. TOTP ainda pode ser vítima de phishing, portanto aplicações de maior risco devem oferecer WebAuthn ou passkeys. Com criptografia, auditoria e step-up, o fluxo se torna uma camada útil de autenticação sem criar novos segredos expostos.



