{"id":54,"date":"2026-06-11T16:58:54","date_gmt":"2026-06-11T16:58:54","guid":{"rendered":"https:\/\/shattered.io\/pt\/2026\/06\/11\/autenticacao-dois-fatores-nodejs\/"},"modified":"2026-06-11T16:58:54","modified_gmt":"2026-06-11T16:58:54","slug":"autenticacao-dois-fatores-nodejs","status":"publish","type":"post","link":"https:\/\/shattered.io\/pt\/2026\/06\/11\/autenticacao-dois-fatores-nodejs\/","title":{"rendered":"Autentica\u00e7\u00e3o de Dois Fatores em Node.js: 12 Passos [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">A <strong>autentica\u00e7\u00e3o de dois fatores<\/strong> deixou de ser um extra opcional. Em 2026, qualquer aplica\u00e7\u00e3o web que guarde dados sens\u00edveis precisa de uma segunda barreira para l\u00e1 da palavra-passe. A Microsoft repete h\u00e1 anos que mais de 99,9% das contas comprometidas n\u00e3o tinham MFA ativada, um n\u00famero que resume bem porque \u00e9 que este passo importa. Neste tutorial implementamos, do zero, <strong>autentica\u00e7\u00e3o de dois fatores<\/strong> baseada em TOTP (Time-based One-Time Password) numa API Node.js, com c\u00f3digo de seis d\u00edgitos compat\u00edvel com o Google Authenticator, Microsoft Authenticator e Authy.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">No final ter\u00e1 um projeto funcional completo: gera\u00e7\u00e3o do segredo partilhado, c\u00f3digo QR para registo, verifica\u00e7\u00e3o do c\u00f3digo de seis d\u00edgitos, c\u00f3digos de recupera\u00e7\u00e3o, limita\u00e7\u00e3o de tentativas e integra\u00e7\u00e3o no fluxo de login com JWT. S\u00e3o 12 passos, cerca de 30 minutos de trabalho, e tudo assente nas normas RFC 6238 e RFC 4226. Nenhuma depend\u00eancia paga, nenhum servi\u00e7o externo obrigat\u00f3rio.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"autenticacao-de-dois-fatores-o-que-muda-em-2026\">Autentica\u00e7\u00e3o de dois fatores: o que muda em 2026<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A <strong>autentica\u00e7\u00e3o de dois fatores<\/strong> combina algo que o utilizador sabe (a palavra-passe) com algo que possui (o telem\u00f3vel com a app de c\u00f3digos). Mesmo que um atacante roube a palavra-passe num ataque de phishing ou numa fuga de dados, n\u00e3o consegue entrar sem o c\u00f3digo que muda a cada 30 segundos no dispositivo da v\u00edtima.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Em 2026, a paisagem mudou em tr\u00eas pontos concretos. Primeiro, o SMS perdeu credibilidade: o envio de c\u00f3digos por mensagem continua vulner\u00e1vel ao SIM swap, em que o atacante transfere o n\u00famero da v\u00edtima para um cart\u00e3o SIM controlado por si. O TOTP, por gerar os c\u00f3digos localmente no dispositivo, elimina esse risco. Segundo, as passkeys (FIDO2\/WebAuthn) consolidaram-se como o fator mais resistente a phishing, porque a credencial fica ligada \u00e0 origem do site e n\u00e3o pode ser reutilizada num dom\u00ednio falso. Terceiro, mesmo com as passkeys em ascens\u00e3o, o TOTP mant\u00e9m-se como o m\u00e9todo de compatibilidade universal, suportado por todas as apps de autentica\u00e7\u00e3o e por milh\u00f5es de contas existentes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A escolha pr\u00e1tica para a maioria das equipas em 2026 \u00e9 clara: oferecer TOTP como segundo fator universal hoje, e planear passkeys como caminho a seguir. Este tutorial foca o TOTP porque \u00e9 o que pode implementar numa tarde, sem depender do suporte de hardware do cliente. Conv\u00e9m saber, desde j\u00e1, a sua principal limita\u00e7\u00e3o: o TOTP n\u00e3o \u00e9 resistente a phishing em tempo real. Um atacante que monte um proxy reverso entre o utilizador e o site verdadeiro pode capturar e reencaminhar o c\u00f3digo dentro da janela de 30 segundos. Mitigamos isso mais \u00e0 frente com limita\u00e7\u00e3o de tentativas e boas pr\u00e1ticas de sess\u00e3o.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"como-funciona-o-totp-rfc-6238-e-rfc-4226\">Como funciona o TOTP (RFC 6238 e RFC 4226)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">O TOTP \u00e9 definido na <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc6238\" target=\"_blank\" rel=\"noopener\">RFC 6238<\/a> e assenta sobre o HOTP, descrito na <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc4226\" target=\"_blank\" rel=\"noopener\">RFC 4226<\/a>. A ideia \u00e9 simples e elegante. O servidor e a app de autentica\u00e7\u00e3o partilham um segredo \u00fanico, gerado no momento do registo. A partir da\u00ed, ambos calculam o mesmo c\u00f3digo de seis d\u00edgitos usando dois ingredientes: esse segredo partilhado e a hora atual.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">O c\u00e1lculo divide o tempo em janelas (por norma de 30 segundos), conta quantas janelas passaram desde 1 de janeiro de 1970, e aplica um HMAC-SHA1 sobre esse contador usando o segredo como chave. Do resultado extrai-se um n\u00famero de seis d\u00edgitos. Como o servidor e a app t\u00eam o mesmo segredo e o mesmo rel\u00f3gio, chegam ao mesmo c\u00f3digo de forma independente, sem nunca trocarem mensagens depois do registo inicial. Por isso \u00e9 que o TOTP funciona mesmo com o telem\u00f3vel em modo de avi\u00e3o.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Os par\u00e2metros s\u00e3o quase sempre os mesmos em todas as aplica\u00e7\u00f5es populares. A tabela abaixo mostra os valores padr\u00e3o que vamos respeitar para garantir compatibilidade total.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Par\u00e2metro<\/th><th>Valor padr\u00e3o<\/th><th>Google Authenticator<\/th><th>Microsoft Authenticator<\/th><th>Authy<\/th><\/tr><\/thead><tbody><tr><td>Per\u00edodo (janela)<\/td><td>30 segundos<\/td><td>30 s<\/td><td>30 s<\/td><td>30 s<\/td><\/tr><tr><td>D\u00edgitos<\/td><td>6<\/td><td>6<\/td><td>6<\/td><td>6<\/td><\/tr><tr><td>Algoritmo<\/td><td>HMAC-SHA1<\/td><td>SHA1<\/td><td>SHA1<\/td><td>SHA1<\/td><\/tr><tr><td>Codifica\u00e7\u00e3o do segredo<\/td><td>Base32<\/td><td>Base32<\/td><td>Base32<\/td><td>Base32<\/td><\/tr><tr><td>Norma<\/td><td>RFC 6238<\/td><td>RFC 6238<\/td><td>RFC 6238<\/td><td>RFC 6238<\/td><\/tr><\/tbody><\/table><figcaption class=\"wp-block-table__caption\">Par\u00e2metros TOTP padr\u00e3o suportados pelas principais apps de autentica\u00e7\u00e3o.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Repare numa decis\u00e3o deliberada: o HMAC-SHA1. Apesar de o SHA-1 estar obsoleto para assinaturas digitais (recorde a colis\u00e3o SHATTERED), no contexto do HMAC-TOTP mant\u00e9m-se seguro e, mais importante, \u00e9 o \u00fanico algoritmo que todas as apps leem de forma fi\u00e1vel. Alterar para SHA-256 partiria a compatibilidade com muitos leitores de QR. Mantemos o SHA1.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"pre-requisitos-e-versoes\">Pr\u00e9-requisitos e vers\u00f5es<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Antes de escrever c\u00f3digo, confirme o ambiente. Este tutorial foi testado com as vers\u00f5es da tabela seguinte. Pode usar vers\u00f5es mais recentes, mas evite vers\u00f5es anteriores do Node.js, que n\u00e3o suportam algumas funcionalidades do Express 5.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Ferramenta<\/th><th>Vers\u00e3o usada<\/th><th>Fun\u00e7\u00e3o no projeto<\/th><\/tr><\/thead><tbody><tr><td>Node.js<\/td><td>22 LTS ou 24 LTS<\/td><td>Ambiente de execu\u00e7\u00e3o<\/td><\/tr><tr><td>npm<\/td><td>10 ou superior<\/td><td>Gestor de pacotes<\/td><\/tr><tr><td>otplib<\/td><td>13.4.1<\/td><td>Gera\u00e7\u00e3o e verifica\u00e7\u00e3o de TOTP<\/td><\/tr><tr><td>qrcode<\/td><td>1.5.4<\/td><td>Gerar o c\u00f3digo QR de registo<\/td><\/tr><tr><td>express<\/td><td>5.2.1<\/td><td>Servidor HTTP da API<\/td><\/tr><tr><td>jsonwebtoken<\/td><td>9.x<\/td><td>Emitir o token de sess\u00e3o<\/td><\/tr><\/tbody><\/table><figcaption class=\"wp-block-table__caption\">Pr\u00e9-requisitos e vers\u00f5es de refer\u00eancia para o projeto.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Confirme as vers\u00f5es instaladas antes de avan\u00e7ar. Uma incompatibilidade de Node.js \u00e9 a causa n\u00famero um de erros logo no primeiro passo.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>node --version\n# v22.x.x ou v24.x.x\n\nnpm --version\n# 10.x.x ou superior<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Precisa tamb\u00e9m de uma app de autentica\u00e7\u00e3o no telem\u00f3vel para testar. Qualquer uma serve: Google Authenticator, Microsoft Authenticator, Authy, ou alternativas de c\u00f3digo aberto como o Aegis (Android) ou o Raivo (iOS). Conhecimentos b\u00e1sicos de JavaScript ass\u00edncrono (async\/await) e de pedidos HTTP s\u00e3o suficientes para acompanhar.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"passo-1-inicializar-o-projeto-node-js\">Passo 1: Inicializar o projeto Node.js<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Crie uma pasta para o projeto e inicialize-o. Vamos usar m\u00f3dulos ES (import\/export), por isso definimos o tipo do pacote logo de in\u00edcio.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir 2fa-nodejs &amp;&amp; cd 2fa-nodejs\nnpm init -y\nnpm pkg set type=\"module\"\nnpm pkg set engines.node=\"&gt;=22\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">O comando <code>npm pkg set type=\"module\"<\/code> evita o aviso recorrente sobre sintaxe de importa\u00e7\u00e3o e permite usar <code>import<\/code> sem extens\u00f5es de ficheiro estranhas. A defini\u00e7\u00e3o de <code>engines.node<\/code> documenta a vers\u00e3o m\u00ednima e avisa quem instalar o projeto num ambiente antigo.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"passo-2-instalar-as-dependencias-otplib-qrcode-express\">Passo 2: Instalar as depend\u00eancias (otplib, qrcode, express)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Instale as quatro bibliotecas centrais. O <strong>otplib<\/strong> trata da gera\u00e7\u00e3o e verifica\u00e7\u00e3o dos c\u00f3digos TOTP, o <strong>qrcode<\/strong> desenha o c\u00f3digo QR, o <strong>express<\/strong> serve a API e o <strong>jsonwebtoken<\/strong> emite o token de sess\u00e3o ap\u00f3s a verifica\u00e7\u00e3o bem-sucedida.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm install otplib@13.4.1 qrcode@1.5.4 express@5.2.1 jsonwebtoken@9\nnpm install --save-dev nodemon<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Escolhemos o otplib em vez do speakeasy (vers\u00e3o 2.0.0, sem atualiza\u00e7\u00f5es recentes) porque o otplib \u00e9 mantido ativamente, tem API moderna baseada em classes e separa de forma limpa o TOTP do HOTP. Adicione um script de arranque ao <code>package.json<\/code> para reiniciar o servidor durante o desenvolvimento.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm pkg set scripts.dev=\"nodemon server.js\"\nnpm pkg set scripts.start=\"node server.js\"<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"passo-3-gerar-o-segredo-partilhado-do-utilizador\">Passo 3: Gerar o segredo partilhado do utilizador<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">O segredo partilhado \u00e9 o cora\u00e7\u00e3o da <strong>autentica\u00e7\u00e3o de dois fatores<\/strong>. \u00c9 gerado uma \u00fanica vez, no momento em que o utilizador ativa o 2FA, e nunca mais \u00e9 mostrado em texto simples. Crie um ficheiro <code>totp.js<\/code> com as fun\u00e7\u00f5es utilit\u00e1rias.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ totp.js\nimport { authenticator } from 'otplib';\n\n\/\/ Parametros explicitos para garantir compatibilidade total\nauthenticator.options = {\n  digits: 6,\n  step: 30,        \/\/ janela de 30 segundos\n  window: 1,       \/\/ tolera 1 janela de deriva de relogio\n  algorithm: 'sha1',\n};\n\n\/\/ Gera um segredo Base32 novo para um utilizador\nexport function gerarSegredo() {\n  return authenticator.generateSecret(); \/\/ 32 caracteres Base32\n}\n\n\/\/ Cria a URI otpauth:\/\/ que a app de autenticacao le\nexport function gerarOtpauthUri(email, segredo) {\n  const emissor = 'ShatteredApp';\n  return authenticator.keyuri(email, emissor, segredo);\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A fun\u00e7\u00e3o <code>keyuri<\/code> produz uma string no formato <code>otpauth:\/\/totp\/ShatteredApp:ana@exemplo.pt?secret=...&amp;issuer=ShatteredApp<\/code>. \u00c9 esta URI que transformamos em c\u00f3digo QR no passo seguinte. O campo <code>emissor<\/code> aparece como nome da conta dentro da app do utilizador, por isso use o nome real do seu servi\u00e7o.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"passo-4-criar-o-codigo-qr-para-o-authenticator\">Passo 4: Criar o c\u00f3digo QR para o authenticator<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">O utilizador n\u00e3o vai escrever 32 caracteres \u00e0 m\u00e3o. Convertemos a URI otpauth num c\u00f3digo QR em formato Data URL, que o frontend mostra como imagem. Acrescente ao <code>totp.js<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ totp.js (continuacao)\nimport QRCode from 'qrcode';\n\n\/\/ Devolve um Data URL PNG pronto a usar num elemento de imagem\nexport async function gerarQrCode(otpauthUri) {\n  try {\n    return await QRCode.toDataURL(otpauthUri, {\n      errorCorrectionLevel: 'M',\n      margin: 2,\n      width: 240,\n    });\n  } catch (erro) {\n    throw new Error('Falha ao gerar o codigo QR: ' + erro.message);\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">O n\u00edvel de corre\u00e7\u00e3o de erros <code>M<\/code> (15%) \u00e9 o equil\u00edbrio certo entre densidade e fiabilidade de leitura. N\u00e3o suba para <code>H<\/code> sem motivo: aumenta a densidade do QR e dificulta a leitura em ecr\u00e3s pequenos. Forne\u00e7a sempre, como alternativa, o segredo em texto para introdu\u00e7\u00e3o manual, para o caso de a c\u00e2mara do utilizador falhar.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"passo-5-guardar-o-segredo-cifrado-na-base-de-dados\">Passo 5: Guardar o segredo cifrado na base de dados<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Erro grave e frequente: guardar o segredo TOTP em texto simples. Se a base de dados vazar, todos os segundos fatores ficam comprometidos de uma vez. O segredo deve ser cifrado em repouso com uma chave que viva fora da base de dados (numa vari\u00e1vel de ambiente ou num cofre de segredos). Aqui usamos AES-256-GCM, o padr\u00e3o recomendado para cifra autenticada.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ cripto.js\nimport crypto from 'node:crypto';\n\n\/\/ Chave de 32 bytes em hex, definida em variavel de ambiente\nconst CHAVE = Buffer.from(process.env.TOTP_ENC_KEY, 'hex');\n\nexport function cifrar(textoSimples) {\n  const iv = crypto.randomBytes(12); \/\/ GCM usa IV de 12 bytes\n  const cipher = crypto.createCipheriv('aes-256-gcm', CHAVE, iv);\n  const cifrado = Buffer.concat([\n    cipher.update(textoSimples, 'utf8'),\n    cipher.final(),\n  ]);\n  const tag = cipher.getAuthTag();\n  \/\/ Formato armazenado: iv:tag:cifrado (tudo em hex)\n  return [iv.toString('hex'), tag.toString('hex'), cifrado.toString('hex')].join(':');\n}\n\nexport function decifrar(armazenado) {\n  const [ivHex, tagHex, dadosHex] = armazenado.split(':');\n  const decipher = crypto.createDecipheriv('aes-256-gcm', CHAVE, Buffer.from(ivHex, 'hex'));\n  decipher.setAuthTag(Buffer.from(tagHex, 'hex'));\n  return Buffer.concat([\n    decipher.update(Buffer.from(dadosHex, 'hex')),\n    decipher.final(),\n  ]).toString('utf8');\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Gere a chave de cifra uma vez com <code>openssl rand -hex 32<\/code> e guarde-a numa vari\u00e1vel de ambiente <code>TOTP_ENC_KEY<\/code>. Nunca a coloque no reposit\u00f3rio de c\u00f3digo. Se quiser aprofundar a cifra sim\u00e9trica, veja o nosso guia dedicado ao AES-256, ligado na sec\u00e7\u00e3o de cobertura relacionada.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"passo-6-verificar-o-codigo-de-seis-digitos\">Passo 6: Verificar o c\u00f3digo de seis d\u00edgitos<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Chegamos \u00e0 valida\u00e7\u00e3o. Quando o utilizador introduz o c\u00f3digo de seis d\u00edgitos, o servidor recalcula o c\u00f3digo esperado a partir do segredo decifrado e compara. O otplib faz a compara\u00e7\u00e3o em tempo constante, evitando fugas por timing. Acrescente ao <code>totp.js<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ totp.js (continuacao)\n\/\/ Devolve true se o codigo corresponder a janela atual (ou adjacente)\nexport function verificarCodigo(codigo, segredo) {\n  \/\/ Normaliza: remove espacos que apps por vezes inserem\n  const limpo = String(codigo).replace(\/\\s+\/g, '');\n  if (!\/^\\d{6}$\/.test(limpo)) return false;\n  try {\n    return authenticator.check(limpo, segredo);\n  } catch {\n    return false;\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Repare em duas defesas. Primeiro, validamos o formato (exatamente seis d\u00edgitos) antes de chamar o otplib, o que descarta de imediato entradas inv\u00e1lidas. Segundo, envolvemos a verifica\u00e7\u00e3o num try\/catch porque um segredo malformado faz o otplib lan\u00e7ar uma exce\u00e7\u00e3o, e queremos devolver <code>false<\/code> em vez de derrubar o servidor. O m\u00e9todo <code>check<\/code> respeita a op\u00e7\u00e3o <code>window: 1<\/code>, aceitando o c\u00f3digo da janela anterior e da seguinte para tolerar pequena deriva de rel\u00f3gio.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"passo-7-gerar-e-validar-codigos-de-recuperacao\">Passo 7: Gerar e validar c\u00f3digos de recupera\u00e7\u00e3o<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">O que acontece se o utilizador perder o telem\u00f3vel? Sem plano B, fica trancado para fora da conta. Os c\u00f3digos de recupera\u00e7\u00e3o resolvem isto: um conjunto de c\u00f3digos de uso \u00fanico, gerados no registo, que o utilizador guarda em local seguro. Guardamo-los com hash, nunca em texto simples, exatamente como far\u00edamos com palavras-passe.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ recuperacao.js\nimport crypto from 'node:crypto';\n\n\/\/ Gera 10 codigos de recuperacao legiveis\nexport function gerarCodigosRecuperacao(quantidade = 10) {\n  const codigos = [];\n  for (let i = 0; i &lt; quantidade; i++) {\n    \/\/ 5 bytes -&gt; 10 caracteres hex, agrupados em xxxxx-xxxxx\n    const bruto = crypto.randomBytes(5).toString('hex');\n    codigos.push(bruto.slice(0, 5) + '-' + bruto.slice(5));\n  }\n  return codigos;\n}\n\n\/\/ Guarda apenas o hash SHA-256 de cada codigo\nexport function fazerHashCodigos(codigos) {\n  return codigos.map((c) =&gt;\n    crypto.createHash('sha256').update(c).digest('hex')\n  );\n}\n\n\/\/ Verifica um codigo contra a lista de hashes guardada\nexport function validarCodigoRecuperacao(codigo, hashesGuardados) {\n  const hash = crypto.createHash('sha256').update(codigo.trim()).digest('hex');\n  return hashesGuardados.indexOf(hash); \/\/ -1 se invalido; senao, o indice\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Quando um c\u00f3digo de recupera\u00e7\u00e3o \u00e9 usado com sucesso, remova-o (ou marque-o como consumido) na base de dados a partir do \u00edndice devolvido. Cada c\u00f3digo serve uma \u00fanica vez. Mostre os dez c\u00f3digos ao utilizador apenas uma vez, no ecr\u00e3 de ativa\u00e7\u00e3o, e avise para os guardar antes de continuar.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"passo-8-integrar-o-2fa-no-fluxo-de-login-com-jwt\">Passo 8: Integrar o 2FA no fluxo de login com JWT<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A <strong>autentica\u00e7\u00e3o de dois fatores<\/strong> encaixa-se a meio do login, n\u00e3o no in\u00edcio. O fluxo correto tem dois andares: primeiro a palavra-passe, depois o segundo fator. S\u00f3 ap\u00f3s ambos \u00e9 que emitimos o token de sess\u00e3o completo. Para a fase interm\u00e9dia usamos um token tempor\u00e1rio, de curta dura\u00e7\u00e3o, que prova que a palavra-passe j\u00e1 passou mas que ainda falta o segundo fator.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ auth.js\nimport jwt from 'jsonwebtoken';\n\nconst SEGREDO_JWT = process.env.JWT_SECRET;\n\n\/\/ Token intermedio: palavra-passe correta, falta o 2FA\nexport function emitirTokenParcial(userId) {\n  return jwt.sign({ sub: userId, mfa: 'pendente' }, SEGREDO_JWT, {\n    expiresIn: '5m',\n  });\n}\n\n\/\/ Token de sessao completo: ambos os fatores validados\nexport function emitirTokenCompleto(userId) {\n  return jwt.sign({ sub: userId, mfa: 'ok' }, SEGREDO_JWT, {\n    expiresIn: '1h',\n  });\n}\n\n\/\/ Middleware que exige sessao totalmente autenticada\nexport function exigirMfaCompleto(req, res, next) {\n  const cabecalho = req.headers.authorization || '';\n  const token = cabecalho.replace('Bearer ', '');\n  try {\n    const dados = jwt.verify(token, SEGREDO_JWT);\n    if (dados.mfa !== 'ok') {\n      return res.status(403).json({ erro: 'Segundo fator em falta' });\n    }\n    req.userId = dados.sub;\n    next();\n  } catch {\n    return res.status(401).json({ erro: 'Token invalido' });\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Esta separa\u00e7\u00e3o evita um erro cl\u00e1ssico: emitir a sess\u00e3o completa logo ap\u00f3s a palavra-passe e tratar o 2FA como mera formalidade ignor\u00e1vel. Com o token parcial, qualquer endpoint protegido recusa o acesso at\u00e9 o segundo fator estar validado. Para mais detalhe sobre tokens de sess\u00e3o, veja o nosso tutorial de autentica\u00e7\u00e3o JWT, ligado em baixo.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"passo-9-lidar-com-a-deriva-de-relogio-window\">Passo 9: Lidar com a deriva de rel\u00f3gio (window)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">O TOTP depende de rel\u00f3gios sincronizados. Se o rel\u00f3gio do telem\u00f3vel do utilizador estiver atrasado ou adiantado alguns segundos, o c\u00f3digo que ele v\u00ea pode n\u00e3o coincidir com o que o servidor calcula nesse instante. A op\u00e7\u00e3o <code>window<\/code> resolve a maior parte destes casos ao aceitar tamb\u00e9m os c\u00f3digos das janelas vizinhas.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Valor de window<\/th><th>Janelas aceites<\/th><th>Toler\u00e2ncia total<\/th><th>Recomenda\u00e7\u00e3o<\/th><\/tr><\/thead><tbody><tr><td>0<\/td><td>S\u00f3 a atual<\/td><td>At\u00e9 30 s<\/td><td>Demasiado r\u00edgido<\/td><\/tr><tr><td>1<\/td><td>Anterior, atual, seguinte<\/td><td>Cerca de 90 s<\/td><td>Equil\u00edbrio recomendado<\/td><\/tr><tr><td>2<\/td><td>Duas para cada lado<\/td><td>Cerca de 150 s<\/td><td>S\u00f3 com rel\u00f3gios pouco fi\u00e1veis<\/td><\/tr><tr><td>4 ou mais<\/td><td>Quatro ou mais<\/td><td>Mais de 4 min<\/td><td>Risco de seguran\u00e7a, evitar<\/td><\/tr><\/tbody><\/table><figcaption class=\"wp-block-table__caption\">Efeito do par\u00e2metro window na toler\u00e2ncia e na seguran\u00e7a.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">N\u00e3o caia na tenta\u00e7\u00e3o de subir o <code>window<\/code> para reduzir queixas de suporte. Cada janela extra alarga a janela de oportunidade para um atacante reutilizar um c\u00f3digo capturado. O valor <code>1<\/code> que definimos no passo 3 cobre a esmagadora maioria dos casos reais. Garanta antes que o rel\u00f3gio do seu servidor est\u00e1 sincronizado via NTP, porque a causa mais comum de falhas n\u00e3o \u00e9 o telem\u00f3vel do utilizador, \u00e9 o servidor com a hora errada.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"passo-10-proteger-contra-forca-bruta-rate-limiting\">Passo 10: Proteger contra for\u00e7a bruta (rate limiting)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Um c\u00f3digo de seis d\u00edgitos tem apenas um milh\u00e3o de combina\u00e7\u00f5es. Sem prote\u00e7\u00e3o, um atacante com a palavra-passe correta poderia tentar todos os c\u00f3digos por for\u00e7a bruta at\u00e9 acertar dentro da janela de toler\u00e2ncia. A defesa \u00e9 limitar as tentativas por conta e bloquear temporariamente ap\u00f3s um n\u00famero pequeno de falhas.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ limitador.js\n\/\/ Limitador simples em memoria (use Redis em producao)\nconst tentativas = new Map();\n\nconst MAX_FALHAS = 5;\nconst JANELA_MS = 15 * 60 * 1000; \/\/ 15 minutos\n\nexport function podeTentar(userId) {\n  const registo = tentativas.get(userId);\n  if (!registo) return true;\n  if (Date.now() &gt; registo.ate) {\n    tentativas.delete(userId);\n    return true;\n  }\n  return registo.falhas &lt; MAX_FALHAS;\n}\n\nexport function registarFalha(userId) {\n  const registo = tentativas.get(userId) || { falhas: 0, ate: Date.now() + JANELA_MS };\n  registo.falhas += 1;\n  registo.ate = Date.now() + JANELA_MS;\n  tentativas.set(userId, registo);\n}\n\nexport function limparTentativas(userId) {\n  tentativas.delete(userId);\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Em produ\u00e7\u00e3o, troque o <code>Map<\/code> em mem\u00f3ria por Redis ou outra cache partilhada, caso contr\u00e1rio o limite n\u00e3o funciona com v\u00e1rios processos ou servidores. Limpe o contador ap\u00f3s uma verifica\u00e7\u00e3o bem-sucedida com <code>limparTentativas<\/code>, para que um utilizador leg\u00edtimo que erre o c\u00f3digo uma vez n\u00e3o fique penalizado depois de acertar.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"passo-11-testar-o-fluxo-completo\">Passo 11: Testar o fluxo completo<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Antes de montar o servidor final, valide a l\u00f3gica de ponta a ponta com um pequeno script. Isto confirma que a gera\u00e7\u00e3o do segredo, a cria\u00e7\u00e3o do c\u00f3digo e a verifica\u00e7\u00e3o encaixam, sem precisar de telem\u00f3vel.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ teste.js\nimport { authenticator } from 'otplib';\nimport { gerarSegredo, verificarCodigo } from '.\/totp.js';\n\nconst segredo = gerarSegredo();\nconsole.log('Segredo Base32:', segredo);\n\n\/\/ Simula a app: gera o codigo atual a partir do mesmo segredo\nconst codigoAtual = authenticator.generate(segredo);\nconsole.log('Codigo gerado:', codigoAtual);\n\nconsole.log('Verificacao valida:', verificarCodigo(codigoAtual, segredo));\nconsole.log('Verificacao invalida:', verificarCodigo('000000', segredo));<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Execute com <code>node teste.js<\/code>. A sa\u00edda esperada confirma que o c\u00f3digo gerado passa e que um c\u00f3digo aleat\u00f3rio falha:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Segredo Base32: KZXW6YTBOI5XW4TBORSXG===\nCodigo gerado: 482915\nVerificacao valida: true\nVerificacao invalida: false<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Se a verifica\u00e7\u00e3o v\u00e1lida devolver <code>false<\/code>, o problema quase sempre \u00e9 o rel\u00f3gio do sistema. Sincronize-o e repita. Quando este teste passa, a base criptogr\u00e1fica est\u00e1 correta e pode confiar nela no servidor.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"passo-12-projeto-completo-funcional-server-js\">Passo 12: Projeto completo funcional (server.js)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Juntamos tudo num servidor Express com os tr\u00eas endpoints essenciais: ativar o 2FA (devolve o QR), confirmar a ativa\u00e7\u00e3o, e fazer login com o segundo fator. Para manter o exemplo focado, usamos um objeto em mem\u00f3ria como base de dados; substitua-o pela sua camada de persist\u00eancia real.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ server.js\nimport express from 'express';\nimport { gerarSegredo, gerarOtpauthUri, gerarQrCode, verificarCodigo } from '.\/totp.js';\nimport { cifrar, decifrar } from '.\/cripto.js';\nimport { gerarCodigosRecuperacao, fazerHashCodigos } from '.\/recuperacao.js';\nimport { emitirTokenCompleto, exigirMfaCompleto } from '.\/auth.js';\nimport { podeTentar, registarFalha, limparTentativas } from '.\/limitador.js';\n\nconst app = express();\napp.use(express.json());\n\n\/\/ \"Base de dados\" em memoria apenas para demonstracao\nconst utilizadores = new Map();\nutilizadores.set('ana@exemplo.pt', { id: 'u1', mfaAtivo: false, segredo: null });\n\n\/\/ 1) Iniciar ativacao do 2FA: devolve QR e segredo\napp.post('\/2fa\/ativar', async (req, res) =&gt; {\n  const { email } = req.body;\n  const u = utilizadores.get(email);\n  if (!u) return res.status(404).json({ erro: 'Utilizador nao encontrado' });\n\n  const segredo = gerarSegredo();\n  const uri = gerarOtpauthUri(email, segredo);\n  const qr = await gerarQrCode(uri);\n\n  \/\/ Guarda o segredo cifrado, ainda como pendente\n  u.segredoPendente = cifrar(segredo);\n  res.json({ qr, segredoManual: segredo });\n});\n\n\/\/ 2) Confirmar ativacao: o utilizador introduz o primeiro codigo\napp.post('\/2fa\/confirmar', (req, res) =&gt; {\n  const { email, codigo } = req.body;\n  const u = utilizadores.get(email);\n  if (!u || !u.segredoPendente) return res.status(400).json({ erro: 'Sem ativacao pendente' });\n\n  const segredo = decifrar(u.segredoPendente);\n  if (!verificarCodigo(codigo, segredo)) {\n    return res.status(401).json({ erro: 'Codigo invalido' });\n  }\n\n  u.segredo = u.segredoPendente;\n  u.mfaAtivo = true;\n  delete u.segredoPendente;\n\n  const recuperacao = gerarCodigosRecuperacao();\n  u.recuperacaoHashes = fazerHashCodigos(recuperacao);\n  res.json({ ativo: true, codigosRecuperacao: recuperacao });\n});\n\n\/\/ 3) Login com segundo fator (palavra-passe ja validada antes)\napp.post('\/login\/2fa', (req, res) =&gt; {\n  const { email, codigo } = req.body;\n  const u = utilizadores.get(email);\n  if (!u || !u.mfaAtivo) return res.status(400).json({ erro: '2FA nao ativo' });\n\n  if (!podeTentar(u.id)) {\n    return res.status(429).json({ erro: 'Demasiadas tentativas. Tente mais tarde.' });\n  }\n\n  const segredo = decifrar(u.segredo);\n  if (!verificarCodigo(codigo, segredo)) {\n    registarFalha(u.id);\n    return res.status(401).json({ erro: 'Codigo invalido' });\n  }\n\n  limparTentativas(u.id);\n  res.json({ token: emitirTokenCompleto(u.id) });\n});\n\n\/\/ Endpoint protegido de exemplo\napp.get('\/perfil', exigirMfaCompleto, (req, res) =&gt; {\n  res.json({ mensagem: 'Acesso concedido', userId: req.userId });\n});\n\napp.listen(3000, () =&gt; console.log('API 2FA a correr em http:\/\/localhost:3000'));<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Arranque com <code>npm run dev<\/code>. Teste a ativa\u00e7\u00e3o com um pedido ao endpoint <code>\/2fa\/ativar<\/code>, leia o QR com a sua app, e confirme com <code>\/2fa\/confirmar<\/code>. A partir da\u00ed, cada login passa por <code>\/login\/2fa<\/code>. Tem agora um sistema de <strong>autentica\u00e7\u00e3o de dois fatores<\/strong> funcional, com segredo cifrado, c\u00f3digos de recupera\u00e7\u00e3o e limita\u00e7\u00e3o de tentativas.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"erros-comuns-ao-implementar-2fa\">Erros comuns ao implementar 2FA<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Cinco armadilhas aparecem repetidamente em implementa\u00e7\u00f5es reais. Evit\u00e1-las desde o in\u00edcio poupa horas de depura\u00e7\u00e3o e fecha buracos de seguran\u00e7a.<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>Guardar o segredo em texto simples.<\/strong> Se a base de dados vazar, todos os segundos fatores caem de uma vez. Cifre sempre em repouso, como no passo 5.<\/li><li><strong>N\u00e3o confirmar a ativa\u00e7\u00e3o.<\/strong> Ativar o 2FA sem pedir um primeiro c\u00f3digo v\u00e1lido tranca utilizadores que leram mal o QR. Exija sempre um c\u00f3digo de confirma\u00e7\u00e3o antes de marcar o 2FA como ativo.<\/li><li><strong>Esquecer os c\u00f3digos de recupera\u00e7\u00e3o.<\/strong> Sem plano B, um telem\u00f3vel perdido \u00e9 uma conta perdida. Gere os c\u00f3digos no registo e guarde apenas o hash.<\/li><li><strong>Abrir demasiado o window.<\/strong> Subir a toler\u00e2ncia para calar queixas de suporte alarga a janela de ataque. Sincronize o rel\u00f3gio do servidor em vez de relaxar a verifica\u00e7\u00e3o.<\/li><li><strong>N\u00e3o limitar tentativas.<\/strong> Seis d\u00edgitos s\u00e3o um milh\u00e3o de hip\u00f3teses. Sem rate limiting, a for\u00e7a bruta torna-se vi\u00e1vel. Bloqueie ap\u00f3s poucas falhas.<\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"resolucao-de-problemas-troubleshooting\">Resolu\u00e7\u00e3o de problemas (troubleshooting)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Os oito problemas seguintes cobrem quase todos os pedidos de ajuda que surgem ao implementar TOTP em Node.js. A tabela liga sintoma a causa e solu\u00e7\u00e3o.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Sintoma<\/th><th>Causa prov\u00e1vel<\/th><th>Solu\u00e7\u00e3o<\/th><\/tr><\/thead><tbody><tr><td>C\u00f3digo sempre inv\u00e1lido<\/td><td>Rel\u00f3gio do servidor dessincronizado<\/td><td>Ativar NTP no servidor; testar com data correta<\/td><\/tr><tr><td>App n\u00e3o l\u00ea o QR<\/td><td>URI otpauth malformada ou emissor com carateres especiais<\/td><td>Validar a URI; usar emissor alfanum\u00e9rico simples<\/td><\/tr><tr><td>otplib lan\u00e7a exce\u00e7\u00e3o<\/td><td>Segredo n\u00e3o est\u00e1 em Base32 v\u00e1lido<\/td><td>Gerar o segredo s\u00f3 com generateSecret; n\u00e3o inventar<\/td><\/tr><tr><td>C\u00f3digo v\u00e1lido na app, falha no servidor<\/td><td>Algoritmo ou d\u00edgitos diferentes do padr\u00e3o<\/td><td>Fixar digits 6, step 30, algorithm sha1<\/td><\/tr><tr><td>Erro ao decifrar o segredo<\/td><td>TOTP_ENC_KEY mudou ou est\u00e1 em falta<\/td><td>Garantir chave est\u00e1vel e com 64 carateres hex<\/td><\/tr><tr><td>Login passa sem 2FA<\/td><td>Endpoint protegido aceita token parcial<\/td><td>Verificar mfa igual a ok no middleware<\/td><\/tr><tr><td>Erro de import\/m\u00f3dulo<\/td><td>type n\u00e3o definido como module<\/td><td>Definir type module no package.json<\/td><\/tr><tr><td>Express 5 d\u00e1 erro de rota<\/td><td>Sintaxe de rota antiga incompat\u00edvel<\/td><td>Atualizar padr\u00f5es de rota ou usar Node 22+<\/td><\/tr><\/tbody><\/table><figcaption class=\"wp-block-table__caption\">Oito problemas comuns de TOTP em Node.js, com causa e solu\u00e7\u00e3o.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Para diagnosticar o problema mais frequente, o rel\u00f3gio, gere no servidor e no telem\u00f3vel o c\u00f3digo no mesmo segredo e compare. Se diferem, a hora \u00e9 a culpada. Sincronize via NTP e o sintoma desaparece quase sempre.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"dicas-avancadas-e-o-caminho-para-as-passkeys\">Dicas avan\u00e7adas e o caminho para as passkeys<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Com o sistema base a funcionar, h\u00e1 melhorias que elevam a robustez sem grande esfor\u00e7o. A primeira \u00e9 guardar a \u00faltima janela TOTP usada por cada utilizador e recusar a sua reutiliza\u00e7\u00e3o. Isto impede o ataque em que um c\u00f3digo capturado \u00e9 reenviado dentro da mesma janela de 30 segundos, fechando parcialmente a brecha de phishing em tempo real do TOTP.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A segunda \u00e9 for\u00e7ar HTTPS e cookies seguros em todo o fluxo, com a flag <code>HttpOnly<\/code> e <code>SameSite=Strict<\/code> nos cookies de sess\u00e3o. O 2FA protege o login, mas n\u00e3o serve de nada se a sess\u00e3o for roubada por um canal inseguro depois. A terceira \u00e9 registar eventos de seguran\u00e7a: ativa\u00e7\u00f5es de 2FA, falhas repetidas, uso de c\u00f3digos de recupera\u00e7\u00e3o. Estes registos alimentam alertas e ajudam na resposta a incidentes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Olhando para a frente, o destino s\u00e3o as passkeys. A norma <a href=\"https:\/\/fidoalliance.org\/passkeys\/\" target=\"_blank\" rel=\"noopener\">FIDO2\/WebAuthn<\/a> liga a credencial \u00e0 origem do site, o que torna o phishing praticamente in\u00fatil: uma passkey criada para o seu dom\u00ednio n\u00e3o funciona num dom\u00ednio falso, ao contr\u00e1rio de um c\u00f3digo TOTP que o utilizador pode ser enganado a escrever em qualquer lado. A estrat\u00e9gia sensata em 2026 \u00e9 oferecer TOTP hoje, como segundo fator universal, e adicionar passkeys como op\u00e7\u00e3o preferencial, mantendo o TOTP como alternativa de compatibilidade. Para o enquadramento de boas pr\u00e1ticas de MFA, a <a href=\"https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/Multifactor_Authentication_Cheat_Sheet.html\" target=\"_blank\" rel=\"noopener\">folha de dicas de MFA da OWASP<\/a> \u00e9 a refer\u00eancia a consultar.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"comparacao-totp-sms-e-passkeys\">Compara\u00e7\u00e3o: TOTP, SMS e passkeys<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Para escolher bem o segundo fator, conv\u00e9m ter os tr\u00eas m\u00e9todos lado a lado. A tabela resume as diferen\u00e7as que mais pesam na decis\u00e3o.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Crit\u00e9rio<\/th><th>SMS<\/th><th>TOTP (este tutorial)<\/th><th>Passkey (FIDO2)<\/th><\/tr><\/thead><tbody><tr><td>Resistente a SIM swap<\/td><td>N\u00e3o<\/td><td>Sim<\/td><td>Sim<\/td><\/tr><tr><td>Resistente a phishing<\/td><td>N\u00e3o<\/td><td>Parcial<\/td><td>Sim<\/td><\/tr><tr><td>Funciona offline<\/td><td>N\u00e3o<\/td><td>Sim<\/td><td>Sim<\/td><\/tr><tr><td>Custo por c\u00f3digo<\/td><td>Pago (gateway SMS)<\/td><td>Zero<\/td><td>Zero<\/td><\/tr><tr><td>Suporte universal de apps<\/td><td>Total<\/td><td>Total<\/td><td>A crescer<\/td><\/tr><tr><td>Esfor\u00e7o de implementa\u00e7\u00e3o<\/td><td>Baixo<\/td><td>M\u00e9dio<\/td><td>M\u00e9dio a alto<\/td><\/tr><\/tbody><\/table><figcaption class=\"wp-block-table__caption\">Compara\u00e7\u00e3o dos tr\u00eas segundos fatores mais comuns em 2026.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">A leitura \u00e9 clara. O SMS est\u00e1 em fim de vida como fator s\u00e9rio por causa do SIM swap e do custo. O TOTP \u00e9 o equil\u00edbrio pr\u00e1tico para implementar j\u00e1, sem custos e com suporte universal. A passkey \u00e9 o futuro resistente a phishing, mas ainda exige mais trabalho e suporte do lado do cliente. Implementar TOTP hoje n\u00e3o fecha portas: \u00e9 o degrau interm\u00e9dio certo no caminho para as passkeys.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"perguntas-frequentes\">Perguntas frequentes<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"o-totp-e-mesmo-seguro-em-2026\">O TOTP \u00e9 mesmo seguro em 2026?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Sim, para a maioria das aplica\u00e7\u00f5es. O TOTP elimina o risco de SIM swap do SMS e funciona offline. A sua \u00fanica fraqueza s\u00e9ria \u00e9 o phishing em tempo real, mitigado com limita\u00e7\u00e3o de tentativas, recusa de reutiliza\u00e7\u00e3o de c\u00f3digos e, idealmente, com a ado\u00e7\u00e3o futura de passkeys. Para um servi\u00e7o comum, o TOTP bem implementado \u00e9 uma melhoria enorme face a s\u00f3 palavra-passe.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"devo-usar-otplib-ou-speakeasy\">Devo usar otplib ou speakeasy?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Recomendamos o otplib (13.4.1). \u00c9 mantido ativamente, tem API moderna e separa TOTP de HOTP de forma limpa. O speakeasy (2.0.0) ainda funciona, mas n\u00e3o recebe atualiza\u00e7\u00f5es h\u00e1 bastante tempo. Para projetos novos em 2026, o otplib \u00e9 a escolha mais segura a longo prazo.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"porque-e-que-o-codigo-do-utilizador-as-vezes-falha\">Porque \u00e9 que o c\u00f3digo do utilizador \u00e0s vezes falha?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Na esmagadora maioria dos casos, \u00e9 deriva de rel\u00f3gio. Se o servidor n\u00e3o estiver sincronizado por NTP, calcula c\u00f3digos para um instante ligeiramente diferente do telem\u00f3vel. A op\u00e7\u00e3o <code>window: 1<\/code> tolera pequenas diferen\u00e7as, mas a solu\u00e7\u00e3o de raiz \u00e9 manter a hora do servidor correta.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"preciso-de-cifrar-o-segredo-na-base-de-dados\">Preciso de cifrar o segredo na base de dados?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Sim, sem exce\u00e7\u00e3o. O segredo TOTP \u00e9 equivalente a uma palavra-passe permanente: quem o tiver gera c\u00f3digos v\u00e1lidos para sempre. Cifre-o em repouso com AES-256-GCM e uma chave guardada fora da base de dados, como mostr\u00e1mos no passo 5.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"quantos-codigos-de-recuperacao-devo-gerar\">Quantos c\u00f3digos de recupera\u00e7\u00e3o devo gerar?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Dez \u00e9 o padr\u00e3o da ind\u00fastria e o valor que us\u00e1mos. Cada um serve uma \u00fanica vez. Guarde apenas o hash de cada c\u00f3digo e mostre os c\u00f3digos em texto ao utilizador s\u00f3 uma vez, no momento da ativa\u00e7\u00e3o, com aviso claro para os guardar em local seguro.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"o-totp-substitui-a-palavra-passe\">O TOTP substitui a palavra-passe?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">N\u00e3o. O TOTP \u00e9 um segundo fator, n\u00e3o o primeiro. Combina-se com a palavra-passe para formar a <strong>autentica\u00e7\u00e3o de dois fatores<\/strong>. Para substituir totalmente a palavra-passe precisaria de passkeys, que podem funcionar como fator \u00fanico resistente a phishing. At\u00e9 l\u00e1, palavra-passe forte mais TOTP \u00e9 a base recomendada.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"posso-usar-sha-256-em-vez-de-sha-1-no-totp\">Posso usar SHA-256 em vez de SHA-1 no TOTP?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Tecnicamente sim, mas perde compatibilidade. Muitas apps de autentica\u00e7\u00e3o assumem HMAC-SHA1 e ignoram o par\u00e2metro de algoritmo na URI otpauth. No contexto do HMAC-TOTP, o SHA-1 mant\u00e9m-se seguro, por isso a recomenda\u00e7\u00e3o pr\u00e1tica \u00e9 fixar SHA1 para garantir que todas as apps leem corretamente.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"cobertura-relacionada\">Cobertura relacionada<\/h3>\n\n\n\n<ul class=\"wp-block-list\"><li><a href=\"\/authentification-jwt-nodejs\/\">Autentica\u00e7\u00e3o JWT em Node.js: 12 passos<\/a><\/li><li><a href=\"\/argon2-password-hashing-nodejs\/\">Argon2 para hashing de palavras-passe em Node.js<\/a><\/li><li><a href=\"\/aes-256-encryption-nodejs\/\">Cifra AES-256 em Node.js, passo a passo<\/a><\/li><li><a href=\"\/password-security\/\">Seguran\u00e7a de palavras-passe: o que protege mesmo as contas<\/a><\/li><li><a href=\"\/phishing-attacks\/\">Ataques de phishing: como reconhecer e evitar<\/a><\/li><li><a href=\"\/security\/\">Guia pr\u00e1tico de seguran\u00e7a online (p\u00e1gina pilar)<\/a><\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Fontes t\u00e9cnicas e normas: <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc6238\" target=\"_blank\" rel=\"noopener\">RFC 6238 (TOTP)<\/a>, <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc4226\" target=\"_blank\" rel=\"noopener\">RFC 4226 (HOTP)<\/a>, <a href=\"https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/Multifactor_Authentication_Cheat_Sheet.html\" target=\"_blank\" rel=\"noopener\">OWASP MFA Cheat Sheet<\/a>, <a href=\"https:\/\/fidoalliance.org\/passkeys\/\" target=\"_blank\" rel=\"noopener\">FIDO Alliance (passkeys)<\/a> e <a href=\"https:\/\/nodejs.org\/en\/download\" target=\"_blank\" rel=\"noopener\">Node.js (vers\u00f5es LTS)<\/a>.<\/p>\n\n","protected":false},"excerpt":{"rendered":"<p>A autentica\u00e7\u00e3o de dois fatores deixou de ser um extra opcional. Em 2026, qualquer aplica\u00e7\u00e3o web que guarde dados sens\u00edveis precisa de uma segunda barreira para l\u00e1 da palavra-passe. A\u2026<\/p>\n","protected":false},"author":5,"featured_media":55,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-54","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-security"],"_links":{"self":[{"href":"https:\/\/shattered.io\/pt\/wp-json\/wp\/v2\/posts\/54","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/shattered.io\/pt\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/shattered.io\/pt\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/shattered.io\/pt\/wp-json\/wp\/v2\/users\/5"}],"replies":[{"embeddable":true,"href":"https:\/\/shattered.io\/pt\/wp-json\/wp\/v2\/comments?post=54"}],"version-history":[{"count":0,"href":"https:\/\/shattered.io\/pt\/wp-json\/wp\/v2\/posts\/54\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/pt\/wp-json\/wp\/v2\/media\/55"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/pt\/wp-json\/wp\/v2\/media?parent=54"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/pt\/wp-json\/wp\/v2\/categories?post=54"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/pt\/wp-json\/wp\/v2\/tags?post=54"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}