Skip to content

feat(auth): step-up MFA TOTP por app com janela de confiança configurável #1005

Description

@milkway

Contexto

O Ruscker possui contas locais criadas por administradores, login por usuário/senha e controle de acesso por app (access-groups / access-users), mas não possui segundo fator.

Queremos permitir que apenas aplicações sensíveis exijam autenticação reforçada. Usuários que nunca acessarem um app protegido não devem ser obrigados a configurar MFA.

Relacionado:

Decisão de produto

Implementar step-up MFA por app, inicialmente com TOTP compatível com Google Authenticator, Microsoft Authenticator, Authy, 1Password e equivalentes.

O Ruscker não envia o código: o aplicativo autenticador gera localmente um código temporário a partir de um segredo cadastrado por QR Code. E-mail/SMS e links de convite ficam fora deste MVP.

O fator pertence ao usuário, não ao app. O usuário cadastra TOTP uma única vez; cada app decide se exige MFA e qual a idade máxima aceita para a última comprovação.

Configuração por app

Adicionar campos opcionais ao Spec, preservando compatibilidade:

require-mfa: true
mfa-validity-days: 7
  • require-mfa: Option<bool>, padrão false.
  • mfa-validity-days: Option<u16>, usado somente quando require-mfa=true.
  • Proposta de semântica: padrão efetivo de 7 dias e máximo validado de 30 dias.
  • Valor 0: comprovação válida somente na sessão de login atual, sem dispositivo lembrado entre logins.

No formulário de apps:

  • checkbox “Exigir 2FA”;
  • ao marcar, mostrar “Solicitar novamente após N dias”;
  • ajuda explicando que usuários sem TOTP serão guiados pelo cadastro no primeiro acesso;
  • campos disponíveis no YAML, import/export, histórico de versão e quatro locales.

Fluxo do usuário

App sem MFA

Comportamento atual, sem qualquer prompt adicional.

App com MFA — usuário anônimo

  1. Redirecionar para o login normal, preservando next.
  2. Após usuário/senha, continuar para a decisão de step-up do app.

App com MFA — usuário ainda sem TOTP

  1. Redirecionar para uma página autenticada de cadastro, por exemplo /account/security/mfa/setup?next=....
  2. Solicitar novamente a senha atual antes de vincular o fator.
  3. Gerar um segredo TOTP com CSPRNG e mostrar QR Code + chave manual.
  4. O próprio usuário escaneia o QR Code no dispositivo dele. O administrador nunca recebe nem visualiza o segredo.
  5. Exigir um primeiro código válido para confirmar o cadastro.
  6. Gerar códigos de recuperação de uso único e mostrá-los uma única vez.
  7. Retornar ao app originalmente solicitado.

App com MFA — usuário já cadastrado

  • Se existir uma comprovação recente válida para aquele navegador/dispositivo e ela estiver dentro de mfa-validity-days, abrir o app diretamente.
  • Caso contrário, solicitar um código TOTP e, após sucesso, retornar ao app.
  • Uma comprovação TOTP pode satisfazer todos os apps: cada app compara a idade de mfa_verified_at com sua própria janela. Exemplo: uma comprovação de 3 dias abre um app com janela de 7 dias, mas não um app com janela de 1 dia.

