Website mit der Bestätigung in zwei Schritten und einem Sicherheitsschlüssel schützen (WebAuthn)

1. Aufgaben

Sie beginnen mit einer grundlegenden Webanwendung, die eine passwortbasierte Anmeldung unterstützt.

Auf dieser Grundlage wird dann die Bestätigung in zwei Schritten über einen Sicherheitsschlüssel unterstützt. Gehen Sie dabei so vor:

  • Die Möglichkeit, einen WebAuthn-Zugang zu registrieren.
  • Die Bestätigung in zwei Schritten, bei der der Nutzer nach seinem zweiten Faktor gefragt wird – den WebAuthn-Anmeldedaten –, wenn er einen registriert hat.
  • Eine Oberfläche für die Anmeldedatenverwaltung: eine Liste mit Anmeldedaten, die Nutzern das Umbenennen und Löschen von Anmeldedaten ermöglicht.

16ce77744061c5f7

Sehen Sie sich die fertige Web-App an und probieren Sie sie aus.

2. Über WebAuthn

WebAuthn-Grundlagen

Warum WebAuthn?

Phishing ist ein großes Sicherheitsproblem im Web: Die meisten Datenpannen, bei denen Passwörter preisgegeben werden, sind schwache oder gestohlene Passwörter, die auf anderen Websites wiederverwendet werden. Die branchenweite kollektive Antwort auf dieses Problem war die Multi-Faktor-Authentifizierung. Implementierungen sind jedoch fragmentiert und viele von ihnen werden noch nicht angemessen an Phishing herangeführt.

Die Web Authentication API oder WebAuthn ist ein standardisiertes Phishing-resistentes Protokoll, das von jeder Webanwendung verwendet werden kann.

Funktionsweise

Quelle: webauthn.guide

Mit WebAuthn können Server Nutzer mithilfe der Kryptografie des öffentlichen Schlüssels anstelle eines Passworts registrieren und authentifizieren. Websites können ein Anmeldedaten erstellen, das aus einem Private-Public-Schlüsselpaar besteht.

  • Der private Schlüssel wird sicher auf dem Gerät des Nutzers gespeichert.
  • Der öffentliche Schlüssel und die zufällig generierte ID werden zum Speichern an den Server gesendet.

Der öffentliche Schlüssel wird vom Server verwendet, um die Identität des Nutzers nachzuweisen. Es ist nicht geheim, weil es ohne den entsprechenden privaten Schlüssel nutzlos ist.

Vorteile

WebAuthn hat zwei Hauptvorteile:

  • Kein gemeinsames Secret: Der Server speichert kein Secret. Dadurch sind Datenbanken für Hacker weniger attraktiv, da die öffentlichen Schlüssel für sie nicht nützlich sind.
  • Bereichsbezogene Anmeldedaten: Für site.example registrierte Anmeldedaten können nicht in evil-site.example verwendet werden. Damit ist WebAuthn Phishing-sicher.

Anwendungsfälle

Ein Anwendungsfall für WebAuthn ist die Bestätigung in zwei Schritten mit einem Sicherheitsschlüssel. Dies ist insbesondere für Webanwendungen des Unternehmens relevant.

Unterstützte Browser

Sie wurden von W3C und FIDO in Zusammenarbeit mit Google, Mozilla, Microsoft, Yubico und anderen Unternehmen geschrieben.

Glossar

  • Authenticator: eine Software- oder Hardwareentität, die einen Nutzer registrieren und später im Besitz der registrierten Anmeldedaten behalten kann. Es gibt zwei Arten von Authenticators:
  • Roaming-Authentifizierung für Nutzer, die sich mit jedem Gerät anmelden können, über das sie sich anmelden möchten. Beispiel: USB-Sicherheitsschlüssel, Smartphone.
  • Plattform-Authentifizierung: Eine Authentifizierungsmethode, die in das Gerät eines Nutzers integriert ist. Beispiel: Touch ID von Apple.
  • Anmeldedaten: Schlüsselpaar aus privatem und öffentlichem Schlüssel
  • Vertrauensstellung der (Server) der Website, die versucht, den Nutzer zu authentifizieren
  • FIDO-Server: der Server, der für die Authentifizierung verwendet wird. FIDO ist eine Familie von Protokollen, die von der FIDO-Allianz entwickelt wurde. Eine dieser Protokolle ist WebAuthn.

In diesem Workshop nutzen wir einen Roaming-Authenticator.

3. Hinweis

Voraussetzungen

Für dieses Codelab benötigen Sie Folgendes:

  • Grundlegendes Verständnis von WebAuthn
  • Grundkenntnisse in JavaScript und HTML
  • Ein aktueller Browser, der WebAuthn unterstützt.
  • Einen Sicherheitsschlüssel, der U2F-konform ist

Sie können einen der folgenden als Sicherheitsschlüssel verwenden:

  • Ein Android-Smartphone mit Android>=7 (Nougat), auf dem Chrome ausgeführt wird. In diesem Fall benötigen Sie außerdem ein Windows-, macOS- oder Chrome OS-Gerät mit funktionierendem Bluetooth.
  • Ein USB-Schlüssel, z. B. ein YubiKey

6539dc7ffec2538c.png

Quelle: https://www.yubico.com/products/security-key/

dd56e2cfe0f7ced2.png

Lerninhalte

Lernen ✅

  • So registrieren und verwenden Sie einen Sicherheitsschlüssel als zweiten Faktor für die WebAuthn-Authentifizierung.
  • Wie du diesen Vorgang nutzerfreundlicher gestalten kannst

Sie werden nicht mehr lernen TrueView

  • Erstellen eines FIDO-Servers, des Servers, der für die Authentifizierung verwendet wird. Das ist in Ordnung, weil Sie als Webanwendungs- oder Website-Entwickler in der Regel auf vorhandene FIDO-Serverimplementierungen angewiesen sind. Sie sollten immer die Funktionalität und Qualität der Serverimplementierungen prüfen, auf die Sie sich verlassen. In diesem Codelab verwendet der FIDO-Server SimpleWebAuthn. Weitere Optionen finden Sie auf der offiziellen Seite der FIDO Alliance. Informationen zu Open-Source-Bibliotheken finden Sie unter webauthn.io oder AwesomeWebAuthn.

