O login que passou a pedir uma segunda prova — 2FA no Capivara
Capivara·

O login que passou a pedir uma segunda prova — 2FA no Capivara

4 min de leitura← Voltar para timeline

O login do Capivara sempre foi uma porta simples: e-mail e senha, mais nada. O problema é que o painel guarda coisas que não combinam com uma porta simples — finanças, pedidos, dados de família, o histórico de tudo que passa pelo hub. Uma senha só, para um hub inteiro, é pouco.

A peça do segundo fator já existia no banco desde 07 de outubro, cifrada, esperando. O que faltava era ligar essa peça ao login. Foi o que o commit af51d4c fez, hoje às 14h32.

O que mudou, em números

Quatro arquivos, 89 linhas adicionadas e 2 removidas:

  • backend/routers/auth.py (+47) — o fluxo de login passou a conferir o código TOTP;
  • backend/schemas.py (+2) — LoginRequest ganhou o campo totp_code, opcional;
  • frontend/src/pages/Login.tsx (+36/-2) — a segunda tela, que aparece só quando o servidor pede;
  • frontend/src/lib/i18n.tsx (+6) — o texto novo nas duas línguas.

O detalhe que faz o fluxo funcionar sem quebrar quem já entra hoje está no contrato da API: o código só é exigido quando o usuário tem um segredo TOTP verificado. Sem o código, nesse caso, a resposta é um 401 com detail: "2fa_required" — e é esse detalhe que o frontend lê para trocar o formulário. Código errado não ganha tratamento especial: entra no mesmo balde de rate limit do login e vai para o log de auditoria.

O segredo não fica em claro no banco

Antes de o login exigir o segundo fator, o segredo TOTP precisava sobreviver a um backup vazado. O backend/totp_crypto.py guarda o segredo no formato:

"v1:" + base64url( nonce(12) || ciphertext || tag(16) )

É AES-256-GCM, com nonce aleatório a cada gravação — dois segredos iguais nunca viram o mesmo blob. A decifragem tem duas decisões que valem a pena contar:

  1. valor sem o prefixo v1: é tratado como legado em claro e devolvido como está, o que permitiu migrar sem janela de manutenção e sem re-cadastrar ninguém;
  2. se o blob foi adulterado, o AESGCM levanta InvalidTag — a falha é alta, não silenciosa. Um segredo corrompido vira erro visível, não um segundo fator que aceita qualquer coisa.

O que eu consegui medir

Os dois arquivos de teste que cobrem esse caminho, rodados no virtualenv do próprio backend:

~/lifelog — bash
$cat about.txt
╔══════════════════════════════════════╗
║  Samuel Medeiros                    ║
║  Senior Software Engineer           ║
║  Stack: Python · TypeScript · Rust  ║
║  Projetos: Arachne, Dogwalk,        ║
║            Capivara, TatuEngine      ║
╚══════════════════════════════════════╝
      
$

Vale registrar o que não está medido: a suíte de frontend tem 11 falhas pré-existentes no ThemeToggle, confirmadas num worktree limpo do HEAD — não são deste trabalho e entraram na conta para não virarem “novidade” depois.

Onde isso deixa o login hoje

O único usuário cadastrado tem segredo TOTP registrado com verified = 0. Na prática: ninguém é barrado hoje, porque a exigência só vale para segredo verificado. A ativação é um passo explícito no admin — escanear o QR, confirmar um código — e só depois disso o login passa a pedir a segunda prova. É de propósito: ligar o segundo fator por padrão, sem confirmação, trancaria o dono do hub do lado de fora da própria casa.

O commit seguinte, 99a616e, ajustou a unit do serviço aos caminhos pós-migração — o backend agora sobe pelo virtualenv do próprio repositório, com diretório de trabalho fixo. Um serviço que sobe do lugar errado é a versão silenciosa do mesmo bug de porta fechada.

Próximo passo

Falta o que não se resolve em código: ativar o 2FA no admin e gerar os códigos de recuperação antes de exigir o segundo fator em qualquer outro lugar. Sem recuperação, um celular perdido vira um hub inacessível — e aí o segundo fator passa a ser o problema, não a solução.

Leia também

Mesmo projeto, mesma trilha — as histórias se conectam.

Próximo passo

Se isto te serviu, tem mais: escrevo sobre o mesmo assunto nos outros projetos, e a loja tem as ferramentas que eu uso aqui.

Sem spam, descadastro de um clique. Também estou aqui: Loja de skills · Portfólio