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.
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 inevil-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
Quelle: https://www.yubico.com/products/security-key/
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.
- 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.
- 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.
- 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.
- Stecken Sie den Sicherheitsschlüssel in den Desktop und tippen Sie darauf.
- Auf Ihrem Computer (webauthn.io) sollte der Indikator „Erfolg“ angezeigt werden.
- 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.
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.
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. Wennnavigator.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
undrpId
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ürrpName
undrpId
verwendet. Eine gültigerpId
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 inexcludeCredentials
aufgeführten Anmeldedaten enthält. In unserem Codelab istexcludeCredentials
eine Liste der vorhandenen Anmeldedaten für diesen Nutzer. Mit diesem unduser.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 aufauthenticatorSelection
: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 wirpreferred
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:
Gehen Sie in diesem Fall so vor:
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ärenauth
-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.
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.
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
undtransports
im Back-End. Dies wurde bereits im Auslöser-Code ausgeführt. Sehen Sie sichauth.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 vonnavigator.credentials.create
hinzufügen - Legen Sie
encodedCredential.transports
undencodedCredential.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
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 derauthSettings
änderst, insbesondere wenn du einen Schlüssel verwendest, der die Nutzerbestätigung unterstützt.