Enforcement

  • Aplicar o guard no proxy antes de spawn, seleção de réplica ou encaminhamento ao container.
  • Proteger acesso direto por URL, não apenas o clique no card.
  • Preservar next com segurança: somente paths locais e normalizados, incluindo server.context-path.
  • O MVP cobre apps interativos em /app/*.
  • APIs em /api/* não recebem redirects HTML: se a política for configurada futuramente para APIs, responder 401/403 de forma explícita. Definir suporte a API e external links fora deste MVP.
  • Usuários Admin autenticados por conta também cumprem MFA. A sessão de token break-glass pode fazer bypass para evitar lockout, mas deve gerar log/auditoria destacada.

Persistência e HA

Não guardar apenas um timestamp global no usuário: isso faria qualquer navegador aproveitar a validação feita em outro dispositivo.

Criar um grant de dispositivo confiável:

  • cookie opaco, aleatório, HttpOnly, Secure, SameSite=Strict;
  • token armazenado somente como hash no servidor;
  • vínculo com usuário, dispositivo/navegador, mfa_verified_at, expiração e versão do fator;
  • implementação SQLite e Postgres para manter o comportamento em HA;
  • validade efetiva limitada pelo menor prazo entre o grant e a política do app;
  • revogar grants em reset/troca de senha, reset do MFA, exclusão/desativação do usuário e “sair de todos os dispositivos”;
  • um logout comum continua removendo a sessão primária; a decisão de manter ou remover o dispositivo lembrado deve ficar explícita na UI (“Esquecer este dispositivo”).

A sessão primária atual dura 24h, portanto janelas de vários dias não podem depender somente de AdminSessionStore/SessionInfo.

Armazenamento do fator

  • Segredo TOTP criptografado em repouso com RUSCKER_MASTER_KEY; nunca em claro no DB, logs, audit ou HTML depois do enrollment.
  • Se não houver master key, cadastro e uso de apps com require-mfa=true falham fechados, com diagnóstico operacional claro.
  • Códigos de recuperação armazenados como hashes e consumidos uma única vez.
  • Administrador vê somente o estado “2FA configurado” e pode executar um reset auditado após verificar a identidade; nunca vê QR Code/segredo.

Segurança do desafio

  • Código TOTP com janela curta e tolerância de relógio limitada.
  • Impedir replay do mesmo contador TOTP após sucesso.
  • Limite estrito de tentativas por usuário/desafio e backoff; resposta sem enumeração de contas.
  • Operações de setup, reset e recuperação protegidas contra CSRF e session fixation.
  • Não registrar códigos, segredos, recovery codes ou tokens de dispositivo.
  • Auditoria: mfa.enroll, mfa.verify, mfa.recovery_used, mfa.reset, mfa.trusted_device.revoke e mfa.break_glass_bypass.

UX administrativo

  • O administrador continua criando o usuário e a senha inicial como hoje.
  • Não há cadastro prévio de Google Authenticator pelo administrador.
  • O usuário é conduzido ao enrollment somente quando tenta acessar o primeiro app protegido.
  • Na tela de usuários, exibir estado do fator e ação de reset com confirmação forte e auditoria.

Critérios de aceite

  • Usuário que acessa somente apps sem MFA nunca vê setup ou desafio.
  • Acesso direto a um app protegido exige login e depois TOTP quando necessário.
  • Usuário sem fator cadastra TOTP por QR Code e retorna ao app original.
  • O mesmo fator funciona para todos os apps protegidos.
  • Cada app respeita sua própria janela de validade.
  • A comprovação é vinculada ao navegador/dispositivo, não global ao usuário.
  • Um request bloqueado não inicia nem acorda container.
  • Funciona em root e sob server.context-path.
  • Funciona em SQLite e Postgres/HA.
  • Reset de senha/MFA e exclusão do usuário revogam grants.
  • Break-glass bypass é explícito e auditado.
  • Segredos/códigos não aparecem em logs, audit, export YAML ou respostas posteriores ao enrollment.
  • Testes cobrem expiração, replay, tentativa excessiva, recovery code, revogação, acesso direto, múltiplos apps e HA.
  • Formulário, mensagens e documentação possuem paridade pt/en/es/fr.

Fatiamento sugerido

Não implementar este épico inteiro em um PR:

  1. Schema + form + validação — campos opcionais, YAML, import/export e i18n.
  2. Enrollment TOTP + recovery — armazenamento criptografado, página de segurança e reset administrativo.
  3. Desafio + grants de dispositivo — verificação, cookie opaco, expiração e revogação SQLite/Postgres.
  4. Guard por app no proxynext, base-path, bloqueio antes do spawn e break-glass auditado.
  5. Hardening + testes/Docs — rate limit, replay, CSRF, HA e documentação operacional.

Fora de escopo

  • Envio de códigos por e-mail ou SMS.
  • Convites por e-mail.
  • Cadastro do fator pelo administrador/terceiro.
  • WebAuthn/passkeys.
  • MFA para clientes de API não interativos.
  • Integração OIDC/SAML; quando Épico: autenticação externa OIDC/SAML (Phase 8 / SSO) #934 avançar, a política por app poderá aceitar amr/acr/auth_time do IdP como evidência de step-up.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions