Dieses Tutorial erklärt, wie Sie Passkeys in Ihrer Web-App implementieren. Wir verwenden Node.js (TypeScript), SimpleWebAuthn, Vanilla HTML / JavaScript und MySQL.
Vincent
Created: June 20, 2025
Updated: June 20, 2025
We aim to make the Internet a safer place using passkeys. That's why we want to support developers with tutorials on how to implement passkeys.
In diesem Tutorial unterstützen wir Sie bei der Implementierung von Passkeys und bieten eine Schritt-für-Schritt-Anleitung, wie Sie Passkeys zu Ihrer Website hinzufügen können.
Eine moderne, robuste und benutzerfreundliche Authentifizierung ist entscheidend, wenn Sie eine großartige Website oder App erstellen möchten. Passkeys haben sich als Antwort auf diese Herausforderung herauskristallisiert. Als neuer Standard für Logins versprechen sie eine Zukunft ohne die Nachteile traditioneller Passwörter und bieten ein wirklich passwortloses Login-Erlebnis (das nicht nur sicher, sondern auch äußerst bequem ist).
Recent Articles
📖
Passkeys in nativen Apps: Native vs. WebView-Implementierung
👤
Passkey-Fehlerbehebung: Lösungen für Passkey-Probleme und -Fehler
👤
So aktivieren Sie Passkeys unter Windows
⚙️
Passkey-Tutorial: So implementieren Sie Passkeys in Web-Apps
⚙️
E2E-Tests für Passkeys mit Playwright über den virtuellen WebAuthn-Authenticator
Was das Potenzial von Passkeys wirklich unterstreicht, ist die breite Unterstützung, die sie erhalten haben. Jeder bedeutende Browser, sei es Chrome, Firefox, Safari oder Edge, und alle wichtigen Gerätehersteller (Apple, Microsoft, Google) haben Unterstützung integriert. Diese einstimmige Annahme zeigt, dass Passkeys der neue Standard für Logins sind.
Ja, es gibt bereits Tutorials zur Integration von Passkeys in Webanwendungen. Sei es für Frontend-Frameworks wie React, Vue.js oder Next.js, es gibt eine Fülle von Anleitungen, die darauf ausgelegt sind, Herausforderungen zu mindern und Ihre Passkey-Implementierungen zu beschleunigen. Es fehlt jedoch ein End-to-End-Tutorial, das minimalistisch und hardwarenah bleibt. Viele Entwickler haben uns angesprochen und um ein Tutorial gebeten, das Klarheit in die Implementierung von Passkeys für Web-Apps bringt.
Genau aus diesem Grund haben wir diese Anleitung erstellt. Unser Ziel? Ein minimal lauffähiges Setup für Passkeys zu erstellen, das die Frontend-, Backend- und Datenbankebene umfasst (letztere wird oft vernachlässigt, obwohl sie ernsthafte Kopfschmerzen verursachen kann).
Am Ende dieser Reise werden Sie eine minimal lauffähige Webanwendung erstellt haben, in der Sie:
Für diejenigen, die es eilig haben oder eine Referenz benötigen, ist die gesamte Codebasis auf GitHub verfügbar.
Neugierig, wie das Endergebnis aussieht? Hier ist ein kleiner Vorgeschmack auf das fertige Projekt (wir geben zu, es sieht sehr einfach aus, aber das Interessante steckt unter der Oberfläche):
Wir sind uns vollkommen bewusst, dass Teile des Codes und des Projekts anders oder anspruchsvoller gestaltet werden können, aber wir wollten uns auf das Wesentliche konzentrieren. Deshalb haben wir die Dinge absichtlich einfach und auf Passkeys zentriert gehalten.
Wie füge ich Passkeys zu meiner Produktionswebsite hinzu?
Dies ist ein sehr minimales Beispiel für die Passkey-Authentifizierung. Die folgenden Dinge werden in diesem Tutorial NICHT berücksichtigt / implementiert oder nur sehr grundlegend:
Die vollständige Unterstützung all dieser Funktionen erfordert einen enormen zusätzlichen Entwicklungsaufwand. Für Interessierte empfehlen wir einen Blick auf diesen Artikel über Missverständnisse bei der Passkey-Implementierung durch Entwickler.
Bevor wir tief in die Implementierung von Passkeys eintauchen, werfen wir einen Blick auf die erforderlichen Fähigkeiten und Werkzeuge. Hier ist, was Sie für den Anfang benötigen:
Ein solides Verständnis der Bausteine des Webs – HTML, CSS und JavaScript – ist unerlässlich. Wir haben die Dinge absichtlich einfach gehalten, auf moderne JavaScript-Frameworks verzichtet und uns auf Vanilla JavaScript / HTML verlassen. Das einzige anspruchsvollere, was wir verwenden, ist die WebAuthn-Wrapper-Bibliothek @simplewebauthn/browser.
Für unser Backend verwenden wir einen Node.js
(Express) Server, der in TypeScript geschrieben ist. Wir haben
uns auch entschieden, mit der
WebAuthn-Serverimplementierung von SimpleWebAuthn
(@simplewebauthn/server
zusammen mit @simplewebauthn/typescript-types
) zu arbeiten. Es
gibt zahlreiche WebAuthn-Serverimplementierungen,
sodass Sie natürlich auch eine dieser verwenden können. Da wir uns für den TypeScript
WebAuthn-Server entschieden haben, sind grundlegende Kenntnisse in
Node.js und npm erforderlich.
Alle Benutzerdaten und öffentlichen Schlüssel der Passkeys werden in einer Datenbank gespeichert. Wir haben MySQL als Datenbanktechnologie ausgewählt. Ein grundlegendes Verständnis von MySQL und relationalen Datenbanken ist von Vorteil, obwohl wir Sie durch die einzelnen Schritte führen werden.
Im Folgenden verwenden wir die Begriffe WebAuthn und Passkeys oft austauschbar, auch wenn sie offiziell nicht dasselbe bedeuten mögen. Zum besseren Verständnis, insbesondere im Code-Teil, treffen wir diese Annahme jedoch.
Mit diesen Voraussetzungen sind Sie bestens gerüstet, um in die Welt der Passkeys einzutauchen.
Ben Gould
Head of Engineering
I’ve built hundreds of integrations in my time, including quite a few with identity providers and I’ve never been so impressed with a developer experience as I have been with Corbado.
Über 10.000 Entwickler vertrauen Corbado und machen das Internet mit Passkeys sicherer. Haben Sie Fragen? Wir haben über 150 Blogbeiträge zu Passkeys verfasst.
Tritt der Passkeys Community beiBevor wir uns mit dem Code und den Konfigurationen befassen, werfen wir einen Blick auf die Architektur des Systems, das wir erstellen möchten. Hier ist eine Aufschlüsselung der Architektur, die wir einrichten werden:
Mit dieser Architekturübersicht sollten Sie eine konzeptionelle Karte haben, wie die Komponenten unserer Anwendung zusammenspielen. Im weiteren Verlauf werden wir tiefer in jede dieser Komponenten eintauchen und deren Einrichtung, Konfiguration und Zusammenspiel detailliert beschreiben.
Das folgende Diagramm beschreibt den Prozessablauf während der Registrierung (Sign-up):
Das folgende Diagramm beschreibt den Prozessablauf während der Authentifizierung (Login):
Darüber hinaus finden Sie hier die Projektstruktur (nur die wichtigsten Dateien):
passkeys-tutorial ├── src # Enthält den gesamten TypeScript-Quellcode des Backends │ ├── controllers # Geschäftslogik zur Behandlung spezifischer Anfragetypen │ │ ├── authentication.ts # Logik für die Passkey-Authentifizierung │ │ └── registration.ts # Logik für die Passkey-Registrierung │ ├── middleware │ │ ├── customError.ts # Hinzufügen benutzerdefinierter Fehlermeldungen in standardisierter Weise │ │ └── errorHandler.ts # Allgemeiner Fehlerhandler │ ├── public │ │ ├── index.html # Haupt-HTML-Datei im Frontend │ │ ├── css │ │ │ └── style.css # Grundlegendes Styling │ │ └── js │ │ └── script.js # JavaScript-Logik (inkl. WebAuthn-API) │ ├── routes # Definitionen von API-Routen und deren Handlern │ │ └── routes.ts # Spezifische Passkey-Routen │ ├── services │ │ ├── credentialService.ts# Interagiert mit der Credential-Tabelle │ │ └── userService.ts # Interagiert mit der User-Tabelle │ ├── utils # Hilfsfunktionen und Dienstprogramme │ | ├── constants.ts # Einige Konstanten (z.B. rpID) │ | └── utils.ts # Hilfsfunktion │ ├── database.ts # Stellt die Verbindung von Node.js zur MySQL-Datenbank her │ ├── index.ts # Einstiegspunkt des Node.js-Servers │ └── server.ts # Verwaltet alle Servereinstellungen ├── config.json # Einige Konfigurationen für das Node.js-Projekt ├── docker-compose.yml # Definiert Dienste, Netzwerke und Volumes für Docker-Container ├── Dockerfile # Erstellt ein Docker-Image des Projekts ├── init-db.sql # Definiert unser MySQL-Datenbankschema ├── package.json # Verwaltet Node.js-Projektabhängigkeiten und Skripte └── tsconfig.json # Konfiguriert, wie TypeScript Ihren Code kompiliert
Bei der Implementierung von Passkeys ist die Einrichtung der Datenbank eine Schlüsselkomponente. Unser Ansatz verwendet einen Docker-Container, der MySQL ausführt und eine unkomplizierte und isolierte Umgebung bietet, die für zuverlässige Tests und Bereitstellungen unerlässlich ist.
Unser Datenbankschema ist absichtlich minimalistisch gehalten und umfasst nur zwei Tabellen. Diese Einfachheit trägt zu einem klareren Verständnis und einer einfacheren Wartung bei.
Detaillierte Tabellenstruktur
1. Credentials-Tabelle: Diese Tabelle ist zentral für die Passkey-Authentifizierung und speichert die Passkey-Anmeldeinformationen. Kritische Spalten:
credential_id
sind der geeignete Datentyp und die Formatierung entscheidend.2. Users-Tabelle: Verknüpft Benutzerkonten mit ihren entsprechenden Anmeldeinformationen.
Beachten Sie, dass wir die erste Tabelle credentials
genannt haben, da dies unserer
Erfahrung und den Empfehlungen anderer Bibliotheken entspricht (im Gegensatz zum Vorschlag
von SimpleWebAuthn, sie authenticator oder
authenticator_device
zu nennen).
Die Datentypen für credential_id
und public_key
sind entscheidend. Fehler entstehen
oft durch falsche Datentypen, Kodierung oder Formatierung (insbesondere der Unterschied
zwischen Base64 und Base64URL ist eine häufige Fehlerquelle), was den gesamten
Registrierungs- (Sign-up) oder Authentifizierungsprozess (Login) stören kann.
Alle notwendigen SQL-Befehle zum Einrichten dieser Tabellen sind in der Datei
init-db.sql
enthalten. Dieses Skript gewährleistet eine schnelle und fehlerfreie
Initialisierung der Datenbank.
Für anspruchsvollere Fälle können Sie credential_device_type
oder credential_backed_up
hinzufügen, um mehr Informationen über die Anmeldeinformationen zu speichern und die
Benutzererfahrung zu verbessern. Darauf verzichten wir in diesem Tutorial jedoch.
init-db.sqlCREATE TABLE users ( id VARCHAR(255) PRIMARY KEY, username VARCHAR(255) NOT NULL UNIQUE ); CREATE TABLE credentials ( id INT AUTO_INCREMENT PRIMARY KEY, user_id VARCHAR(255) NOT NULL, credential_id VARCHAR(255) NOT NULL, public_key TEXT NOT NULL, counter INT NOT NULL, transports VARCHAR(255), FOREIGN KEY (user_id) REFERENCES users (id) );
Nachdem wir diese Datei erstellt haben, erstellen wir eine neue docker-compose.yml
-Datei
auf der Root-Ebene des Projekts:
docker-compose.ymlversion: "3.1" services: db: image: mysql command: --default-authentication-plugin=mysql_native_password restart: always environment: MYSQL_ROOT_PASSWORD: my-secret-pw MYSQL_DATABASE: webauthn_db ports: - "3306:3306" volumes: - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql
Diese Datei startet die MySQL-Datenbank auf Port 3306 und erstellt die definierte Datenbankstruktur. Es ist wichtig zu beachten, dass der Name und das Passwort für die hier verwendete Datenbank zu Demonstrationszwecken einfach gehalten sind. In einer Produktionsumgebung sollten Sie für erhöhte Sicherheit komplexere Anmeldeinformationen verwenden.
Als Nächstes fahren wir mit dem Ausführen unseres Docker-Containers fort. An diesem Punkt
enthält unsere docker-compose.yml
-Datei nur diesen einen Container, aber wir werden
später weitere Komponenten hinzufügen. Um den Container zu starten, verwenden Sie den
folgenden Befehl:
docker compose up -d
Sobald der Container läuft, müssen wir überprüfen, ob die Datenbank wie erwartet funktioniert. Öffnen Sie ein Terminal und führen Sie den folgenden Befehl aus, um mit der MySQL-Datenbank zu interagieren:
docker exec -it <container ID> mysql -uroot -p
Sie werden aufgefordert, das Root-Passwort einzugeben, das in unserem Beispiel
my-secret-pw
lautet. Nach dem Einloggen wählen Sie die webauthn_db
-Datenbank aus und
zeigen die Tabellen mit diesen Befehlen an:
use webauthn_db; show tables;
An dieser Stelle sollten Sie die beiden in unserem Skript definierten Tabellen sehen. Anfänglich sind diese Tabellen leer, was anzeigt, dass unsere Datenbankeinrichtung abgeschlossen und für die nächsten Schritte bei der Implementierung von Passkeys bereit ist.
Das Backend ist das Herzstück jeder Passkey-Anwendung und fungiert als zentraler Knotenpunkt für die Verarbeitung von Benutzerauthentifizierungsanfragen vom Frontend. Es kommuniziert mit der WebAuthn-Serverbibliothek zur Handhabung von Registrierungs- (Sign-up) und Authentifizierungsanfragen (Login) und interagiert mit Ihrer MySQL-Datenbank, um Benutzeranmeldeinformationen zu speichern und abzurufen. Im Folgenden führen wir Sie durch die Einrichtung Ihres Backends mit Node.js (Express) mit TypeScript, das eine öffentliche API zur Bearbeitung aller Anfragen bereitstellt.
Erstellen Sie zunächst ein neues Verzeichnis für Ihr Projekt und navigieren Sie mit Ihrem Terminal oder Ihrer Eingabeaufforderung dorthin.
Führen Sie den Befehl aus
npx create-express-typescript-application passkeys-tutorial
Dies erstellt ein grundlegendes Code-Skelett einer Node.js (Express) App, die in TypeScript geschrieben ist und die wir für weitere Anpassungen verwenden können.
Ihr Projekt erfordert mehrere Schlüsselpakete, die wir zusätzlich installieren müssen:
Wechseln Sie in das neue Verzeichnis und installieren Sie sie mit den folgenden Befehlen (wir installieren auch die erforderlichen TypeScript-Typen):
cd passkeys-tutorial npm install @simplewebauthn/server mysql2 uuid express-session @types/express-session @types/uuid
Um zu bestätigen, dass alles korrekt installiert ist, führen Sie aus
npm run dev:nodemon
Dies sollte Ihren Node.js-Server im Entwicklungsmodus mit Nodemon starten, der den Server bei jeder Dateiänderung automatisch neu startet.
Tipp zur Fehlerbehebung: Wenn Fehler auftreten, versuchen Sie, ts-node
in der
package.json
-Datei auf Version 10.8.1 zu aktualisieren und führen Sie dann npm i
aus,
um die Updates zu installieren.
Ihre server.ts
-Datei hat die grundlegende Einrichtung und Middleware für eine
Express-Anwendung. Um die Passkey-Funktionalität zu integrieren,
müssen Sie hinzufügen:
Diese Erweiterungen sind der Schlüssel zur Aktivierung der Passkey-Authentifizierung im Backend Ihrer Anwendung. Wir richten sie später ein.
Nachdem wir die Datenbank in Abschnitt 4 erstellt und
gestartet haben, müssen wir nun sicherstellen, dass unser Backend eine Verbindung zur
MySQL-Datenbank herstellen kann. Dazu erstellen wir eine neue database.ts
-Datei im
/src
-Ordner und fügen den folgenden Inhalt hinzu:
database.tsimport mysql from "mysql2"; // Create a MySQL pool const pool = mysql.createPool({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, waitForConnections: true, connectionLimit: 10, queueLimit: 0, }); // Promisify for Node.js async/await. export const promisePool = pool.promise();
Diese Datei wird später von unserem Server verwendet, um auf die Datenbank zuzugreifen.
Werfen wir einen kurzen Blick auf unsere config.json
, in der bereits zwei Variablen
definiert sind: der Port, auf dem wir die Anwendung ausführen, und die Umgebung:
config.json{ "PORT": 8080, "NODE_ENV": "development" }
package.json
kann so bleiben, wie es ist, und sollte so aussehen:
package.json{ "name": "passkeys-tutorial", "version": "0.0.1", "description": "passkeys-tutorial initialised with create-express-typescript-application.", "main": "src/index.ts", "scripts": { "build": "tsc", "start": "node ./build/src/index.js", "dev": "ts-node ./src/index.ts", "dev:nodemon": "nodemon -w src -e ts,json -x ts-node ./src/index.ts", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": ["express", "typescript"], "devDependencies": { "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/morgan": "^1.9.9", "@types/node": "^14.18.63", "@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/parser": "^4.33.0", "eslint": "^7.32.0", "nodemon": "^2.0.22", "ts-node": "^10.8.1", "typescript": "^4.9.5" }, "dependencies": { "@simplewebauthn/server": "^8.3.5", "@types/express-session": "^1.17.10", "@types/uuid": "^9.0.7", "cors": "^2.8.5", "env-cmd": "^10.1.0", "express": "^4.18.2", "express-session": "^1.17.3", "fs": "^0.0.1-security", "helmet": "^4.6.0", "morgan": "^1.10.0", "mysql2": "^3.6.5", "uuid": "^9.0.1" } }
index.ts
sieht so aus:
index.tsimport app from "./server"; import config from "../config.json"; // Start the application by listening to specific port const port = Number(process.env.PORT || config.PORT || 8080); app.listen(port, () => { console.info("Express application started on port: " + port); });
In server.ts
müssen wir noch einige Dinge anpassen. Außerdem wird ein temporärer Cache
irgendeiner Art (z.B. redis, memcache oder express-session) benötigt, um temporäre
Challenges zu speichern, gegen die sich Benutzer authentifizieren können. Wir haben uns
für express-session
entschieden und deklarieren das express-session
-Modul oben, damit
die Dinge mit express-session
funktionieren. Zusätzlich straffen wir das Routing und
entfernen die Fehlerbehandlung vorerst (diese wird später zur Middleware hinzugefügt):
server.tsimport express, { Express } from "express"; import morgan from "morgan"; import helmet from "helmet"; import cors from "cors"; import config from "../config.json"; import { router as passkeyRoutes } from "./routes/routes"; import session from "express-session"; const app: Express = express(); declare module "express-session" { interface SessionData { currentChallenge?: string; loggedInUserId?: string; } } /************************************************************************************ * Basic Express Middlewares ***********************************************************************************/ app.set("json spaces", 4); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use( session({ // @ts-ignore secret: process.env.SESSION_SECRET, saveUninitialized: true, resave: false, cookie: { maxAge: 86400000, httpOnly: true, // Ensure to not expose session cookies to clientside scripts }, }), ); // Handle logs in console during development if (process.env.NODE_ENV === "development" || config.NODE_ENV === "development") { app.use(morgan("dev")); app.use(cors()); } // Handle security and origin in production if (process.env.NODE_ENV === "production" || config.NODE_ENV === "production") { app.use(helmet()); } /************************************************************************************ * Register all routes ***********************************************************************************/ app.use("/api/passkey", passkeyRoutes); app.use(express.static("src/public")); export default app;
Um die Daten in unseren beiden erstellten Tabellen effektiv zu verwalten, entwickeln wir
zwei separate Dienste in einem neuen src/services
-Verzeichnis: authenticatorService.ts
und userService.ts
.
Jeder Dienst wird CRUD-Methoden (Create, Read, Update, Delete) kapseln, die es uns ermöglichen, auf modulare und organisierte Weise mit der Datenbank zu interagieren. Diese Dienste erleichtern das Speichern, Abrufen und Aktualisieren von Daten in den authenticator- und Benutzertabellen. So sollte die Struktur dieser erforderlichen Dateien angelegt sein:
userService.ts
sieht so aus:
userService.tsimport { promisePool } from "../database"; // Adjust the import path as necessary import { v4 as uuidv4 } from "uuid"; export const userService = { async getUserById(userId: string) { const [rows] = await promisePool.query("SELECT * FROM users WHERE id = ?", [ userId, ]); // @ts-ignore return rows[0]; }, async getUserByUsername(username: string) { try { const [rows] = await promisePool.query( "SELECT * FROM users WHERE username = ?", [username], ); // @ts-ignore return rows[0]; } catch (error) { return null; } }, async createUser(username: string) { const id = uuidv4(); await promisePool.query("INSERT INTO users (id, username) VALUES (?, ?)", [ id, username, ]); return { id, username }; }, };
credentialService.ts
sieht wie folgt aus:
credentialService.tsimport { promisePool } from "../database"; import type { AuthenticatorDevice } from "@simplewebauthn/typescript-types"; export const credentialService = { async saveNewCredential( userId: string, credentialId: string, publicKey: string, counter: number, transports: string, ) { try { await promisePool.query( "INSERT INTO credentials (user_id, credential_id, public_key, counter, transports) VALUES (?, ?, ?, ?, ?)", [userId, credentialId, publicKey, counter, transports], ); } catch (error) { console.error("Error saving new credential:", error); throw error; } }, async getCredentialByCredentialId( credentialId: string, ): Promise<AuthenticatorDevice | null> { try { const [rows] = await promisePool.query( "SELECT * FROM credentials WHERE credential_id = ? LIMIT 1", [credentialId], ); // @ts-ignore if (rows.length === 0) return null; // @ts-ignore const row = rows[0]; return { userID: row.user_id, credentialID: row.credential_id, credentialPublicKey: row.public_key, counter: row.counter, transports: row.transports ? row.transports.split(",") : [], } as AuthenticatorDevice; } catch (error) { console.error("Error retrieving credential:", error); throw error; } }, async updateCredentialCounter(credentialId: string, newCounter: number) { try { await promisePool.query( "UPDATE credentials SET counter = ? WHERE credential_id = ?", [newCounter, credentialId], ); } catch (error) { console.error("Error updating credential counter:", error); throw error; } }, };
Zur zentralen Fehlerbehandlung und zur Erleichterung des Debuggings fügen wir eine
errorHandler.ts
-Datei hinzu:
errorHandler.tsimport { Request, Response, NextFunction } from "express"; import { CustomError } from "./customError"; interface ErrorWithStatus extends Error { statusCode?: number; } export const handleError = ( err: CustomError, req: Request, res: Response, next: NextFunction, ) => { const statusCode = err.statusCode || 500; const message = err.message || "Internal Server Error"; console.log(message); res.status(statusCode).send({ error: message }); };
Außerdem fügen wir eine neue customError.ts
-Datei hinzu, da wir später in der Lage sein
wollen, benutzerdefinierte Fehler zu erstellen, um Fehler schneller zu finden:
customError.tsexport class CustomError extends Error { statusCode: number; constructor(message: string, statusCode: number = 500) { super(message); this.statusCode = statusCode; Object.setPrototypeOf(this, CustomError.prototype); } }
Im utils
-Ordner erstellen wir zwei Dateien constants.ts
und utils.ts
.
constant.ts
enthält einige grundlegende WebAuthn-Serverinformationen, wie den Namen der
relying party, die
relying party ID und den Ursprung:
constant.tsexport const rpName: string = "Passkeys Tutorial"; export const rpID: string = "localhost"; export const origin: string = `http://${rpID}:8080`;
utils.ts
enthält zwei Funktionen, die wir später zum Kodieren und Dekodieren von Daten
benötigen:
utils.tsexport const uint8ArrayToBase64 = (uint8Array: Uint8Array): string => Buffer.from(uint8Array).toString("base64"); export const base64ToUint8Array = (base64: string): Uint8Array => new Uint8Array(Buffer.from(base64, "base64"));
Jetzt kommen wir zum Herzstück unseres Backends: den Controllern. Wir erstellen zwei
Controller, einen zum Erstellen eines neuen Passkeys (registration.ts
) und einen zum
Einloggen mit einem Passkey (authentication.ts
).
registration.ts
sieht so aus:
registration.tsimport { generateRegistrationOptions, verifyRegistrationResponse, } from "@simplewebauthn/server"; import { uint8ArrayToBase64 } from "../utils/utils"; import { rpName, rpID, origin } from "../utils/constants"; import { credentialService } from "../services/credentialService"; import { userService } from "../services/userService"; import { RegistrationResponseJSON } from "@simplewebauthn/typescript-types"; import { Request, Response, NextFunction } from "express"; import { CustomError } from "../middleware/customError"; export const handleRegisterStart = async ( req: Request, res: Response, next: NextFunction, ) => { const { username } = req.body; if (!username) { return next(new CustomError("Username empty", 400)); } try { let user = await userService.getUserByUsername(username); if (user) { return next(new CustomError("User already exists", 400)); } else { user = await userService.createUser(username); } const options = await generateRegistrationOptions({ rpName, rpID, userID: user.id, userName: user.username, timeout: 60000, attestationType: "direct", excludeCredentials: [], authenticatorSelection: { residentKey: "preferred", }, // Support for the two most common algorithms: ES256, and RS256 supportedAlgorithmIDs: [-7, -257], }); req.session.loggedInUserId = user.id; req.session.currentChallenge = options.challenge; res.send(options); } catch (error) { next( error instanceof CustomError ? error : new CustomError("Internal Server Error", 500), ); } }; export const handleRegisterFinish = async ( req: Request, res: Response, next: NextFunction, ) => { const { body } = req; const { currentChallenge, loggedInUserId } = req.session; if (!loggedInUserId) { return next(new CustomError("User ID is missing", 400)); } if (!currentChallenge) { return next(new CustomError("Current challenge is missing", 400)); } try { const verification = await verifyRegistrationResponse({ response: body as RegistrationResponseJSON, expectedChallenge: currentChallenge, expectedOrigin: origin, expectedRPID: rpID, requireUserVerification: true, }); if (verification.verified && verification.registrationInfo) { const { credentialPublicKey, credentialID, counter } = verification.registrationInfo; await credentialService.saveNewCredential( loggedInUserId, uint8ArrayToBase64(credentialID), uint8ArrayToBase64(credentialPublicKey), counter, body.response.transports, ); res.send({ verified: true }); } else { next(new CustomError("Verification failed", 400)); } } catch (error) { next( error instanceof CustomError ? error : new CustomError("Internal Server Error", 500), ); } finally { req.session.loggedInUserId = undefined; req.session.currentChallenge = undefined; } };
Lassen Sie uns die Funktionalitäten unserer Controller überprüfen, die die beiden Schlüsselschnittstellen im WebAuthn-Registrierungsprozess (Sign-up) behandeln. Hier liegt auch einer der größten Unterschiede zur passwortbasierten Authentifizierung: Für jeden Registrierungs- (Sign-up) oder Authentifizierungsversuch (Login) sind zwei Backend-API-Aufrufe erforderlich, die dazwischen spezifische Frontend-Inhalte benötigen. Passwörter benötigen normalerweise nur einen Endpunkt.
1. handleRegisterStart Endpunkt:
Dieser Endpunkt wird vom Frontend ausgelöst und empfängt einen Benutzernamen, um einen neuen Passkey und ein neues Konto zu erstellen. In diesem Beispiel erlauben wir die Erstellung eines neuen Kontos / Passkeys nur, wenn noch kein Konto existiert. In realen Anwendungen müssten Sie dies so handhaben, dass den Benutzern mitgeteilt wird, dass bereits ein Passkey existiert und das Hinzufügen vom selben Gerät nicht möglich ist (aber der Benutzer könnte Passkeys von einem anderen Gerät nach einer Form der Bestätigung hinzufügen). Der Einfachheit halber übersehen wir dies in diesem Tutorial.
Die PublicKeyCredentialCreationOptions
werden vorbereitet. residentKey
wird auf preferred
und attestationType
auf direct
gesetzt, um mehr Daten vom authenticator für eine mögliche
Datenbankspeicherung zu sammeln.
Im Allgemeinen bestehen die PublicKeyCredentialCreationOptions aus den folgenden Daten:
dictionary [PublicKeyCredentialCreationOptions](/glossary/publickeycredentialcreationoptions) { required PublicKeyCredentialRpEntity rp; required PublicKeyCredentialUserEntity user; required BufferSource challenge; required sequence<PublicKeyCredentialParameters> pubKeyCredParams; unsigned long timeout; sequence<PublicKeyCredentialDescriptor> [excludeCredentials](/glossary/excludecredentials) = []; AuthenticatorSelectionCriteria authenticatorSelection; DOMString attestation = "none"; AuthenticationExtensionsClientInputs extensions; };
rp.name
) und der
Domain (rp.id
).user.name
, user.id
und
user.displayName
.Die User ID und die Challenge werden in einem Sitzungsobjekt gespeichert, was den Prozess für Tutorialzwecke vereinfacht. Außerdem wird die Sitzung nach jedem Registrierungs- (Sign-up) oder Authentifizierungsversuch (Login) gelöscht.
2. handleRegisterFinish Endpunkt:
Dieser Endpunkt ruft die zuvor gesetzte user ID und
Challenge ab. Er verifiziert die RegistrationResponse
mit der Challenge. Wenn sie gültig
ist, speichert er eine neue Anmeldeinformation für den Benutzer. Sobald sie in der
Datenbank gespeichert ist, werden die user ID und die
Challenge aus der Sitzung entfernt.
Tipp: Beim Debuggen Ihrer Anwendung empfehlen wir dringend, Chrome als Browser und seine integrierten Funktionen zu verwenden, um die Entwicklererfahrung von passkey-basierten Anwendungen zu verbessern, z. B. den virtuellen WebAuthn-Authenticator und das Geräteprotokoll (siehe unsere Tipps für Entwickler unten für weitere Informationen).
Als Nächstes gehen wir zu authentication.ts
über, das eine ähnliche Struktur und
Funktionalität aufweist.
authentication.ts
sieht so aus:
authentication.tsimport { Request, Response, NextFunction } from "express"; import { generateAuthenticationOptions, verifyAuthenticationResponse, } from "@simplewebauthn/server"; import { uint8ArrayToBase64, base64ToUint8Array } from "../utils/utils"; import { rpID, origin } from "../utils/constants"; import { credentialService } from "../services/credentialService"; import { userService } from "../services/userService"; import { AuthenticatorDevice } from "@simplewebauthn/typescript-types"; import { isoBase64URL } from "@simplewebauthn/server/helpers"; import { VerifiedAuthenticationResponse, VerifyAuthenticationResponseOpts, } from "@simplewebauthn/server/esm"; import { CustomError } from "../middleware/customError"; export const handleLoginStart = async ( req: Request, res: Response, next: NextFunction, ) => { const { username } = req.body; try { const user = await userService.getUserByUsername(username); if (!user) { return next(new CustomError("User not found", 404)); } req.session.loggedInUserId = user.id; // [allowCredentials](/glossary/allowcredentials) is purposely for this demo left empty. This causes all existing local credentials // to be displayed for the service instead only the ones the username has registered. const options = await generateAuthenticationOptions({ timeout: 60000, allowCredentials: [], userVerification: "required", rpID, }); req.session.currentChallenge = options.challenge; res.send(options); } catch (error) { next( error instanceof CustomError ? error : new CustomError("Internal Server Error", 500), ); } }; export const handleLoginFinish = async ( req: Request, res: Response, next: NextFunction, ) => { const { body } = req; const { currentChallenge, loggedInUserId } = req.session; if (!loggedInUserId) { return next(new CustomError("User ID is missing", 400)); } if (!currentChallenge) { return next(new CustomError("Current challenge is missing", 400)); } try { const credentialID = isoBase64URL.toBase64(body.rawId); const bodyCredIDBuffer = isoBase64URL.toBuffer(body.rawId); const dbCredential: AuthenticatorDevice | null = await credentialService.getCredentialByCredentialId(credentialID); if (!dbCredential) { return next(new CustomError("Credential not registered with this site", 404)); } // @ts-ignore const user = await userService.getUserById(dbCredential.userID); if (!user) { return next(new CustomError("User not found", 404)); } // @ts-ignore dbCredential.credentialID = base64ToUint8Array(dbCredential.credentialID); // @ts-ignore dbCredential.credentialPublicKey = base64ToUint8Array( dbCredential.credentialPublicKey, ); let verification: VerifiedAuthenticationResponse; const opts: VerifyAuthenticationResponseOpts = { response: body, expectedChallenge: currentChallenge, expectedOrigin: origin, expectedRPID: rpID, authenticator: dbCredential, }; verification = await verifyAuthenticationResponse(opts); const { verified, authenticationInfo } = verification; if (verified) { await credentialService.updateCredentialCounter( uint8ArrayToBase64(bodyCredIDBuffer), authenticationInfo.newCounter, ); res.send({ verified: true }); } else { next(new CustomError("Verification failed", 400)); } } catch (error) { next( error instanceof CustomError ? error : new CustomError("Internal Server Error", 500), ); } finally { req.session.currentChallenge = undefined; req.session.loggedInUserId = undefined; } };
Unser Authentifizierungsprozess (Login) umfasst zwei Endpunkte:
1. handleLoginStart Endpunkt:
Dieser Endpunkt wird aktiviert, wenn ein Benutzer versucht, sich einzuloggen. Er prüft zunächst, ob der Benutzername in der Datenbank existiert, und gibt einen Fehler zurück, wenn er nicht gefunden wird. In einem realen Szenario könnten Sie stattdessen anbieten, ein neues Konto zu erstellen.
Für bestehende Benutzer ruft er die Benutzer-ID aus der Datenbank ab, speichert sie in der Sitzung und generiert PublicKeyCredentialRequestOptions-Optionen. allowCredentials wird leer gelassen, um die Verwendung von Anmeldeinformationen nicht einzuschränken. Deshalb können im Passkey-Modal alle verfügbaren Passkeys für diese relying party ausgewählt werden.
Die generierte Challenge wird ebenfalls in der Sitzung gespeichert und die PublicKeyCredentialRequestOptions werden an das Frontend zurückgesendet.
Die PublicKeyCredentialRequestOptions bestehen aus den folgenden Daten:
dictionary [PublicKeyCredentialRequestOptions](/glossary/publickeycredentialrequestoptions) { required BufferSource challenge; unsigned long timeout; USVString rpId; sequence<PublicKeyCredentialDescriptor> [allowCredentials](/glossary/allowcredentials) = []; DOMString userVerification = "preferred"; AuthenticationExtensionsClientInputs extensions; };
2. handleLoginFinish Endpunkt:
Dieser Endpunkt ruft currentChallenge
und loggedInUserId
aus der Sitzung ab.
Er fragt die Datenbank nach der richtigen Anmeldeinformation ab, indem er die
credential ID aus dem Body verwendet. Wenn die
Anmeldeinformation gefunden wird, bedeutet dies, dass der mit dieser
credential ID verknüpfte Benutzer nun authentifiziert
(eingeloggt) werden kann. Dann können wir den Benutzer aus der Benutzertabelle über die
Benutzer-ID abfragen, die wir von der Anmeldeinformation erhalten, und die
authenticationResponse
mit der Challenge und dem Anfrage-Body verifizieren. Wenn alles
erfolgreich ist, zeigen wir die Erfolgsmeldung für den Login an. Wenn keine passende
Anmeldeinformation gefunden wird, wird ein Fehler gesendet.
Zusätzlich wird, wenn die Verifizierung erfolgreich ist, der Zähler der Anmeldeinformation aktualisiert, die verwendete Challenge und die loggedInUserId werden aus der Sitzung entfernt.
Darüber hinaus können wir den Ordner src/app
und src/constant
zusammen mit allen darin
enthaltenen Dateien löschen.
Hinweis: Eine ordnungsgemäße Sitzungsverwaltung und Routenschutz, die in realen Anwendungen entscheidend sind, werden hier der Einfachheit halber in diesem Tutorial weggelassen.
Zu guter Letzt müssen wir sicherstellen, dass unsere Controller erreichbar sind, indem wir
die entsprechenden Routen zu routes.ts
hinzufügen, das sich in einem neuen Verzeichnis
src/routes
befindet:
routes.tsimport [express](/blog/ nodejs - passkeys ) from 'express'; import {handleError} from '../middleware/errorHandler'; import {handleRegisterStart, handleRegisterFinish} from '../controllers/registration'; import {handleLoginStart, handleLoginFinish} from '../controllers/authentication'; const router = express.Router(); router.post('/registerStart', handleRegisterStart); router.post('/registerFinish', handleRegisterFinish); router.post('/loginStart', handleLoginStart); router.post('/loginFinish', handleLoginFinish); router.use(handleError); export {router};
Dieser Teil des Passkey-Tutorials konzentriert sich darauf, wie Sie Passkeys im Frontend
Ihrer Anwendung unterstützen. Wir haben ein sehr einfaches Frontend, das aus drei Dateien
besteht: index.html
, styles.css
und script.js
. Alle drei Dateien befinden sich in
einem neuen src/public
-Ordner.
Die index.html
-Datei enthält ein Eingabefeld für den Benutzernamen und zwei
Schaltflächen zum Registrieren und Anmelden. Außerdem importieren wir das
@simplewebauthn/browser
-Skript, das die Interaktion mit der Web Authentication API des
Browsers in der js/script.js
-Datei vereinfacht.
index.html
sieht so aus:
index.html<!DOCTYPE html> <html lang="de"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Passkey Tutorial</title> <link rel="stylesheet" href="css/style.css" /> </head> <body> <div class="container"> <h1>Passkey Tutorial</h1> <div id="message"></div> <div class="input-group"> <input type="text" id="username" placeholder="Benutzernamen eingeben" /> <button id="registerButton">Registrieren</button> <button id="loginButton">Anmelden</button> </div> </div> <script src="https://unpkg.com/@simplewebauthn/browser/dist/bundle/index.es5.umd.min.js"></script> <script src="js/script.js"></script> </body> </html>
script.js
sieht wie folgt aus:
script.jsdocument.getElementById("registerButton").addEventListener("click", register); document.getElementById("loginButton").addEventListener("click", login); function showMessage(message, isError = false) { const messageElement = document.getElementById("message"); messageElement.textContent = message; messageElement.style.color = isError ? "red" : "green"; } async function register() { // Benutzernamen aus dem Eingabefeld abrufen const username = document.getElementById("username").value; try { // Registrierungsoptionen von Ihrem Server abrufen. Hier erhalten wir auch die Challenge. const response = await fetch("/api/passkey/registerStart", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: username }), }); console.log(response); // Überprüfen, ob die Registrierungsoptionen in Ordnung sind. if (!response.ok) { throw new Error( "Benutzer existiert bereits oder Registrierungsoptionen konnten nicht vom Server abgerufen werden", ); } // Die Registrierungsoptionen in JSON konvertieren. const options = await response.json(); console.log(options); // Dies veranlasst den Browser, das Passkey / WebAuthn-Modal anzuzeigen (z.B. Face ID, Touch ID, Windows Hello). // Eine neue Attestation wird erstellt. Das bedeutet auch, dass ein neues öffentlich-privates Schlüsselpaar erstellt wird. const attestationResponse = await SimpleWebAuthnBrowser.startRegistration(options); // Senden Sie die attestationResponse zur Überprüfung und Speicherung an den Server zurück. const verificationResponse = await fetch("/api/passkey/registerFinish", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(attestationResponse), }); if (verificationResponse.ok) { showMessage("Registrierung erfolgreich"); } else { showMessage("Registrierung fehlgeschlagen", true); } } catch (error) { showMessage("Fehler: " + error.message, true); } } async function login() { // Benutzernamen aus dem Eingabefeld abrufen const username = document.getElementById("username").value; try { // Anmeldeoptionen von Ihrem Server abrufen. Hier erhalten wir auch die Challenge. const response = await fetch("/api/passkey/loginStart", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: username }), }); // Überprüfen, ob die Anmeldeoptionen in Ordnung sind. if (!response.ok) { throw new Error("Anmeldeoptionen konnten nicht vom Server abgerufen werden"); } // Die Anmeldeoptionen in JSON konvertieren. const options = await response.json(); console.log(options); // Dies veranlasst den Browser, das Passkey / WebAuthn-Modal anzuzeigen (z.B. Face ID, Touch ID, Windows Hello). // Eine neue assertionResponse wird erstellt. Das bedeutet auch, dass die Challenge signiert wurde. const assertionResponse = await SimpleWebAuthnBrowser.startAuthentication(options); // Senden Sie die assertionResponse zur Überprüfung an den Server zurück. const verificationResponse = await fetch("/api/passkey/loginFinish", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(assertionResponse), }); if (verificationResponse.ok) { showMessage("Anmeldung erfolgreich"); } else { showMessage("Anmeldung fehlgeschlagen", true); } } catch (error) { showMessage("Fehler: " + error.message, true); } }
In script.js
gibt es drei Hauptfunktionen:
1. showMessage-Funktion:
Dies ist eine Hilfsfunktion, die hauptsächlich zur Anzeige von Fehlermeldungen verwendet wird und beim Debuggen hilft.
2. Register-Funktion:
Wird ausgelöst, wenn der Benutzer auf "Registrieren" klickt. Sie extrahiert den
Benutzernamen aus dem Eingabefeld und sendet ihn an den passkeyRegisterStart
-Endpunkt.
Die Antwort enthält
PublicKeyCredentialCreationOptions,
die in JSON konvertiert und an SimpleWebAuthnBrowser.startRegistration
übergeben werden.
Dieser Aufruf aktiviert den Geräte-authenticator (wie Face ID
oder Touch ID). Nach erfolgreicher lokaler Authentifizierung wird die signierte Challenge
an den passkeyRegisterFinish
-Endpunkt zurückgesendet, wodurch der
Passkey-Erstellungsprozess abgeschlossen wird.
Während des Registrierungsprozesses (Sign-up) spielt das attestation-Objekt eine entscheidende Rolle, also werfen wir einen genaueren Blick darauf.
Das attestation-Objekt besteht hauptsächlich aus drei
Komponenten: fmt
, attStmt
und authData
. Das fmt
-Element gibt das Format der
attestation-Anweisung an, während attStmt
die eigentliche
Attestierungsanweisung darstellt. In Szenarien, in denen die
attestation als unnötig erachtet wird, wird fmt
als "none"
bezeichnet, was zu einem leeren attStmt
führt.
Der Fokus liegt auf dem authData
-Segment innerhalb dieser Struktur. Dieses Segment ist
der Schlüssel zum Abrufen wesentlicher Elemente wie der Relying Party ID, Flags, Zähler
und attestierten Anmeldeinformationsdaten auf unserem Server. Bezüglich der Flags sind von
besonderem Interesse BS (Backup State) und BE (Backup Eligibility), die mehr Informationen
darüber liefern, ob ein Passkey synchronisiert ist (z.B. über
iCloud Keychain oder
1Password). Außerdem liefern UV
(User Verification) und UP
(User Presence) weitere nützliche Informationen.
Es ist wichtig zu beachten, dass verschiedene Teile des Attestation-Objekts, einschließlich der Authenticator-Daten, der relying party ID und der attestation-Anweisung, entweder gehasht oder vom authenticator mit seinem privaten Schlüssel digital signiert werden. Dieser Prozess ist integraler Bestandteil der Aufrechterhaltung der Gesamtintegrität des Attestation-Objekts.
3. Login-Funktion:
Wird aktiviert, wenn der Benutzer auf "Anmelden" klickt. Ähnlich wie die
Registrierungsfunktion extrahiert sie den Benutzernamen und sendet ihn an den
passkeyLoginStart
-Endpunkt. Die Antwort, die
PublicKeyCredentialRequestOptions
enthält, wird in JSON konvertiert und mit SimpleWebAuthnBrowser.startAuthentication
verwendet. Dies löst die lokale Authentifizierung auf dem Gerät aus. Die signierte
Challenge wird dann an den passkeyLoginFinish
-Endpunkt zurückgesendet. Eine erfolgreiche
Antwort von diesem Endpunkt zeigt an, dass sich der Benutzer erfolgreich in die App
eingeloggt hat.
Zusätzlich bietet die begleitende CSS-Datei ein einfaches Styling für die Anwendung:
body { font-family: "Helvetica Neue", Arial, sans-serif; text-align: center; padding: 40px; background-color: #f3f4f6; color: #333; } .container { max-width: 400px; margin: auto; background: white; padding: 20px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); border-radius: 8px; } h1 { color: #007bff; font-size: 24px; margin-bottom: 20px; } .input-group { margin-bottom: 20px; } input[type="text"] { padding: 10px; margin-bottom: 10px; border: 1px solid #ced4da; border-radius: 4px; width: calc(100% - 22px); } button { width: calc(50% - 20px); padding: 10px 0; margin: 5px; font-size: 16px; cursor: pointer; border: none; border-radius: 4px; background-color: #007bff; color: white; } button:hover { background-color: #0056b3; } #message { color: #dc3545; margin: 20px; }
Um Ihre Anwendung in Aktion zu sehen, kompilieren und führen Sie Ihren TypeScript-Code mit folgendem Befehl aus:
npm run dev
Ihr Server sollte nun unter http://localhost:8080 laufen.
Überlegungen für die Produktion:
Denken Sie daran, was wir behandelt haben, ist ein grundlegender Überblick. Wenn Sie eine Passkey-Anwendung in einer Produktionsumgebung bereitstellen, müssen Sie sich eingehender mit folgenden Themen befassen:
Wir haben bereits einen Docker-Container für unsere Datenbank eingerichtet. Als Nächstes
erweitern wir unser Docker-Compose-Setup, um den Server mit Backend und Frontend
einzubeziehen. Ihre docker-compose.yml
-Datei sollte entsprechend aktualisiert werden.
Um unsere Anwendung zu containerisieren, erstellen wir ein neues Dockerfile, das die erforderlichen Pakete installiert und den Entwicklungsserver startet:
Docker# Use an official Node runtime as a parent image FROM node:20-alpine # Set the working directory in the container WORKDIR /usr/src/app # Copy package.json and package-lock.json COPY package*.json ./ # Install any needed packages RUN npm install # Bundle your app's source code inside the Docker image COPY . . # Make port 8080 available to the world outside this container EXPOSE 8080 # Define the command to run your app CMD ["npm", "run", "dev"]
Dann erweitern wir auch die docker-compose.yml
-Datei, um diesen Container zu starten:
docker-compose.ymlversion: "3.1" services: db: image: mysql command: --default-authentication-plugin=mysql_native_password restart: always environment: MYSQL_ROOT_PASSWORD: my-secret-pw MYSQL_DATABASE: webauthn_db ports: - "3306:3306" volumes: - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql app: build: . ports: - "8080:8080" environment: - DB_HOST=db - DB_USER=root - DB_PASSWORD=my-secret-pw - DB_NAME=webauthn_db - SESSION_SECRET=secret123 depends_on: - db
Wenn Sie nun docker compose up
in Ihrem Terminal ausführen und auf
http://localhost:8080 zugreifen, sollten Sie die funktionierende
Version Ihrer Passkey-Web-App sehen (hier ausgeführt auf
Windows 11 23H2 + Chrome 119):
Da wir schon seit einiger Zeit mit Passkey-Implementierungen arbeiten, sind wir auf einige Herausforderungen gestoßen, wenn man an realen Passkey-Apps arbeitet:
Darüber hinaus haben wir die folgenden Tipps für Entwickler, wenn es um den Implementierungsteil geht:
Nutzen Sie den Passkeys-Debugger
Der Passkeys-Debugger hilft dabei, verschiedene WebAuthn-Servereinstellungen und Client-Antworten zu testen. Außerdem bietet er einen großartigen Parser für Authenticator-Antworten.
Debuggen mit der Chrome Device Log-Funktion
Verwenden Sie das Geräteprotokoll von Chrome (zugänglich über chrome://device-log/), um FIDO/WebAuthn-Aufrufe zu überwachen. Diese Funktion bietet Echtzeitprotokolle des Authentifizierungsprozesses (Login), sodass Sie die ausgetauschten Daten sehen und auftretende Probleme beheben können.
Ein weiterer sehr nützlicher Shortcut, um alle Ihre Passkeys in Chrome zu erhalten, ist die Verwendung von chrome://settings/passkeys.
Verwenden Sie den virtuellen WebAuthn-Authenticator von Chrome
Um die Verwendung der Touch ID-, Face ID- oder Windows Hello-Aufforderung während der Entwicklung zu vermeiden, verfügt Chrome über einen sehr praktischen virtuellen WebAuthn-Authenticator, der einen echten Authenticator emuliert. Wir empfehlen dringend, ihn zu verwenden, um die Dinge zu beschleunigen. Weitere Details finden Sie hier.
Testen Sie auf verschiedenen Plattformen und Browsern
Stellen Sie die Kompatibilität und Funktionalität auf verschiedenen Browsern und Plattformen sicher. WebAuthn verhält sich in verschiedenen Browsern unterschiedlich, daher ist gründliches Testen der Schlüssel.
Testen Sie auf verschiedenen Geräten
Hier ist es besonders nützlich, mit Tools wie ngrok zu arbeiten, mit denen Sie Ihre lokale Anwendung auf anderen (mobilen) Geräten erreichbar machen können.
Setzen Sie die Benutzerverifizierung auf „Preferred“
Wenn Sie die Eigenschaften für userVerification
in den
PublicKeyCredentialRequestOptions
definieren, wählen Sie die Einstellung preferred
, da dies ein guter Kompromiss zwischen
Benutzerfreundlichkeit und Sicherheit ist. Das bedeutet, dass Sicherheitsprüfungen auf
geeigneten Geräten vorhanden sind, während die Benutzerfreundlichkeit auf Geräten ohne
biometrische Fähigkeiten erhalten bleibt.
Wir hoffen, dieses Passkey-Tutorial vermittelt ein klares Verständnis dafür, wie man Passkeys effektiv implementiert. Im Laufe des Tutorials haben wir die wesentlichen Schritte zur Erstellung einer Passkey-Anwendung durchlaufen und uns dabei auf grundlegende Konzepte und die praktische Umsetzung konzentriert. Obwohl diese Anleitung als Ausgangspunkt dient, gibt es in der Welt von WebAuthn noch viel mehr zu entdecken und zu verfeinern.
Wir ermutigen Entwickler, tiefer in die Nuancen von Passkeys einzutauchen (z.B. das Hinzufügen mehrerer Passkeys, die Überprüfung der Passkey-Bereitschaft auf Geräten oder das Anbieten von Wiederherstellungslösungen). Es ist eine Reise, die es wert ist, angetreten zu werden, und die sowohl Herausforderungen als auch immense Belohnungen bei der Verbesserung der Benutzerauthentifizierung bietet. Mit Passkeys bauen Sie nicht nur eine Funktion; Sie tragen zu einer sichereren und benutzerfreundlicheren digitalen Welt bei.
Enjoyed this read?
🤝 Join our Passkeys Community
Share passkeys implementation tips and get support to free the world from passwords.
🚀 Subscribe to Substack
Get the latest news, strategies, and insights about passkeys sent straight to your inbox.
Related Articles
Table of Contents