Passkeys em Delphi: login sem senha com WebAuthn

· Componentes
Passkeys em Delphi: login sem senha com WebAuthn

Toda senha aceita pela sua aplicação pode ser adivinhada, reutilizada em outro site, digitada em uma página de login falsa ou vazar de um backup de banco de dados. As passkeys eliminam os quatro problemas de uma vez. O usuário faz login com a impressão digital, o rosto ou o PIN que já desbloqueia o telefone ou o notebook, e não há mais nenhum segredo no seu servidor que valha a pena roubar.

Na visão geral dos novos componentes de login, as passkeys ficaram com cinco tópicos, e o post sobre SAML cobriu o login para empresas que administram seu próprio identity provider. Este post é o lado sem senha: como o TsgcWSAPIServer_WebAuthn registra passkeys, faz login de usuários sem nome de usuário, mostra as passkeys na lista de autofill do navegador e guarda cada credential no seu próprio banco de dados.

O que é uma passkey

Uma passkey é um par de chaves criado pelo authenticator do usuário: Windows Hello, chaveiro do iCloud, Gerenciador de Senhas do Google, um gerenciador de senhas ou uma chave de segurança FIDO2. A chave privada nunca sai do authenticator. Seu servidor guarda apenas a chave pública, então uma tabela de passkeys vazada não permite que ninguém faça login. Cada assinatura fica vinculada ao domínio do seu site, então um domínio de phishing parecido não obtém nada que possa usar. É isso que resistente a phishing significa aqui.

Em termos WebAuthn, uma passkey é um credential detectável, também chamado de resident key. O authenticator guarda o userHandle junto com a chave privada, e é por isso que o usuário pode fazer login sem digitar um nome de usuário e o navegador pode listar sozinho as passkeys do seu site.

Pedindo credentials detectáveis

O WebAuthn precisa de um contexto seguro, então sirva suas páginas por https, ou a partir de localhost durante o desenvolvimento, e defina WebAuthnOptions.RelyingParty com o nome de host que o navegador mostra. O residentKey pedido pelas opções de registro vem de WebAuthnOptions.DefaultOptions.Registration.DiscoverableCredential:

Uma única requisição de registro ainda pode sobrescrever o padrão com o campo discoverable_credential, definido como required, preferred ou discouraged.

Login sem nome de usuário

Peça as opções de autenticação com o nome de usuário vazio. As opções então não trazem nenhuma lista allowCredentials, o navegador mostra as passkeys que possui para o seu relying party, e o usuário escolhe uma. Não há nada para digitar e, portanto, nada para digitar errado.

Como o servidor não escolheu o credential dessa vez, ele verifica mais coisas. A resposta precisa incluir o userHandle, o userHandle precisa pertencer ao credential que assinou, e um credential que sua aplicação não conhece é rejeitado.

Passkeys na lista de autofill

O autofill, ou conditional mediation, coloca as passkeys do seu site na lista de sugestões do campo de nome de usuário, ao lado das senhas salvas. Essa é a forma suave de migrar os usuários: a página de login continua funcionando para quem tem senha, e quem tem passkey a escolhe na lista.

O autofill precisa de um navegador com conditional mediation, o que hoje significa as versões atuais do Chrome, Edge e Safari. browserSupportsWebAuthnAutofill() retorna False nos demais, então mantenha também um botão “Entrar com uma passkey” na página.

Várias passkeys, um userHandle

Usuários reais têm mais de uma passkey: uma no notebook, uma no telefone, talvez uma chave de segurança para o dia em que ambos se perderem. Registre cada uma com o mesmo nome de usuário. Quando esse nome de usuário já tem credentials conhecidos pelo servidor, o novo registro reutiliza o userHandle deles (user.id), de modo que toda passkey da conta compartilha um único userHandle e todas levam ao mesmo usuário.

O servidor conhece os credentials registrados enquanto está em execução e os que você adiciona com AddCredential. Quando suas passkeys vivem em um banco de dados, adicione-as na inicialização para que cada conta mantenha um único userHandle. Quando um usuário realmente digita um nome de usuário, as opções de autenticação listam cada passkey desse usuário em allowCredentials, e o authenticator usa a que ele possui.

Guarde as passkeys no seu próprio banco de dados

O componente não persiste credentials. Suas passkeys pertencem ao lado da sua tabela de usuários, e quatro eventos conectam as duas coisas:

Salvar uma passkey e carregá-la de volta são dois handlers curtos:

uses
  sgcWebAuthn_Classes;

// registration: store the whole record, keyed by its credential id
procedure TForm1.WebAuthnWebAuthnRegistrationSuccessful(Sender: TObject;
  const aRegistration: TsgcWebAuthn_Registration;
  const aCredentialRecord: TsgcWebAuthn_CredentialRecord; var Accept: Boolean);
begin
  DBInsertPasskey(aCredentialRecord.CredentialId, aCredentialRecord.Username,
    aCredentialRecord.AsJSON);