Haftungsausschluss

Der Nutzer muss ein Passwort eingeben, um sich anzumelden. Der Einfachheit halber wird das Passwort in diesem Codelab nicht gespeichert oder geprüft. In einer echten Anwendung würden Sie prüfen, ob sie serverseitig richtig ist.

In diesem Codelab werden grundlegende Sicherheitsprüfungen wie CSRF-Prüfungen, Sitzungsvalidierung und Eingabebereinigung durchgeführt. Viele Sicherheitsmaßnahmen sind jedoch nicht vorhanden, z. B. gibt es keine Eingabebeschränkung für Passwörter, um Brute-Force-Angriffe zu verhindern. Hier spielt es keine Rolle, weil Passwörter nicht gespeichert werden. Verwenden Sie diesen Code jedoch nicht unverändert.

4. Authentifizierungs-App einrichten

Wenn Sie ein Android-Smartphone als Authentifizierungsbestätigung verwenden

  • Prüfen Sie, ob Chrome sowohl auf Ihrem Computer als auch auf Ihrem Smartphone auf dem neuesten Stand ist.
  • Öffnen Sie Chrome auf Ihrem Computer und auf Ihrem Smartphone und melden Sie sich mit dem Profil an, das Sie für diesen Workshop verwenden möchten.
  • Aktivieren Sie die Synchronisierung für dieses Profil auf Ihrem Computer und Smartphone. Verwenden Sie dazu chrome://settings/syncSetup.
  • Aktivieren Sie Bluetooth sowohl auf Ihrem Computer als auch auf Ihrem Smartphone.
  • Öffnen Sie auf einem Chrome-Computer mit demselben Profil webauthn.io.
  • Geben Sie einen einfachen Nutzernamen ein. Behalten Sie für Attestierungstyp und Authenticator-Typ die Werte Keine und Ohne Angabe (Standardeinstellung) bei. Klicken Sie auf Registrieren.

6b49ff0298f5a0af

  • Daraufhin wird ein Browserfenster geöffnet, in dem Sie aufgefordert werden, Ihre Identität zu bestätigen. Wählen Sie Ihr Smartphone in der Liste aus.

ffebe58ac826eaf2 852de328fcd4eb42.png

  • Sie erhalten eine Benachrichtigung mit dem Titel Identität bestätigen. Tippen Sie dann darauf.
  • Auf deinem Smartphone wirst du aufgefordert, deinen PIN-Code einzugeben oder den Fingerabdrucksensor zu berühren. Geben Sie den Namen ein.
  • Auf Ihrem Computer (webauthn.io) sollte der Indikator „Erfolg“ angezeigt werden.

fc0acf00a4d412fa.png

  • Klicken Sie auf Ihrem Computer auf webauthn.io auf die Anmeldeschaltfläche.
  • Auch hier sollte wieder ein Browserfenster geöffnet werden. Wählen Sie Ihr Smartphone in der Liste aus.
  • Tippe auf deinem Smartphone auf die Benachrichtigung, die erscheint, und gib deine PIN ein oder tippe auf den Fingerabdrucksensor.
  • webauthn.io sollte Ihnen mitteilen, dass Sie angemeldet sind. Dein Smartphone funktioniert ordnungsgemäß als Sicherheitsschlüssel; du bist jetzt für den Workshop bereit.

Verwendung eines USB-Sicherheitsschlüssels als Authentifizierungs-App

  • Öffnen Sie webauthn.io in Chrome auf dem Computer.
  • Geben Sie einen einfachen Nutzernamen ein. Behalten Sie für Attestierungstyp und Authenticator-Typ die Werte Keine und Ohne Angabe (Standardeinstellung) bei. Klicken Sie auf Registrieren.
  • Daraufhin wird ein Browserfenster geöffnet, in dem Sie aufgefordert werden, Ihre Identität zu bestätigen. Wählen Sie in der Liste USB-Sicherheitsschlüssel aus.

ffebe58ac826eaf2 9fe75f04e43da035.png

  • Stecken Sie den Sicherheitsschlüssel in den Desktop und tippen Sie darauf.

923d5adb8aa8286c

  • Auf Ihrem Computer (webauthn.io) sollte der Indikator „Erfolg“ angezeigt werden.

fc0acf00a4d412fa.png

  • Klicken Sie auf Ihrem Computer auf webauthn.io auf die Schaltfläche Login (Anmelden).
  • Auch hier sollte ein Browserfenster geöffnet werden. Wählen Sie in der Liste USB-Sicherheitsschlüssel aus.
  • Tippen Sie auf die Taste.
  • Webauthn.io sollte Ihnen mitteilen, dass Sie angemeldet sind. Dein USB-Sicherheitsschlüssel funktioniert ordnungsgemäß. Für den Workshop ist die Einrichtung abgeschlossen.

7e1c0bb19c9f3043

5. Einrichten

In diesem Codelab nutzen Sie Glitch, einen Online-Code-Editor, der Code automatisch und sofort bereitstellt.

Startcode Forken

Öffnen Sie das Startprojekt.

Klicken Sie auf die Schaltfläche Remix-Funktion.

Dadurch wird eine Kopie des Startcodes erstellt. Sie haben jetzt Ihren eigenen Code. In deiner Gabel (in Glitch „Remix“ genannt) kannst du dieses Codelab komplett erledigen.

konf2b9f552c9809b6.png

Startcode ausprobieren

Sehen Sie sich den Startcode an, den Sie bereits für einen kurzen Fork erstellt haben.

