Passkeys en Delphi : connexion sans mot de passe avec WebAuthn

· Composants
Passkeys en Delphi : connexion sans mot de passe avec WebAuthn

Tout mot de passe accepté par votre application peut être deviné, réutilisé sur un autre site, saisi sur une fausse page de connexion, ou fuiter depuis une sauvegarde de base de données. Les passkeys suppriment ces quatre problèmes d'un coup. L'utilisateur se connecte avec l'empreinte, le visage ou le code PIN qui déverrouille déjà le téléphone ou l'ordinateur portable, et il n'y a plus, sur votre serveur, de secret qui vaille la peine d'être volé.

Dans l'aperçu des nouveaux composants de connexion, les passkeys n'avaient droit qu'à cinq points, et l'article sur SAML couvrait la connexion pour les entreprises qui gèrent leur propre fournisseur d'identité. Cet article couvre le volet sans mot de passe : comment TsgcWSAPIServer_WebAuthn enregistre les passkeys, connecte les utilisateurs sans nom d'utilisateur, affiche les passkeys dans la liste d'autofill du navigateur et conserve chaque credential dans votre propre base de données.

Ce qu'est une passkey

Une passkey est une paire de clés créée par l'authenticator de l'utilisateur : Windows Hello, le trousseau iCloud, le gestionnaire de mots de passe Google, un gestionnaire de mots de passe ou une clé de sécurité FIDO2. La clé privée ne quitte jamais l'authenticator. Votre serveur ne stocke que la clé publique, si bien qu'une table de passkeys qui fuite ne permet à personne de se connecter. Chaque signature est liée au domaine de votre site, si bien qu'un domaine de phishing qui y ressemble n'obtient rien d'exploitable. C'est ce que signifie ici résistant au phishing.

En termes WebAuthn, une passkey est un credential détectable, aussi appelé resident key. L'authenticator conserve le userHandle à côté de la clé privée, ce qui explique pourquoi l'utilisateur peut se connecter sans taper de nom d'utilisateur et pourquoi le navigateur peut lister seul les passkeys de votre site.

Demander des credentials détectables

WebAuthn exige un contexte sécurisé : servez vos pages en https, ou depuis localhost pendant le développement, et réglez WebAuthnOptions.RelyingParty sur le nom d'hôte affiché par le navigateur. Le residentKey demandé par les options d'enregistrement provient de WebAuthnOptions.DefaultOptions.Registration.DiscoverableCredential :

Une requête d'enregistrement isolée peut toujours écraser la valeur par défaut avec le champ discoverable_credential, réglé sur required, preferred ou discouraged.

Se connecter sans nom d'utilisateur

Demandez les options d'authentification avec un nom d'utilisateur vide. Les options ne portent alors aucune liste allowCredentials, le navigateur affiche les passkeys qu'il détient pour votre relying party, et l'utilisateur en choisit une. Il n'y a rien à taper, donc rien à mal taper.

Comme le serveur n'a pas choisi le credential cette fois, il vérifie davantage de choses. La réponse doit inclure le userHandle, le userHandle doit appartenir au credential qui a signé, et un credential que votre application ne connaît pas est rejeté.

Les passkeys dans la liste d'autofill

L'autofill, ou conditional mediation, place les passkeys de votre site dans la liste de suggestions du champ nom d'utilisateur, à côté des mots de passe enregistrés. C'est la manière en douceur de faire migrer les utilisateurs : la page de connexion continue de fonctionner pour ceux qui ont un mot de passe, et ceux qui ont une passkey la choisissent dans la liste.

L'autofill nécessite un navigateur avec conditional mediation, ce qui aujourd'hui signifie les versions actuelles de Chrome, Edge et Safari. browserSupportsWebAuthnAutofill() renvoie False dans les autres, gardez donc aussi un bouton « Se connecter avec une passkey » sur la page.

Plusieurs passkeys, un seul userHandle

Les vrais utilisateurs ont plus d'une passkey : une sur l'ordinateur portable, une sur le téléphone, peut-être une clé de sécurité pour le jour où les deux sont perdues. Enregistrez chacune avec le même nom d'utilisateur. Quand ce nom d'utilisateur possède déjà des credentials connus du serveur, le nouvel enregistrement réutilise leur userHandle (user.id), si bien que chaque passkey du compte partage un seul userHandle et mène toutes au même utilisateur.

Le serveur connaît les credentials enregistrés pendant son exécution et ceux que vous ajoutez avec AddCredential. Quand vos passkeys vivent dans une base de données, ajoutez-les au démarrage pour que chaque compte conserve un seul userHandle. Quand un utilisateur tape bien un nom d'utilisateur, les options d'authentification listent chaque passkey de cet utilisateur dans allowCredentials, et l'authenticator utilise celle qu'il détient.

Conservez les passkeys dans votre propre base de données

Le composant ne persiste pas les credentials. Vos passkeys se rangent à côté de votre table d'utilisateurs, et quatre événements relient les deux :