end;

// usernameless and autofill sign-in: the browser chose the passkey,
// find it by its credential id and hand it back to the server
procedure TForm1.WebAuthnWebAuthnAuthenticationGetCredential(Sender: TObject;
  const aCredentialId: string;
  const aCredentialRecord: TsgcWebAuthn_CredentialRecord; var Found: Boolean);
var
  vJSON: string;
begin
  Found := DBFindPasskey(aCredentialId, vJSON);
  if Found then
    aCredentialRecord.ReadJSON(vJSON);
end;

DBInsertPasskey e DBFindPasskey representam o seu próprio código de acesso a dados. O registro que você retorna precisa trazer o CredentialId pedido, e o mesmo Username quando a ceremony começou com um nome de usuário, senão o login falha. Guarde também o UserId no registro armazenado, porque o servidor o compara com o userHandle enviado pelo authenticator. Os eventos rodam nas threads de conexão do servidor, então proteja os recursos compartilhados como você os protege em qualquer outro ponto do servidor.

Sincronizadas ou vinculadas ao dispositivo

As flags nos dados do authenticator dizem que tipo de passkey você recebeu, e o registro de credential as guarda como BackupEligible (a flag BE) e BackupState (a flag BS):

Sua política pode tratá-las de forma diferente, por exemplo aceitando somente chaves de segurança vinculadas ao dispositivo em contas de administrador. A cada login, o servidor rejeita uma resposta com a flag BS marcada e a flag BE desmarcada, e uma resposta cuja flag BE difere do BackupEligible armazenado, porque a eligibility de um credential nunca muda. A nova flag BS é copiada para BackupState, e OnWebAuthnAuthenticationSuccessful é o lugar para salvá-la.

O contador de assinaturas e authenticators clonados

Alguns authenticators aumentam um contador de assinaturas a cada login. Quando o contador na resposta ou o SignCount armazenado não é zero, o contador na resposta precisa ser maior que o valor armazenado. Se não for, o login é rejeitado, porque dois authenticators respondendo com a mesma chave é exatamente a aparência de um authenticator clonado.

A verificação só funciona se você salvar o contador após cada login, e o valor armazenado só avançar. Passkeys sincronizadas normalmente informam 0 sempre, o que desliga a verificação para elas. Se você tiver authenticators que não aumentam o contador de forma confiável, defina WebAuthnOptions.AllowSignCountLessOrEqualStoredValue como True para aceitá-los. O valor armazenado ainda assim nunca é reduzido.

Experimente a demo

A demo Demos\26.Authentication\01.Passkeys é um relying party completo: um TsgcWebSocketHTTPServer com um TsgcWSAPIServer_WebAuthn anexado, servindo uma pequena página de login em https://localhost:5443. Ela guarda cada passkey em um arquivo passkeys.json próprio, por meio dos quatro eventos acima.

  1. Compile a demo e mantenha libcrypto-3.dll e libssl-3.dll junto ao executável. Elas estão na pasta da demo.
  2. Deixe o host 127.0.0.1, a porta 5443 e o relying party localhost, e então clique em Start.
  3. Clique em Open Browser e aceite o certificado de teste autoassinado.
  4. Digite um nome de usuário e clique em Register passkey. Registre uma segunda passkey para o mesmo nome de usuário. O formulário lista cada passkey com seu tipo, sincronizada ou vinculada ao dispositivo, e seu contador de assinaturas.
  5. Clique em Sign in without user name e escolha uma passkey. O servidor a encontra por meio de OnWebAuthnAuthenticationGetCredential e o log mostra o usuário.
  6. Recarregue a página e clique no campo de nome de usuário. As passkeys aparecem na lista de autofill. Escolha uma para fazer login.
  7. Reinicie a aplicação. As passkeys carregam de novo a partir de passkeys.json, o que mostra que o armazenamento pertence à sua aplicação e não ao componente.

Documentação

Onde conseguir

O TsgcWSAPIServer_WebAuthn está incluído nas edições Enterprise e All-Access do sgcWebSockets, para Delphi e C++ Builder, e o mesmo componente faz parte do sgcWebSockets .NET. Se você só precisa de autenticação, o pacote sgcAuth o traz junto com os demais componentes de login. Você o encontra na paleta SGC Auth, e nada muda em uma aplicação existente até você soltá-lo em um formulário.

Leia também

Assista ao vídeo

Há um vídeo curto, “Passkeys in Delphi: passwordless login with WebAuthn”, no canal da eSeGeCe. Ele mostra o código na IDE, o que as flags de backup e o contador de assinaturas indicam, e a demo rodando em https://localhost: um usuário registra duas passkeys e então faz login sem digitar um nome de usuário.

Dúvidas, feedback ou ajuda para adicionar passkeys à sua página de login? Fale conosco. Você receberá uma resposta de quem escreveu o código.