Pitch: colocamos login com passkey num editor SQL web. O que aprendemos sobre MFA, TOTP, OIDC e JWT
Um editor de banco que roda no navegador tem um problema de login diferente do da maioria dos apps. Quem entra não ganha só uma conta: ganha as conexões salvas, com as credenciais já configuradas. O Postgres de produção, o Redis, o Mongo. Uma senha vazada ali não expõe um perfil, expõe os dados.
Sou um dos mantenedores do LibreDB Studio, um editor SQL open source (MIT) que roda no navegador, e acabamos de colocar login com passkey. No caminho, esbarramos o tempo todo na mesma confusão: MFA, TOTP, passkey, OIDC e JWT aparecem juntos em qualquer conversa sobre login, mas cada um responde a uma pergunta diferente.
Cada um responde a uma pergunta
- Senha: "você sabe o segredo?" É um fator só, e pode ser reutilizada, vazada ou digitada num site falso.
- MFA: não é uma tecnologia, é uma regra: provar pelo menos dois fatores de tipos diferentes (algo que você sabe, tem ou é). Senha + TOTP é MFA. Uma passkey desbloqueada com PIN ou biometria também é.
- TOTP: um jeito de provar "estou com o celular". Servidor e app compartilham um segredo, e o código é um HMAC desse segredo com o horário em janelas de 30 segundos, cortado em 6 dígitos (RFC 6238).
- Passkey (WebAuthn): um par de chaves. A privada fica no dispositivo ou no gerenciador de senhas, e o servidor guarda só a pública. Para entrar, o dispositivo assina um desafio que o servidor mandou.
- OIDC: não é um método de login, é delegação. Outro sistema (Keycloak, Entra ID, Okta, Google) autentica o usuário do jeito dele e devolve um ID token para o app.
- JWT: também não é login, é um formato de token assinado. Depois que o usuário provou quem é, por qualquer um dos caminhos acima, o servidor emite uma sessão, e essa sessão pode ser um JWT.
Na prática, "OIDC ou passkey?" não é uma escolha no mesmo nível, porque com OIDC as passkeys moram no IdP. Se o seu time já tem um IdP, use-o: MFA, passkey e política de dispositivo ficam num lugar só. Passkey no próprio app é para quem roda sem IdP, o caso comum de time pequeno com tudo self-hosted. E "usamos JWT" não diz nada sobre quão forte é o login.
Por que TOTP não basta
O TOTP protege contra senha vazada, mas não contra phishing. O usuário lê seis dígitos e digita onde pedirem. Um proxy de phishing no estilo Evilginx mostra a tela de login verdadeira, recebe senha e código e repassa os dois ao site real dentro dos 30 segundos. O segundo fator funcionou, só que para o atacante.
A passkey resolve isso por construção, não pela atenção do usuário. O navegador coloca a origem da página dentro do que é assinado e só usa a passkey no domínio em que ela foi criada. Numa página em studio-login.exemplo.com, a passkey de studio.exemplo.com nem aparece, e uma assinatura feita em outra origem não passa na verificação do servidor.
Um ganho menos falado: o servidor guarda só a chave pública. Um vazamento do banco de contas não entrega nada que sirva para entrar, ao contrário do segredo TOTP, que o servidor precisa guardar para calcular o código.
O que aprendemos implementando
- Exija user verification. Sem ela, uma chave de segurança sem PIN prova só "estou com a chave". Se o login por passkey pular o TOTP, você trocou dois fatores por um. Exigindo PIN ou biometria no cadastro e no login, a passkey conta como MFA completo (NIST SP 800-63B-4, AAL2) e pode substituir senha e código.
- A origem vem da configuração, não da requisição. A origem esperada e o RP ID saem de uma variável (
PASSKEY_ORIGIN), nunca doHost, doX-Forwarded-Hostou da URL. Se saíssem da requisição, a checagem compararia a assinatura com o que a própria requisição afirma e deixaria de checar qualquer coisa. Atrás de um proxy reverso, esse valor ainda costuma ser o nome interno, não o que o navegador viu. - Cadastrar uma passkey pede a senha de novo. Com um cookie de sessão roubado, o atacante poderia cadastrar a passkey dele, uma porta dos fundos que continua abrindo depois que a sessão expira. Por isso adicionar ou remover uma passkey pede a senha atual, e o código TOTP quando a conta tem um.
O JWT entra no fim. Depois de qualquer login (senha + TOTP, passkey ou OIDC), o servidor emite a mesma sessão: um JWT num cookie HttpOnly. O ponto fraco clássico do JWT é que não dá para revogar um token já emitido. Por isso o nosso leva uma versão de sessão que o servidor confere com a conta guardada: desativar a conta ou remover uma passkey muda essa versão, e os outros tokens deixam de valer.
Para testar
O login com passkey chegou na versão [VERSÃO]. Para ver funcionando na sua máquina:
docker run -d --name libredb-studio -p 3000:3000 \
-v libredb-data:/app/data \
-e STORAGE_PROVIDER=sqlite \
-e PASSKEY_ORIGIN=http://localhost:3000 \
ghcr.io/libredb/libredb-studio:latest
Abra exatamente http://localhost:3000, não o 127.0.0.1: o navegador não aceita passkey num endereço IP. Entre como admin@libredb.org com a senha que aparece em docker logs libredb-studio, abra o menu do usuário, Sign-in security, Add passkey. Depois saia e use Use a passkey na tela de login.
O fluxo completo, e o que isso protege e não protege, está em docs/PASSKEYS.md: https://github.com/libredb/libredb-studio/blob/main/docs/PASSKEYS.md.
Queria ouvir quem opera banco em produção: no time de vocês, o acesso ao editor de banco passa por SSO ou por conta local? E alguém já viu phishing de TOTP em tempo real acontecer de verdade?
Fonte: https://libredb.org