Unter libs ist bereits eine Bibliothek mit dem Namen auth.js verfügbar. Sie ist eine benutzerdefinierte Bibliothek, die sich um die serverseitige Authentifizierungslogik kümmert. Sie verwendet die Bibliothek fido als Abhängigkeit.

6. Registrierung von Anmeldedaten implementieren

Registrierung von Anmeldedaten implementieren

Das erste, was wir brauchen, um die Bestätigung in zwei Schritten mit einem Sicherheitsschlüssel einzurichten, besteht darin, dem Nutzer das Erstellen von Anmeldedaten zu ermöglichen.

Zuerst fügen wir eine Funktion hinzu, die dies in unserem clientseitigen Code tut.

In public/auth.client.js gibt es die Funktion registerCredential(), die noch nichts tut. Fügen Sie folgenden Code hinzu:

async function registerCredential() {
  // Fetch the credential creation options from the backend
  const credentialCreationOptionsFromServer = await _fetch(
    "/auth/credential-options",
    "POST"
  );
  // Decode the credential creation options
  const credentialCreationOptions = decodeServerOptions(
    credentialCreationOptionsFromServer
  );
  // Create a credential via the browser API; this will prompt the user to touch their security key or tap a button on their phone
  const credential = await navigator.credentials.create({
    publicKey: {
      ...credentialCreationOptions,
    }
  });
  // Encode the newly created credential to send it to the backend
  const encodedCredential = encodeCredential(credential);
  // Send the encoded credential to the backend for storage
  return await _fetch("/auth/credential", "POST", encodedCredential);
}

Diese Funktion wurde bereits für Sie exportiert.

Hier die Vorteile von registerCredential:

  • Die Optionen zum Erstellen von Anmeldedaten werden vom Server abgerufen (/auth/credential-options).
  • Da die Serveroptionen wieder codiert sind, wird sie mit der Dienstprogrammfunktion decodeServerOptions decodiert.
  • Die Anmeldedaten werden durch Aufrufen der Web API navigator.credential.create erstellt. Wenn navigator.credential.create aufgerufen wird, wird der Nutzer durch den Browser zur Auswahl eines Sicherheitsschlüssels aufgefordert.
  • Die neu erstellten Anmeldedaten werden decodiert
  • Die neuen Anmeldedaten werden serverseitig registriert, indem eine Anfrage an /auth/credential gesendet wird, die die codierten Anmeldedaten enthält.

Nebenbei: Sehen Sie sich den Servercode an.

registerCredential() sendet zwei Aufrufe an den Server. Sehen wir uns also an, was im Back-End geschieht.

Optionen zum Erstellen von Anmeldedaten

Wenn der Client eine Anfrage an /auth/credential-options sendet, generiert der Server ein Optionsobjekt und sendet es an den Client zurück.

Dieses Objekt wird dann vom Client im eigentlichen Aufruf der Anmeldedaten verwendet:

