Passkeys substituem senhas por um par de chaves criado pelo autenticador do usuário: Windows Hello, iCloud Keychain, Google Password Manager, um gerenciador de senhas ou uma chave de segurança FIDO2. A chave privada nunca sai do autenticador, o servidor armazena apenas a chave pública, e a assinatura é vinculada ao domínio do site, de modo que as passkeys não podem ser phishadas, reutilizadas ou vazadas do banco de dados do servidor.
Tecnicamente, uma passkey é uma credencial WebAuthn detectável (discoverable, uma resident key): o autenticador mantém o user handle junto à chave privada, de modo que o usuário pode fazer login sem digitar um nome de usuário, e o navegador pode oferecer as passkeys do site na lista de preenchimento automático do campo de nome de usuário. O servidor TsgcWSAPIServer_WebAuthn suporta passkeys com as configurações e eventos descritos a seguir.
O servidor WebAuthn está incluído no sgcWebSockets Enterprise e no pack sgcAuth. O WebAuthn precisa de um contexto seguro: sirva as páginas por https (ou localhost) e defina WebAuthnOptions.RelyingParty com o nome do host mostrado no navegador.
A propriedade WebAuthnOptions.DefaultOptions.Registration.DiscoverableCredential define o residentKey solicitado nas opções de registro:
| Valor | residentKey | Meaning |
|---|---|---|
waundcPreferred (default) | preferred | O autenticador cria uma credencial detectável (discoverable) quando pode. |
waundcRequired | required | Somente credenciais detectáveis (discoverable) são aceitas. Use para passkeys e login sem nome de usuário. |
waundcDiscouraged | discouraged | O autenticador deve criar uma credencial do lado do servidor, que exige o nome de usuário para fazer login. |
Uma requisição de registro pode substituir o padrão com o campo discoverable_credential ("required", "preferred" ou "discouraged"). A configuração só é aplicada quando a própria requisição não solicita um residentKey específico.
WebAuthn.WebAuthnOptions.RelyingParty := 'www.example.com';
// ... passkeys are discoverable credentials: require a resident key
WebAuthn.WebAuthnOptions.DefaultOptions.Registration.DiscoverableCredential :=
waundcRequired;
WebAuthn.WebAuthnOptions.DefaultOptions.Registration.UserVerification :=
waunuvPreferred;
WebAuthn.WebAuthnOptions.DefaultOptions.Authentication.UserVerification :=
waunuvPreferred;
Um usuário geralmente tem várias passkeys, por exemplo uma no computador e outra no telefone. Registre cada uma delas com o mesmo nome de usuário: quando o nome de usuário já possui credenciais conhecidas pelo servidor, o registro reutiliza o user handle delas (user.id), de modo que todas as passkeys da conta compartilham um único user handle. O servidor conhece as credenciais registradas enquanto está em execução e as adicionadas com AddCredential, então, quando suas credenciais residem em um banco de dados, adicione-as na inicialização para manter um único user handle por conta. Com um nome de usuário, as opções de autenticação contêm todas as passkeys do usuário em allowCredentials.
Solicite as opções de autenticação com um nome de usuário vazio. As opções então não trazem allowCredentials, o navegador mostra as passkeys que tem para o relying party e o usuário escolhe uma. O servidor exige o userHandle da resposta e verifica se ele pertence à credencial usada, e uma credencial desconhecida pela aplicação é rejeitada.
// ... "Sign in with a passkey" button: the browser lists the passkeys of the site
const options = await post("/sgcWebAuthn/Authentication/Options", {username: ""});
const assertion = await SimpleWebAuthnBrowser.startAuthentication(options);
const result = await post("/sgcWebAuthn/Authentication/Verify", assertion);
Com a mediação condicional, as passkeys aparecem na lista de preenchimento automático do campo de nome de usuário, ao lado das senhas salvas. Adicione autocomplete="username webauthn" ao input, carregue /sgcWebAuthn.js (servido pelo componente, endpoint EndpointsOptions.WebAuthn) e chame startAuthentication(options, true) com opções sem nome de usuário quando a página carregar. A promise se resolve quando o usuário escolhe uma passkey.
<script src="/sgcWebAuthn.js"></script>
<input id="username" name="username" autocomplete="username webauthn">
<script>
async function post(url, body) {
const r = await fetch(url, {method: "POST",
headers: {"Content-Type": "application/json"}, body: JSON.stringify(body)});
return r.json();
}
async function passkeyAutofill() {
if (!(await SimpleWebAuthnBrowser.browserSupportsWebAuthnAutofill())) return;
// ... no user name: the options carry no allowCredentials
const options = await post("/sgcWebAuthn/Authentication/Options", {username: ""});
// ... true: conditional mediation, the passkeys appear in the autofill list
const assertion = await SimpleWebAuthnBrowser.startAuthentication(options, true);
const result = await post("/sgcWebAuthn/Authentication/Verify", assertion);
if (result.verified) { location.href = "/"; }
}
passkeyAutofill();
</script>
O preenchimento automático precisa de um navegador que suporte mediação condicional (Chrome, Edge e Safari atuais). browserSupportsWebAuthnAutofill() retorna False nos outros navegadores, mantenha um botão "Entrar com uma passkey" para eles.
O componente não persiste credenciais. Quatro eventos o conectam ao seu armazenamento:
| Evento | O que fazer |
|---|---|
| OnWebAuthnRegistrationSuccessful | Salve aCredentialRecord, por exemplo aCredentialRecord.AsJSON, junto com seu CredentialId e Username. |
| OnWebAuthnAuthenticationOptionsRequest | Com um nome de usuário, adicione as passkeys desse usuário a CredentialRecords. Sem um nome de usuário, não adicione nada. |
| OnWebAuthnAuthenticationGetCredential | Disparado quando a credencial usada pelo navegador não está na lista de credenciais da cerimônia, o que ocorre em um login sem nome de usuário ou com preenchimento automático. Procure o credential id, preencha aCredentialRecord e defina Found como True. |
| OnWebAuthnAuthenticationSuccessful | Salve o SignCount e o BackupState atualizados de aAuthentication.Credential.CredentialRecord e crie a sessão. |
// ... 1. registration: store the new passkey (the whole record as JSON)
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;
// ... 2. sign in with a user name: the passkeys of that user
procedure TForm1.WebAuthnWebAuthnAuthenticationOptionsRequest(Sender: TObject;
const aRequest: TsgcWebAuthn_AuthenticationOptions_Request;
var CredentialRecords: TsgcWebAuthn_CredentialRecords; var Accept: Boolean);
var
i: Integer;
oList: TStringList;
begin
if aRequest.Username = '' then
Exit; // ... usernameless or autofill: no allowCredentials
oList := TStringList.Create;
try
DBGetPasskeysOfUser(aRequest.Username, oList); // ... one JSON per line
for i := 0 to oList.Count - 1 do
CredentialRecords.AddCredentialRecordFromJSON(oList[i]);
Accept := oList.Count > 0;
finally
oList.Free;
end;
end;
// ... 3. usernameless and autofill: find the passkey by its credential id
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;
// ... 4. signed in: save the new signature counter and backup state
procedure TForm1.WebAuthnWebAuthnAuthenticationSuccessful(Sender: TObject;
const aRequest: TsgcWebAuthn_AuthenticationVerify_Request;
const aAuthentication: TsgcWebAuthn_Authentication; var Accept: Boolean);
var
oRecord: TsgcWebAuthn_CredentialRecord;
begin
oRecord := aAuthentication.Credential.CredentialRecord;
DBUpdatePasskey(oRecord.CredentialId, oRecord.AsJSON);
CreateUserSession(oRecord.Username);
end;
O registro retornado por OnWebAuthnAuthenticationGetCredential deve ter o CredentialId solicitado, e o mesmo Username quando a cerimônia foi iniciada com um nome de usuário, caso contrário o login falha. Mantenha o UserId (user handle) no registro armazenado, ele é comparado com o userHandle enviado pelo autenticador. Os eventos são executados nas threads de conexão do servidor, proteja os recursos compartilhados de acordo.
Os flags dos dados do autenticador indicam que tipo de passkey o servidor recebeu. Eles são armazenados no registro da credencial como BackupEligible (flag BE) e BackupState (flag BS):
| BackupEligible | BackupState | Meaning |
|---|---|---|
| True | True | Passkey sincronizada, com backup feito pela plataforma ou pelo gerenciador de senhas e disponível nos outros dispositivos do usuário. |
| True | False | Passkey multidispositivo que ainda não tem backup. |
| False | False | Passkey vinculada ao dispositivo, por exemplo uma chave de segurança ou uma chave TPM que nunca sai do dispositivo. Recomende ao usuário registrar uma segunda passkey. |
function GetPasskeyKind(aRecord: TsgcWebAuthn_CredentialRecord): string;
begin
if aRecord.BackupEligible and aRecord.BackupState then
Result := 'synced passkey'
else if aRecord.BackupEligible then
Result := 'multi-device passkey, not backed up yet'
else
Result := 'device-bound passkey';
end;
A cada login, o servidor rejeita uma resposta com o flag BS definido e o flag BE não definido, e uma resposta cujo flag BE difere do BackupEligible armazenado, porque a elegibilidade de uma credencial nunca muda. O novo flag BS é copiado para BackupState, salve-o em OnWebAuthnAuthenticationSuccessful.
Alguns autenticadores incrementam um contador de assinatura a cada login. Quando o contador da resposta ou o SignCount armazenado não é zero, o contador da resposta deve ser maior que o valor armazenado, caso contrário o login é rejeitado porque o autenticador pode ter sido clonado. O valor armazenado só avança, e deve ser salvo após cada login para que a detecção funcione. Passkeys sincronizadas geralmente sempre reportam 0, o que desativa a verificação para elas. Defina WebAuthnOptions.AllowSignCountLessOrEqualStoredValue como True para aceitar um contador que não aumenta (o valor armazenado continua nunca sendo reduzido).
O demo Demos\26.Authentication\01.Passkeys registra várias passkeys por usuário, faz login sem nome de usuário e com preenchimento automático, mostra se cada passkey é sincronizada ou vinculada ao dispositivo, e armazena as credenciais em um arquivo JSON da aplicação através dos eventos acima. Veja também WebAuthn e o cliente JavaScript.