Enregistrer une passkey et la recharger tiennent en deux handlers courts :

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 et DBFindPasskey représentent votre propre code d'accès aux données. L'enregistrement que vous retournez doit porter le CredentialId demandé, et le même Username quand la ceremony a débuté avec un nom d'utilisateur, sinon la connexion échoue. Conservez aussi le UserId dans l'enregistrement stocké, car le serveur le compare au userHandle envoyé par l'authenticator. Les événements s'exécutent dans les threads de connexion du serveur, protégez donc les ressources partagées comme vous les protégez partout ailleurs dans le serveur.

Synchronisées ou liées à l'appareil

Les indicateurs présents dans les données de l'authenticator indiquent quel type de passkey vous avez reçu, et le credential record les conserve sous BackupEligible (l'indicateur BE) et BackupState (l'indicateur BS) :

Votre politique peut les traiter différemment, par exemple en n'acceptant que les clés de sécurité liées à l'appareil sur les comptes administrateur. À chaque connexion, le serveur rejette une réponse dont l'indicateur BS est défini et l'indicateur BE ne l'est pas, ainsi qu'une réponse dont l'indicateur BE diffère du BackupEligible stocké, car l'eligibility d'un credential ne change jamais. Le nouvel indicateur BS est copié dans BackupState, et c'est dans OnWebAuthnAuthenticationSuccessful qu'il faut l'enregistrer.

Le compteur de signatures et les authenticators clonés

Certains authenticators incrémentent un compteur de signatures à chaque connexion. Quand le compteur de la réponse ou le SignCount stocké n'est pas zéro, le compteur de la réponse doit être supérieur à la valeur stockée. Sinon, la connexion est rejetée, car deux authenticators répondant avec la même clé, c'est exactement à quoi ressemble un authenticator cloné.

Cette vérification ne fonctionne que si vous enregistrez le compteur après chaque connexion, et que la valeur stockée n'avance jamais que vers l'avant. Les passkeys synchronisées signalent généralement 0 à chaque fois, ce qui désactive la vérification pour elles. Si vous avez des authenticators qui n'incrémentent pas le compteur de façon fiable, réglez WebAuthnOptions.AllowSignCountLessOrEqualStoredValue sur True pour les accepter. La valeur stockée n'est quand même jamais abaissée.

Essayez la démo

La démo Demos\26.Authentication\01.Passkeys est un relying party complet : un TsgcWebSocketHTTPServer avec un TsgcWSAPIServer_WebAuthn attaché, servant une petite page de connexion sur https://localhost:5443. Elle stocke chaque passkey dans son propre fichier passkeys.json, via les quatre événements ci-dessus.

  1. Compilez la démo et gardez libcrypto-3.dll et libssl-3.dll à côté de l'exécutable. Ils se trouvent dans le dossier de la démo.
  2. Laissez l'hôte 127.0.0.1, le port 5443 et le relying party localhost, puis cliquez sur Start.
  3. Cliquez sur Open Browser et acceptez le certificat de test autosigné.
  4. Tapez un nom d'utilisateur et cliquez sur Register passkey. Enregistrez une seconde passkey pour le même nom d'utilisateur. Le formulaire liste chaque passkey avec son type, synchronisée ou liée à l'appareil, et son compteur de signatures.
  5. Cliquez sur Sign in without user name et choisissez une passkey. Le serveur la retrouve via OnWebAuthnAuthenticationGetCredential et le journal affiche l'utilisateur.
  6. Rechargez la page et cliquez dans le champ nom d'utilisateur. Les passkeys apparaissent dans la liste d'autofill. Choisissez-en une pour vous connecter.
  7. Redémarrez l'application. Les passkeys se rechargent depuis passkeys.json, ce qui montre que le stockage appartient à votre application et non au composant.

Documentation

Où l'obtenir

TsgcWSAPIServer_WebAuthn est inclus dans les éditions Enterprise et All-Access de sgcWebSockets, pour Delphi et C++ Builder, et le même composant fait partie de sgcWebSockets .NET. Si vous n'avez besoin que de l'authentification, le pack sgcAuth l'inclut avec les autres composants de connexion. Vous le trouvez sur la palette SGC Auth, et rien ne change dans une application existante tant que vous ne le déposez pas sur un formulaire.

À lire aussi

Voir la vidéo

Il existe une courte vidéo, « Passkeys in Delphi: passwordless login with WebAuthn », sur la chaîne eSeGeCe. Elle montre le code dans l'IDE, ce que révèlent les indicateurs de sauvegarde et le compteur de signatures, ainsi que la démo tournant sur https://localhost : un utilisateur enregistre deux passkeys, puis se connecte sans taper de nom d'utilisateur.

Des questions, un retour, ou besoin d'aide pour ajouter des passkeys à votre page de connexion ? Contactez-nous. Vous recevrez une réponse des personnes qui ont écrit le code.