navigator.credentials.create({
    publicKey: {
    // Options generated server-side
    ...credentialCreationOptions
// ...
}

Also, was wird in diesem credentialCreationOptions verwendet und im letzten Schritt aus dem clientseitigen registerCredential verwendet?

Sehen Sie sich den Servercode unter router.post("/credential-options", ... an.

Sehen wir uns jetzt nicht jede einzelne Property an. Hier sind noch ein paar interessante Beispiele, die im Servercode für das Optionsobjekt aufgeführt sind. Sie wurden mit der fido2-Bibliothek generiert und letztendlich an den Client zurückgegeben:

  • rpName und rpId beschreiben die Organisation, die den Nutzer registriert und authentifiziert. Beachten Sie, dass sich Anmeldedaten in WebAuthn auf eine bestimmte Domain beziehen. Das ist ein Sicherheitsvorteil. Hier werden die Anmeldedaten für rpName und rpId verwendet. Eine gültige rpId ist beispielsweise der Hostname Ihrer Website. Beachten Sie, dass diese automatisch aktualisiert werden, wenn Sie das Startprojekt unterteilen. 🧘🏻 ♀️
  • excludeCredentials ist eine Liste mit Anmeldedaten. Die neuen Anmeldedaten können nicht für einen Authentifizierungspartner erstellt werden, der auch eines der in excludeCredentials aufgeführten Anmeldedaten enthält. In unserem Codelab ist excludeCredentials eine Liste der vorhandenen Anmeldedaten für diesen Nutzer. Mit diesem und user.id sorgen wir dafür, dass jeder vom Nutzer erstellte Anmeldedaten mit einem anderen Authentifizierungs- (Sicherheitsschlüssel) gespeichert wird. Das bedeutet, dass ein Nutzer, der mehrere Anmeldedaten registriert hat, verschiedene Authentifizierungsschlüssel verwendet (Sicherheitsschlüssel). Wenn Sie einen Sicherheitsschlüssel verlieren, wird der Nutzer nicht aus seinem Konto ausgesperrt.
  • authenticatorSelection definiert den Authentifizierungstyp, den Sie in Ihrer Webanwendung zulassen möchten. Werfen wir einen näheren Blick auf authenticatorSelection:
    • residentKey: preferred bedeutet, dass diese Anwendung keine clientseitigen Anmeldedaten erzwingt. Clientseitige Anmeldedaten sind eine spezielle Art von Anmeldedaten, mit denen Nutzer authentifiziert werden können, ohne sie zuerst identifizieren zu müssen. Hier haben wir preferred eingerichtet, da sich dieses Codelab auf die grundlegende Implementierung konzentriert. Sichtbare Anmeldedaten sind für komplexere Abläufe gedacht.
    • requireResidentKey ist nur für die Abwärtskompatibilität mit WebAuthn v1 vorhanden.
    • userVerification: preferred bedeutet, dass die vertrauende Seite beim Erstellen der Anmeldedaten fordert, wenn die Authentifizierung von Nutzern unterstützt wird, z. B. ein biometrischer Sicherheitsschlüssel oder ein Schlüssel mit integrierter PIN-Funktion. Wenn die Authentifizierung über den einfachen Sicherheitsschlüssel erfolgt, fordert der Server keine Nutzerbestätigung an.
  • ​​pubKeyCredParam beschreibt die bevorzugten kryptografischen Eigenschaften der Anmeldedaten in der Reihenfolge ihrer Wahl.

Alle diese Optionen sind Entscheidungen, die die Webanwendung für ihr Sicherheitsmodell treffen muss. Beachte, dass diese Optionen auf dem Server in einem einzigen authSettings-Objekt definiert werden.

Challenge

Hier ist noch etwas Interessantes: req.session.challenge = options.challenge;.

Da WebAuthn ein kryptografisches Protokoll ist, werden zufällige Angriffe nur verhindert, wenn ein Angreifer eine Nutzlast für die erneute Authentifizierung stiehlt und der Inhaber des privaten Schlüssels, der die Authentifizierung aktivieren soll, nicht mehr der Fall ist.

Um dies zu vermeiden, wird auf dem Server ein Wettkampf erstellt, der spontan signiert wird. Die Signatur wird dann mit den erwarteten Werten verglichen. Dies prüft, ob der Nutzer den privaten Schlüssel zum Zeitpunkt der Generierung des Anmeldedatens behält.

Anmeldecode für Anmeldedaten

Sehen Sie sich den Servercode unter router.post("/credentials", ... an.

Dort werden die Anmeldedaten serverseitig registriert.

Woran liegt das?

Einer der wichtigsten Punkte in diesem Code ist der Bestätigungsaufruf über fido2.verifyAttestationResponse:

  • Die signierte Identitätsbestätigung wird geprüft. So wird sichergestellt, dass die Anmeldedaten von einer Person erstellt wurden, die den privaten Schlüssel bei der Erstellung beibehalten hat.
  • Die ID der vertrauenden Seite, die an ihren Ursprung gebunden ist, wird ebenfalls überprüft. Dadurch werden die Anmeldedaten an diese Webanwendung (und nur an diese Webanwendung) gebunden.

Diese Funktion zur UI hinzufügen

Die Funktion „registerCredential(),“ ist jetzt verfügbar. Jetzt können sie dem Nutzer zur Verfügung gestellt werden.

Klicken Sie dazu auf der Seite Konto auf diesen Link, weil dies ein üblicher Ort für die Authentifizierungsverwaltung ist.

Im account.html-Markup gibt es unter dem Nutzernamen eine bisher leere div mit der Layoutklasse class="flex-h-between". Wir verwenden diese div für UI-Elemente, die einen Bezug zur 2FA-Funktion haben.

Fügen Sie dieses div-Element hinzu:

  • Ein Titel mit der Bestätigung in zwei Schritten
  • Eine Schaltfläche zum Erstellen von Anmeldedaten
 <div class="flex-h-between">
    <h3>
        Two-factor authentication
    </h3>
    <button class="create" id="registerButton" raised>
        ➕ Add a credential
    </button>
</div>

Fügen Sie unterhalb dieses div-Elements das div-Element der Anmeldedaten hinzu, das wir später benötigen:

<div class="flex-h-between">
(HTML you've just added)
</div>
<div id="credentials"></div>

Importieren Sie in account.html-Inline-Skript die soeben erstellte Funktion register und fügen Sie eine Funktion hinzu, die sie aufruft, sowie einen Ereignis-Handler, der an die soeben erstellte Schaltfläche angehängt ist.

// Set up the handler for the button that registers credentials
const registerButton = document.querySelector('#registerButton');
registerButton.addEventListener('click', register);

// Register a credential
async function register() {
  let user = {};
  try {
    const user = await registerCredential();
  } catch (e) {
    // Alert the user that something went wrong
    if (Array.isArray(e)) {
      alert(
        // `msg` not `message`, this is the key's name as per the express validator API
        `Registration failed. ${e.map((err) => `${err.msg} (${err.param})`)}`
      );
    } else {
      alert(`Registration failed. ${e}`);
    }
  }
}

Anmeldedaten für den Nutzer aufrufen

Nachdem Sie die Funktion zum Erstellen von Anmeldedaten hinzugefügt haben, müssen Nutzer nun die Möglichkeit haben, die Anmeldedaten zu sehen, die sie hinzugefügt haben.

Hierfür eignet sich die Seite Konto.

Suchen Sie in account.html die Funktion updateCredentialList().

Fügen Sie den folgenden Code hinzu, der einen Back-End-Aufruf durchführt, um alle registrierten Anmeldedaten für den aktuell angemeldeten Nutzer abzurufen, und damit die zurückgegebenen Anmeldedaten anzeigt:

// Update the list that displays credentials
async function updateCredentialList() {
  // Fetch the latest credential list from the backend
  const response = await _fetch('/auth/credentials', 'GET');
  const credentials = response.credentials || [];
  // Generate the credential list as HTML and pass remove/rename functions as args
  const credentialListHtml = getCredentialListHtml(
    credentials,
    removeEl,
    renameEl
  );
  // Display the list of credentials in the DOM
  const list = document.querySelector('#credentials');
  render(credentialListHtml, list);
}    

Bis dahin kannst du removeEl und renameEl aber in diesem Codelab noch näher kennenlernen.

Fügen Sie updateCredentialList am Anfang des Inline-Skripts innerhalb von account.html einen Aufruf hinzu. Bei diesem Aufruf werden die verfügbaren Anmeldedaten abgerufen, wenn der Nutzer zu seiner Kontoseite gelangt.

<script type="module">
    // ... (imports)
    // Initialize the credential list by updating it once on page load
    updateCredentialList();

Rufen Sie jetzt updateCredentialList auf, sobald registerCredential erfolgreich abgeschlossen wurde. Jetzt werden in den Listen die neu erstellten Anmeldedaten angezeigt:

async function register() {
  let user = {};
  try {
    // ...
  } catch (e) {
    // ...
  }
  // Refresh the credential list to display the new credential
  await updateCredentialList();
}

Selbst ausprobieren 💻🏻 💻

Sie haben die Registrierung für die Anmeldedaten abgeschlossen. Nutzer können jetzt Anmeldedaten für den Sicherheitsschlüssel erstellen und auf der Seite Konto visualisieren.

Führen Sie einen Test durch:

  • Abmelden.
  • Melden Sie sich mit einem beliebigen Nutzer und Passwort an. Wie bereits erwähnt, wird das Passwort in diesem Codelab nicht auf Korrektheit geprüft. Geben Sie ein beliebiges Passwort ein.
  • Klicken Sie auf der Seite Konto auf Anmeldedaten hinzufügen.
  • Sie werden aufgefordert, einen Sicherheitsschlüssel einzustecken und zu berühren. Tun Sie es.
  • Nachdem die Anmeldedaten erstellt wurden, sollten sie auf der Kontoseite angezeigt werden.
  • Aktualisieren Sie die Seite Konto. Die Anmeldedaten werden angezeigt.
  • Falls zwei Schlüssel verfügbar sind, fügen Sie zwei verschiedene Sicherheitsschlüssel als Anmeldedaten hinzu. Sie sollten beide angezeigt werden.
  • Erstellen Sie zwei Anmeldedaten mit demselben Authentifizierungsschlüssel (Schlüssel). Sie sehen, dass diese nicht unterstützt werden. Das ist beabsichtigt – das liegt an der Verwendung von excludeCredentials im Back-End.

7. Bestätigung in zwei Schritten aktivieren

Ihre Nutzer können sich anmelden und Registrierungen aufheben, aber die Anmeldedaten werden nur angezeigt und noch nicht verwendet.

Jetzt ist es an der Zeit, sie einzusetzen und die eigentliche Bestätigung in zwei Schritten einzurichten.

In diesem Abschnitt ändern Sie den Authentifizierungsvorgang in Ihrer Webanwendung von diesem grundlegenden Ablauf:

6ff49a7e520836d0.png

Gehen Sie in diesem Fall so vor:

e7409946cd88efc7.png

Bestätigung in zwei Schritten implementieren

Zuerst fügen wir die erforderlichen Funktionen hinzu und implementieren die Kommunikation mit dem Back-End. Dies geschieht im nächsten Schritt im Front-End.

Hier müssen Sie eine Funktion einrichten, mit der Nutzer mit Anmeldedaten authentifiziert werden.

Suchen Sie in public/auth.client.js nach der leeren Funktion authenticateTwoFactor und fügen Sie ihr den folgenden Code hinzu:

async function authenticateTwoFactor() {
  // Fetch the 2F options from the backend
  const optionsFromServer = await _fetch("/auth/two-factor-options", "POST");
  // Decode them
  const decodedOptions = decodeServerOptions(optionsFromServer);
  // Get a credential via the browser API; this will prompt the user to touch their security key or tap a button on their phone
  const credential = await navigator.credentials.get({
    publicKey: decodedOptions
  });
  // Encode the credential
  const encodedCredential = encodeCredential(credential);
  // Send it to the backend for verification
  return await _fetch("/auth/authenticate-two-factor", "POST", {
    credential: encodedCredential
  });
}

Hinweis: Diese Funktion wurde bereits für dich exportiert. Wir benötigen sie im nächsten Schritt.

Hier die Vorteile von authenticateTwoFactor:

  • Es werden Bestätigungsoptionen in zwei Schritten vom Server angefordert. Genau wie bei den zuvor erstellten Optionen zur Anmeldedatenerstellung sind diese auf dem Server definiert und hängen vom Sicherheitsmodell der Webanwendung ab. Weitere Informationen finden Sie im Servercode unter router.post("/two-factors-options", ....
  • Wenn navigator.credentials.get aufgerufen wird, übernimmt der Browser die Aufforderung und der Nutzer wird aufgefordert, einen zuvor registrierten Schlüssel einzufügen und zu berühren. In diesem Fall werden die Anmeldedaten für diesen spezifischen Authentifizierungsvorgang für die Bestätigung in zwei Schritten ausgewählt.
  • Die ausgewählten Anmeldedaten werden dann in einer Back-End-Anfrage übergeben, damit diese abgerufen werden können.

Nebenbei: Sehen Sie sich den Servercode an.

server.js übernimmt bereits die Navigation und den Zugriff: Dadurch wird sichergestellt, dass nur authentifizierte Nutzer auf die Seite Konto zugreifen können und alle erforderlichen Weiterleitungen erfolgen.

Sehen Sie sich jetzt den Servercode unter router.post("/initialize-authentication", ... an.

Hier sind zwei interessante Punkte zu beachten:

  • Dabei werden sowohl das Passwort als auch die Anmeldedaten gleichzeitig überprüft. Dies ist eine Sicherheitsmaßnahme: Für Nutzer, die die Bestätigung in zwei Schritten eingerichtet haben, möchten wir keine unterschiedlichen UI-Abläufe festlegen, je nachdem, ob das Passwort korrekt war oder nicht. Wir prüfen also sowohl das Passwort als auch die Anmeldedaten gleichzeitig.
  • Wenn sowohl das Passwort als auch die Anmeldedaten gültig sind, schließen wir die Authentifizierung ab, indem wir completeAuthentication(req, res); aufrufen. Das bedeutet, dass wir die Sitzungen von einer temporären auth-Sitzung wechseln, in der der Nutzer noch nicht authentifiziert ist, bis zur Hauptsitzung, in der der Nutzer authentifiziert ist.main

Seite für die Bestätigung in zwei Schritten in den Nutzerfluss einbeziehen

Die neue Seite „second-factor.html“ befindet sich im Ordner „views“.

Sie hat eine Schaltfläche wie Sicherheitsschlüssel verwenden, aber derzeit funktioniert sie nicht.

Durch einen Klick auf diese Schaltfläche wird authenticateTwoFactor() aufgerufen.

  • Wenn authenticateTwoFactor() erfolgreich ist, leite den Nutzer auf die Seite Konto weiter.
  • Wenn der Vorgang nicht erfolgreich ist, weisen Sie den Nutzer darauf hin, dass ein Fehler aufgetreten ist. In einer echten Anwendung implementieren Sie hilfreichere Fehlermeldungen. Der Einfachheit halber verwenden wir in dieser Demo nur eine Fensterwarnung.
    <main>
...
    </main>
    <script type="module">
      import { authenticateTwoFactor, authStatuses } from "/auth.client.js";

      const button = document.querySelector("#authenticateButton");
      button.addEventListener("click", async e => {
        try {
          // Ask the user to authenticate with the second factor; this will trigger a browser prompt
          const response = await authenticateTwoFactor();
          const { authStatus } = response;
          if (authStatus === authStatuses.COMPLETE) {
            // The user is properly authenticated => Navigate to the Account page
            location.href = "/account";
          } else {
            throw new Error("Two-factor authentication failed");
          }
        } catch (e) {
          // Alert the user that something went wrong
          alert(`Two-factor authentication failed. ${e}`);
        }
      });
    </script>
  </body>
</html>

Bestätigung in zwei Schritten nutzen

Nun können Sie einen zweiten Schritt zur Authentifizierung hinzufügen.

Jetzt müssen Nutzer aus der Bestätigung in zwei Schritten diesen Schritt aus index.html hinzufügen.

322a5c49d865a0d8.png

Fügen Sie in index.html unter location.href = "/account"; Code hinzu, der den Nutzer zur Seite mit der zweiten Faktorauthentifizierung weiterleitet, wenn er 2FA eingerichtet hat.

In diesem Codelab wird durch das Erstellen von Anmeldedaten die Bestätigung in zwei Schritten für den Nutzer automatisch aktiviert.

Beachten Sie, dass mit server.js auch eine serverseitige Sitzungsprüfung implementiert wird. Diese gewährleistet, dass nur authentifizierte Nutzer auf account.html zugreifen können.

const { authStatus } = response;
if (authStatus === authStatuses.COMPLETE) {
  // The user is properly authenticated => navigate to account
  location.href = '/account';
} else if (authStatus === authStatuses.NEED_SECOND_FACTOR) {
  // Navigate to the two-factor-auth page because two-factor-auth is set up for this user
  location.href = '/second-factor';
}

Selbst ausprobieren 💻🏻 💻

  • Melden Sie sich mit dem neuen Nutzer maxmustermann an.
  • Melden Sie sich ab.
  • Melden Sie sich in Ihrem Konto als maxmustermann an. Beachten Sie, dass nur ein Passwort erforderlich ist.
  • Erstellen Sie die Anmeldedaten. Das bedeutet, dass du die Bestätigung in zwei Schritten als maxmustermann aktiviert hast.
  • Melden Sie sich ab.
  • Geben Sie Ihren Nutzernamen maxmustermann und Ihr Passwort ein.
  • Hier erfährst du, wie du automatisch zur Seite für die Bestätigung in zwei Schritten gehst.
  • Versuchen Sie, unter /account auf die Seite Konto zuzugreifen. Beachten Sie, dass Sie zur Indexseite weitergeleitet werden, weil Sie nicht vollständig authentifiziert sind: Sie haben einen zweiten Faktor.
  • Kehren Sie zur Seite für die Bestätigung in zwei Schritten zurück und klicken Sie auf Sicherheitsschlüssel für die Bestätigung in zwei Schritten verwenden.
  • Sie sind jetzt angemeldet und sollten Ihre Kontoseite sehen.

8. Anmeldedaten nutzerfreundlicher gestalten

Sie haben die grundlegenden Funktionen der Bestätigung in zwei Schritten mit dem Sicherheitsschlüssel „🚀“ genutzt.

Aber... Ist dir aufgefallen?

Unsere Liste mit Anmeldedaten ist derzeit nicht sehr praktisch: Die Anmeldedaten-ID und der öffentliche Schlüssel sind lange Strings, die bei der Verwaltung von Anmeldedaten nicht hilfreich sind. Menschen sind mit langen Strings und Ziffern nicht gut 🤖

Also optimieren wir sie und fügen Funktionen zum Benennen und Umbenennen von Anmeldedaten mit für Menschen lesbaren Strings hinzu.

Sieh dir „robenseeCredential“ an

Damit Sie die Funktion implementieren können, die nicht zu bahnbrechend ist, können Sie die Funktion zum Umbenennen der Anmeldedaten im Startcode in auth.client.js hinzufügen:

async function renameCredential(credId, newName) {
  const params = new URLSearchParams({
    credId,
    name: newName
  });
  return _fetch(
    `/auth/credential?${params}`,
    "PUT"
  );
}

Dies ist ein regulärer Aufruf zur Datenbankaktualisierung: Der Client sendet eine PUT-Anfrage mit einer Anmeldedaten-ID und einem neuen Namen an das Back-End.

Namen von benutzerdefinierten Anmeldedaten implementieren

In account.html wird die leere Funktion „rename“ angezeigt.

Fügen Sie den folgenden Code hinzu:

// Rename a credential
async function rename(credentialId) {
  // Let the user input a new name
  const newName = window.prompt(`Name this credential:`);
  // Rename only if the user didn't cancel AND didn't enter an empty name
  if (newName && newName.trim()) {
    try {
      // Make the backend call to rename the credential (the name is sanitized) server-side
      await renameCredential(credentialId, newName);
    } catch (e) {
      // Alert the user that something went wrong
      if (Array.isArray(e)) {
        alert(
          // `msg` not `message`, this is the key's name as per the express validator API
          `Renaming failed. ${e.map((err) => `${err.msg} (${err.param})`)}`
        );
      } else {
        alert(`Renaming failed. ${e}`);
      }
    }
    // Refresh the credential list to display the new name
    await updateCredentialList();
  }
}

Es ist möglicherweise sinnvoller, einen neuen Namen zu benennen, nachdem er erstellt wurde. Jetzt können Sie Anmeldedaten ohne Namen erstellen und sie nach der Erstellung umbenennen. Dies führt jedoch zu zwei Back-End-Aufrufen.

Verwende die Funktion rename in register(), damit Nutzer Anmeldedaten bei der Registrierung benennen können:

async function register() {
  let user = {};
  try {
    const user = await registerCredential();
    // Get the latest credential's ID (newly created credential)
    const allUserCredentials = user.credentials;
    const newCredential = allUserCredentials[allUserCredentials.length - 1];
    // Rename it
    await rename(newCredential.credId);
  } catch (e) {
    // ...
  }
  // Refresh the credential list to display the new credential
  await updateCredentialList();
}

Nutzereingaben werden validiert und im Back-End bereinigt:

  check("name")
    .trim()
    .escape()

Namen der Anmeldedaten anzeigen

Rufen Sie getCredentialHtml in templates.js auf.

Es gibt bereits einen Code, mit dem der Name der Anmeldedaten oben auf der Karte mit Anmeldedaten angezeigt wird:

// Register credential
const getCredentialHtml = (credential, removeEl, renameEl) => {
 const { name, credId, publicKey } = credential;
 return html`
    <div class="credential-card">
      <div class="credential-name">
        ${name
          ? html`
              ${name}
            `
          : html`
              <span class="unnamed">(Unnamed)</span>
            `}
      </div>
     // ...
    </div>
  `;
};

Selbst ausprobieren 💻🏻 💻

  • Erstellen Sie die Anmeldedaten.
  • Du wirst aufgefordert, dem Profil einen Namen zu geben.
  • Geben Sie einen neuen Namen ein und klicken Sie auf OK.
  • Die Anmeldedaten werden jetzt umbenannt.
  • Wiederholen Sie dies und prüfen Sie, ob auch das Feld für den Namen leer ist.

Umbenennung von Anmeldedaten aktivieren

Nutzer müssen möglicherweise die Anmeldedaten umbenennen, z. B. weil sie einen zweiten Schlüssel hinzufügen und ihren ersten Schlüssel umbenennen möchten, um sie besser unterscheiden zu können.

Suchen Sie in account.html die bisher leere Funktion renameEl und fügen Sie ihr den folgenden Code hinzu:

// Rename a credential via HTML element
async function renameEl(el) {
  // Define the ID of the credential to update
  const credentialId = el.srcElement.dataset.credentialId;
  // Rename the credential
  await rename(credentialId);
  // Refresh the credential list to display the new name
  await updateCredentialList();
}

Fügen Sie jetzt in class="flex-end" von templates.js im folgenden div-Element den folgenden Code ein: Dieser Code fügt der Vorlage für Anmeldedaten die Schaltfläche Umbenennen hinzu. Wenn diese Schaltfläche angeklickt wird, wird die renameEl-Funktion aufgerufen, die wir gerade erstellt haben:

const getCredentialHtml = (credential, removeEl, renameEl) => {
// ...
 <div class="flex-end">
  <button
    data-credential-id="${credId}"
    @click="${renameEl}"
    class="secondary right"
  >
   Rename
  </button>
 </div>
 // ...
  `;
};

Selbst ausprobieren 💻🏻 💻

  • Klicken Sie auf Umbenennen.
  • Geben Sie einen neuen Namen ein, wenn Sie dazu aufgefordert werden.
  • Klicken Sie auf OK.
  • Die Anmeldedaten wurden umbenannt und die Liste wird automatisch aktualisiert.
  • Wenn Sie die Seite aktualisieren, sollte trotzdem der neue Name angezeigt werden. Das zeigt, dass der neue Name serverseitig beibehalten wurde.

Erstellungsdatum der Anmeldedaten anzeigen

Das Erstellungsdatum ist nicht in den Anmeldedaten enthalten, die über navigator.credential.create() erstellt wurden.

Da diese Informationen jedoch für die Nutzer zwischen den Anmeldedaten hilfreich sein können, haben wir die serverseitige Bibliothek im Starter-Code überarbeitet und ein creationDate-Feld mit dem Wert Date.now() hinzugefügt, sobald neue Anmeldedaten gespeichert wurden.

Füge in templates.js innerhalb von class="creation-date" div Folgendes hinzu, um dem Nutzer Informationen zum Erstellungsdatum anzuzeigen:

<div class="creation-date">
  <label>Created:</label>
  <div class="info">
    ${new Date(creationDate).toLocaleDateString()}
    ${new Date(creationDate).toLocaleTimeString()}
  </div>
</div>

9. Code zukunftssicher machen

Bisher wurde der Nutzer nur aufgefordert, eine einfache Roaming-Authentifizierung zu registrieren, die dann als zweiter Faktor bei der Anmeldung verwendet wird.

Ein weiter ausgereifterer Ansatz wäre, sich auf eine leistungsfähigere Art von Authentifizierung zu verlassen: eine vom Nutzer verifizierte Roaming-Authentifizierung (UVRA). Eine UVRA kann bei der Anmeldung in nur einem Schritt zwei Authentifizierungsfaktoren und eine Phishing-Belastung bieten.

Im Idealfall unterstützen Sie beide Ansätze. Dazu müssen Sie die Nutzererfahrung anpassen:

  • Wenn ein Nutzer nur einen einfachen Roaming-Authentifizierungsvermittler hat, der keine Nutzerbestätigung verwendet, kann er damit einen Phishing-resistenten Konto-Bootstrahl erstellen. Er muss jedoch auch einen Nutzernamen und ein Passwort eingeben. Und genau das machen unser Codelab bereits.
  • Wenn ein anderer Nutzer einen fortgeschritteneren Nutzer verwendet, der das Roaming-Authentifizierungs-Tool überprüft, kann er während des Kontowechsels den Passwortschritt überspringen – und möglicherweise auch den Schritt beim Nutzernamen.

Weitere Informationen

In diesem Codelab passen wir die Nutzererfahrung nicht an, aber wir richten deine Codebasis so ein, dass du die erforderlichen Daten erhältst.

Sie benötigen zwei Dinge:

  • Legen Sie residentKey: preferred in Ihren Back-End-Einstellungen fest. Dieser Vorgang ist bereits abgeschlossen.
  • Richten Sie eine Methode ein, um herauszufinden, ob ein auffindbarer Anmeldedatenname (oder residenter Schlüssel) erstellt wurde.

So finden Sie heraus, ob eindeutige Anmeldedaten erstellt wurden:

  • Fragen Sie den Wert von credProps beim Erstellen der Anmeldedaten ab (credProps: true).
  • Fragen Sie den Wert von transports nach der Erstellung der Anmeldedaten ab. So könnt ihr feststellen, ob die zugrunde liegende Plattform UVRA-Funktionen unterstützt, d. h. ob es sich beispielsweise um ein Mobiltelefon handelt.
  • Speichern Sie den Wert von credProps und transports im Back-End. Dies wurde bereits im Auslöser-Code ausgeführt. Sehen Sie sich auth.js an, wenn Sie neugierig sind.

Lassen Sie uns den Wert für credProps und transports abrufen und an das Back-End senden. Ändern Sie in auth.client.js den registerCredential so:

  • Feld extensions beim Aufrufen von navigator.credentials.create hinzufügen
  • Legen Sie encodedCredential.transports und encodedCredential.credProps fest, bevor Sie die Anmeldedaten zum Speichern an das Back-End senden.

registerCredential sollte so aussehen:

async function registerCredential() {
  // Fetch the credential creation options from the backend
  const credentialCreationOptionsFromServer = await _fetch(
    '/auth/credential-options',
    'POST'
  );
  // Decode the credential creation options
  const credentialCreationOptions = decodeServerOptions(
    credentialCreationOptionsFromServer
  );
  // Create a credential via the browser API; this will prompt the user
  const credential = await navigator.credentials.create({
    publicKey: {
      ...credentialCreationOptions,
      extensions: {
        credProps: true,
      },
    },
  });
  // Encode the newly created credential to send it to the backend
  const encodedCredential = encodeCredential(credential);
  // Set transports and credProps for more advanced user flows
  encodedCredential.transports = credential.response.getTransports();
  encodedCredential.credProps =
    credential.getClientExtensionResults().credProps;
  // Send the encoded credential to the backend for storage
  return await _fetch('/auth/credential', 'POST', encodedCredential);
}

10. Browserübergreifende Unterstützung gewährleisten

Andere Browser als Chromium unterstützen

In der registerCredential-Funktion von public/auth.client.js wird credential.response.getTransports() mit den neu erstellten Anmeldedaten aufgerufen, um diese Informationen letztendlich im Back-End als Hinweis an den Server zu speichern.

Allerdings wird getTransports() derzeit nicht in allen Browsern implementiert (im Gegensatz zu getClientExtensionResults, der in allen Browsern unterstützt wird): Der Aufruf getTransports() gibt in Firefox und Safari einen Fehler zurück, wodurch die Erstellung von Anmeldedaten in diesen Browsern verhindert wird.

Sie müssen den encodedCredential.transports-Aufruf in einer Bedingung umschließen, damit der Code in allen gängigen Browsern ausgeführt wird:

if (credential.response.getTransports) {
  encodedCredential.transports = credential.response.getTransports();
}

Auf dem Server ist transports auf transports || [] gesetzt. In Firefox und Safari ist die Liste transports nicht undefined, sondern eine leere Liste []. Dadurch werden Fehler verhindert.

Nutzer warnen, die Browser verwenden, die WebAuthn nicht unterstützen

1e9c1be837d66ce8

WebAuthn wird zwar in allen gängigen Browsern unterstützt, es ist jedoch empfehlenswert, eine Warnung in Browsern anzuzeigen, die WebAuthn nicht unterstützen.

Prüfen Sie in index.html, ob dieses div-Element vorhanden ist:

<div id="warningbanner" class="invisible">
⚠️ Your browser doesn't support WebAuthn. Open this demo in Chrome, Edge, Firefox or Safari.
</div>

Fügen Sie im Inline-Skript von index.html folgenden Code hinzu, damit das Banner in Browsern angezeigt wird, die WebAuthn nicht unterstützen:

// Display a banner in browsers that don't support WebAuthn
if (!window.PublicKeyCredential) {
  document.querySelector('#warningbanner').classList.remove('invisible');
}

In einer echten Webanwendung führen Sie eine etwas komplexere Konfiguration aus und verfügen über einen korrekten Fallback-Mechanismus für diese Browser. Das zeigt Ihnen aber, wie der WebAuthn-Support unterstützt wird.

11. Perfekt!

✨Du hast es geschafft!

Sie haben die Bestätigung in zwei Schritten mit einem Sicherheitsschlüssel implementiert.

In diesem Codelab wurden die Grundlagen behandelt. Wenn Sie sich mit WebAuthn für 2FA vertraut machen möchten, finden Sie hier einige Ideen:

  • Informationen zur letzten Verwendung auf der Anmeldekarte hinzufügen. Anhand dieser Informationen können Nutzer feststellen, ob ein bestimmter Sicherheitsschlüssel aktiv verwendet wird oder nicht. Das gilt insbesondere dann, wenn sie mehrere Schlüssel registriert haben.
  • Eine zuverlässigere Fehlerbehandlung und präzisere Fehlermeldungen sind möglich.
  • Sieh dir die auth.js an und finde heraus, was passiert, wenn du einige der authSettings änderst, insbesondere wenn du einen Schlüssel verwendest, der die Nutzerbestätigung unterstützt.