Implementierung von WebAuthn für passwortlose Anmeldungen

WBOY
Freigeben: 2024-08-29 13:07:17
Original
348 Leute haben es durchsucht

Geschrieben von Oghenetega Denedo✏️

Das Merken und Speichern von Passwörtern kann für unsere Benutzer ein großer Aufwand sein – stellen Sie sich vor, die Anmeldung wäre für alle insgesamt einfacher. Hier kommt WebAuthn oder Web Authentication API ins Spiel. WebAuthn zielt darauf ab, eine Zukunft ohne Passwörter zu bieten.

In diesem Artikel besprechen wir die Funktionsweise von WebAuthn und erläutern, wie es die Public-Key-Kryptografie nutzt, um die Sicherheit zu gewährleisten. Außerdem führen wir Sie durch die Integration von WebAuthn für eine einfache Web-App, damit Sie lernen, wie Sie die API praktisch nutzen.

Wie jede Lösung hat WebAuthn seine guten und weniger tollen Seiten. Wir prüfen die Vor- und Nachteile, damit Sie feststellen können, ob es für Ihre Authentifizierungsanforderungen am besten geeignet ist. Seien Sie dabei, wenn wir versuchen, uns von Passwortproblemen zu verabschieden und das Versprechen eines nahtlosen Login-Erlebnisses mit WebAuthn zu entdecken.

Was Sie wissen sollten, bevor Sie sich mit WebAuthn vertraut machen

Bevor wir die Implementierung passwortloser Anmeldungen mit WebAuthn durchgehen, ist es wichtig, dass Sie die folgenden Voraussetzungen erfüllen:

  • Node.js auf Ihrem Computer installiert
  • Ein Android- oder iOS-Gerät, das zu Testzwecken mit WebAuthn kompatibel ist
  • Grundlegende Vertrautheit mit Node.js und Express.js
  • MongoDB-Datenbank zum Speichern von Benutzeranmeldeinformationen und Passkeys

Wenn Sie bereits wissen, was WebAuthn ist und wie es funktioniert, können Sie gerne mit dem Abschnitt zur Implementierung fortfahren. Wenn Sie das Gefühl haben, dass Sie eine Auffrischung benötigen, sollten die folgenden Informationen Ihnen dabei helfen, den Grundstein zu legen.

Was ist WebAuthn?

WebAuthn ist ein Webstandard, der aus der Notwendigkeit einer sicheren und passwortlosen Authentifizierung in Webanwendungen heraus ins Leben gerufen wurde, um die größten Mängel bei der Verwendung von Passwörtern zu beheben.

Das Projekt wurde vom World Wide Web Consortium (W3C) in Zusammenarbeit mit der FIDO (Fast Identity Online) mit dem Ziel veröffentlicht, eine standardisierte Schnittstelle zu schaffen, die über Geräte und Betriebssysteme hinweg zur Authentifizierung von Benutzern funktioniert.

Auf praktischer Ebene besteht WebAuthn aus drei wesentlichen Komponenten: der vertrauenden Seite, dem WebAuthn-Client und dem Authentifikator.

Die vertrauende Seite ist der Onlinedienst oder die Anwendung, die eine Authentifizierung für den Benutzer anfordert.

Der WebAuthn-Client fungiert als Vermittler zwischen dem Benutzer und der vertrauenden Seite – er ist in jeden kompatiblen Webbrowser oder jede mobile App eingebettet, die WebAuthn unterstützt.

Der Authentifikator ist das Gerät oder die Methode, mit der die Identität des Benutzers überprüft wird, beispielsweise ein Fingerabdruckscanner, ein Gesichtserkennungssystem oder ein Hardware-Sicherheitsschlüssel.

Wie funktioniert WebAuthn?

Wenn Sie sich für ein Konto auf einer Website registrieren, die WebAuthn unterstützt, starten Sie einen Anmeldevorgang, bei dem Sie einen Authentifikator wie Ihren Fingerabdruckscanner auf Ihrem Telefon verwenden. Dies führt zur Generierung eines öffentlichen Schlüssels, der in der Datenbank der vertrauenden Partei gespeichert wird, und eines privaten Schlüssels, der über eine sichere Hardwareschicht sicher auf Ihrem Gerät gespeichert wird.

Da die Website beim Versuch, sich anzumelden, nicht nach einem Passwort fragt, passiert es tatsächlich, dass nach dem Einleiten der Anmeldung eine Herausforderung an Ihr Gerät gesendet wird. Diese Challenge enthält normalerweise Informationen wie die Website-Adresse, um zu bestätigen, dass Sie sich von der Website aus anmelden, die die vertrauende Seite erwartet.

Nachdem Sie die Herausforderung von der Website erhalten haben, erstellt Ihr Gerät mithilfe Ihres privaten Schlüssels eine signierte Antwort. Diese Antwort zeigt, dass Sie den entsprechenden öffentlichen Schlüssel besitzen, der auf der Website gespeichert ist, ohne den privaten Schlüssel selbst preiszugeben.

Die vertrauende Seite validiert den gespeicherten öffentlichen Schlüssel nach Erhalt Ihrer signierten Antwort. Wenn die Signatur übereinstimmt, kann die Website feststellen, dass Sie der tatsächliche Benutzer sind und Ihnen Zugriff gewähren. Es wurden keine Passwörter ausgetauscht und Ihr privater Schlüssel blieb sicher auf Ihrem Gerät.

So implementieren Sie die passwortlose Authentifizierung mit WebAuthn

Nachdem wir uns nun mit den grundlegenden Konzepten von WebAuthn befasst haben, können wir sehen, wie sich das alles in der Praxis auswirkt. Bei der Anwendung, die wir erstellen werden, handelt es sich um eine einfache Express.js-App mit einigen API-Endpunkten zur Abwicklung der Registrierung und Anmeldung sowie einer einfachen HTML-Seite, die das Anmelde- und Registrierungsformular enthält.

Projekt-Setup

Zuerst müssen Sie das Projekt von GitHub klonen, das den Startercode enthält, sodass wir nicht viel Gerüstbau leisten müssen.

Geben Sie in Ihrem Terminal die folgenden Befehle ein:

git clone https://github.com/josephden16/webauthn-demo.git
Nach dem Login kopieren

git checkout start-here # Hinweis: Stellen Sie sicher, dass Sie sich im Starter-Zweig befinden

Wenn Sie die endgültige Lösung sehen möchten, checken Sie in der endgültigen Lösung oder im Hauptzweig ein.

Als nächstes installieren Sie die Projektabhängigkeiten:

npm install
Nach dem Login kopieren

Als nächstes erstellen Sie eine neue Datei, .env, im Stammverzeichnis des Projekts. Kopieren Sie den Inhalt von .env.sample hinein und geben Sie die entsprechenden Werte an:

# .env PORT=8000 MONGODB_URL=
Nach dem Login kopieren

After following these steps, the project should run without throwing errors, but to confirm, enter the command below to start the development server:

npm run dev
Nach dem Login kopieren

With that, we've set up the project. In the next section, we'll add the login and registration form.

Creating the login and registration form

The next step in our process is creating a single form that can handle registration and logging in. To do this, we must create a new directory in our codebase called public. Inside this directory, we will create a new file called index.html. This file will contain the necessary code to build the form we need.

Inside the index.html file, add the following code:

     WebAuthn Demo       

WebAuthn Demo

Nach dem Login kopieren

So, we've just added a simple login and registration form for users to sign in with WebAuthn. Also, if you check theelement, we've included the link to the Inter font using Google Fonts, Tailwind CSS for styling, and the SimpleWebAuthn browser package.

SimpleWebAuthn is an easy-to-use library for integrating WebAuthn into your web applications, as the name suggests. It offers a client and server library to reduce the hassle of implementing Webauthn in your projects.

When you visit http://localhost:8010, the port will be what you're using, you should see a form like the one below:Implementing WebAuthn for passwordless logins

Let's create a script.js file that'll store all the code for handling form submissions and interacting with the browser's Web Authentication API for registration and authentication. Users must register on a website before logging in, so we must implement the registration functionality first.

Head to the script.js file and include the following code:

const { startRegistration, browserSupportsWebAuthn } = SimpleWebAuthnBrowser; document.addEventListener("DOMContentLoaded", function () { const usernameInput = document.getElementById("username"); const registerBtn = document.getElementById("registerBtn"); const loginBtn = document.getElementById("loginBtn"); const errorDiv = document.getElementById("error"); const loginForm = document.getElementById("loginForm"); const welcomeMessage = document.getElementById("welcomeMessage"); const usernameDisplay = document.getElementById("usernameDisplay"); registerBtn.addEventListener("click", handleRegister); loginBtn.addEventListener("click", handleLogin); });
Nach dem Login kopieren

At the start of the code above, we import the necessary functions to work with WebAuthn. The document.addEventListener("DOMContentLoaded", function () { ... }) part ensures that the code inside the curly braces ({...}) executes after the web page is loaded.

It is important to avoid errors that might occur if you try to access elements that haven't been loaded yet.

Within the DOMContentLoaded event handler, we're initializing variables to store specific HTML elements we'll be working with and event listeners for the login and registration buttons.

Next, let's add the handleRegister() function. Inside the DOMContentLoaded event handler, add the code below:

async function handleRegister(evt) { errorDiv.textContent = ""; errorDiv.style.display = "none"; const userName = usernameInput.value; if (!browserSupportsWebAuthn()) { return alert("This browser does not support WebAuthn"); } const resp = await fetch(`/api/register/start?username=${userName}`, { credentials: "include" }); const registrationOptions = await resp.json(); let authResponse; try { authResponse = await startRegistration(registrationOptions); } catch (error) { if (error.name === "InvalidStateError") { errorDiv.textContent = "Error: Authenticator was probably already registered by user"; } else { errorDiv.textContent = error.message; } } if (!authResponse) { errorDiv.textContent = "Failed to connect with your device"; return; } const verificationResp = await fetch( `/api/register/verify?username=${userName}`, { credentials: "include", method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(authResponse), } ); if (!verificationResp.ok) { errorDiv.textContent = "Oh no, something went wrong!"; return; } const verificationJSON = await verificationResp.json(); if (verificationJSON && verificationJSON.verified) { alert("Registration successful! You can now login"); } else { errorDiv.textContent = "Oh no, something went wrong!"; } }
Nach dem Login kopieren

The handleRegister() function initiates the registration process by retrieving the username entered by the user from an input field. If the browser supports WebAuthn, it sends a request to the /api/register/start endpoint to initiate the registration process.

Once the registration options are retrieved, the startRegistration() method initiates the registration process with the received options. If the registration process is successful, it sends a verification request to another API endpoint /api/register/verify with the obtained authentication response and alerts the user that the registration was successful.

Since we haven't built the API endpoint for handling user registration yet, it won't function as expected, so let's head back to the codebase and create it.

Building the registration API endpoints

To finish the registration functionality, we'll need two API endpoints: one for generating the registration options that'll be passed to the authenticator and the other for verifying the response from the authenticator. Then, we'll store the credential data from the authenticator and user data in the database.

Let's start by creating the MongoDB database models to store user data and passkey. At the project's root, create a new folder called models and within that same folder, create two new files: User.js for the user data and PassKey.js for the passkey.

In the User.js file, add the following code:

import mongoose from "mongoose"; const UserSchema = new mongoose.Schema( { username: { type: String, unique: true, required: true, }, authenticators: [], }, { timestamps: true } ); const User = mongoose.model("User", UserSchema); export default User;
Nach dem Login kopieren

We're defining a simple schema for the user model that'll store the data of registered users. Next, in the PassKey.js file, add the following code:

import mongoose from "mongoose"; const PassKeySchema = new mongoose.Schema( { user: { type: mongoose.Schema.ObjectId, ref: "User", required: true, }, webAuthnUserID: { type: String, required: true, }, credentialID: { type: String, required: true, }, publicKey: { type: String, required: true, }, counter: { type: Number, required: true, }, deviceType: { type: String, enum: ["singleDevice", "multiDevice"], required: true, }, backedUp: { type: Boolean, required: true, }, authenticators: [], transports: [], }, { timestamps: true } ); const PassKey = mongoose.model("PassKey", PassKeySchema); export default PassKey;
Nach dem Login kopieren

We have created a schema for the PassKey model that stores all the necessary data of the authenticator after a successful registration. This schema will be used to identify the authenticator for all future authentications.

Having defined our data models, we can now set up the registration API endpoints. Within the root of the project, create two new folders: routes and controllers. Within each of the newly created folders, add a file named index.js. Within the routes/index.js file, add the code below:

import express from "express"; import { generateRegistrationOptionsCtrl, verifyRegistrationCtrl, } from "../controllers/index.js"; const router = express.Router(); router.get("/register/start", generateRegistrationOptionsCtrl); router.post("/register/verify", verifyRegistrationCtrl); export default router;
Nach dem Login kopieren

We're defining the routes we used earlier for user registration using Express.js. It imports two controller functions for generating registration options and verifying the response from the startRegistration() method that'll be called in the browser.

Let's start by adding the generateRegistrationOptionsCtrl() controller to generate the registration options. In the controllers/index.js file, add the following code:

// Import necessary modules and functions import { generateRegistrationOptions, verifyRegistrationResponse, } from "@simplewebauthn/server"; import { bufferToBase64URLString, base64URLStringToBuffer, } from "@simplewebauthn/browser"; import { v4 } from "uuid"; import User from "../models/User.js"; import PassKey from "../models/PassKey.js"; // Human-readable title for your website const relyingPartyName = "WebAuthn Demo"; // A unique identifier for your website const relyingPartyID = "localhost"; // The URL at which registrations and authentications should occur const origin = `http://${relyingPartyID}`; // Controller function to generate registration options export const generateRegistrationOptionsCtrl = async (req, res) => { const { username } = req.query; const user = await User.findOne({ username }); let userAuthenticators = []; // Retrieve authenticators used by the user before, if any if (user) { userAuthenticators = [...user.authenticators]; } // Generate a unique ID for the current user session let currentUserId; if (!req.session.currentUserId) { currentUserId = v4(); req.session.currentUserId = currentUserId; } else { currentUserId = req.session.currentUserId; } // Generate registration options const options = await generateRegistrationOptions({ rpName: relyingPartyName, rpID: relyingPartyID, userID: currentUserId, userName: username, timeout: 60000, attestationType: "none", // Don't prompt users for additional information excludeCredentials: userAuthenticators.map((authenticator) => ({ id: authenticator.credentialID, type: "public-key", transports: authenticator.transports, })), supportedAlgorithmIDs: [-7, -257], authenticatorSelection: { residentKey: "preferred", userVerification: "preferred", }, }); // Save the challenge to the session req.session.challenge = options.challenge; res.send(options); };
Nach dem Login kopieren

First, we import the necessary functions and modules from libraries like @simplewebauthn/server and uuid. These help us handle the authentication process smoothly.

Next, we define some constants. relyingPartyName is a friendly name for our website. In this case, it's set to "WebAuthn Demo." relyingPartyID is a unique identifier for our website. Here, it's set to "localhost". Then, we construct the origin variable, the URL where registrations and authentications will happen. In this case, it's constructed using the relying party ID.

Moving on to the main part of the code, we have the controller generateRegistrationOptionsCtrl(). It's responsible for generating user registration options.

Inside this function, we first extract the username from the request. Then, we try to find the user in our database using this username. If we find the user, we retrieve the authenticators they've used before. Otherwise, we initialize an empty array for user authenticators.

Next, we generate a unique ID for the current user session. If there's no ID stored in the session yet, we generate a new one using the v4 function from the uuid library and store it in the session. Otherwise, we retrieve the ID from the session.

Then, we use the generateRegistrationOptions() function to create user registration options. After generating these options, we save the challenge to the session and send the options back as a response.

Next, we'll need to add the verifyRegistrationCtrl() controller to handle verifying the response sent from the browser after the user has initiated the registration:

// Controller function to verify registration export const verifyRegistrationCtrl = async (req, res) => { const body = req.body; const { username } = req.query; const user = await User.findOne({ username }); const expectedChallenge = req.session.challenge; // Check if the expected challenge exists if (!expectedChallenge) { return res.status(400).send({ error: "Failed: No challenge found" }); } let verification; try { const verificationOptions = { response: body, expectedChallenge: `${expectedChallenge}`, expectedOrigin: origin, expectedRPID: relyingPartyID, requireUserVerification: false, }; verification = await verifyRegistrationResponse(verificationOptions); } catch (error) { console.error(error); return res.status(400).send({ error: error.message }); } const { verified, registrationInfo } = verification; // If registration is verified, update user data if (verified && registrationInfo) { const { credentialPublicKey, credentialID, counter, credentialBackedUp, credentialDeviceType, } = registrationInfo; const credId = bufferToBase64URLString(credentialID); const credPublicKey = bufferToBase64URLString(credentialPublicKey); const newDevice = { credentialPublicKey: credPublicKey, credentialID: credId, counter, transports: body.response.transports, }; // Check if the device already exists for the user const existingDevice = user?.authenticators.find( (authenticator) => authenticator.credentialID === credId ); if (!existingDevice && user) { // Add the new device to the user's list of devices await User.updateOne( { _id: user._id }, { $push: { authenticators: newDevice } } ); await PassKey.create({ counter, credentialID: credId, user: user._id, webAuthnUserID: req.session.currentUserId, publicKey: credPublicKey, backedUp: credentialBackedUp, deviceType: credentialDeviceType, transports: body.response.transports, authenticators: [newDevice], }); } else { const newUser = await User.create({ username, authenticators: [newDevice], }); await PassKey.create({ counter, credentialID: credId, user: newUser._id, webAuthnUserID: req.session.currentUserId, publicKey: credPublicKey, backedUp: credentialBackedUp, deviceType: credentialDeviceType, transports: body.response.transports, authenticators: [newDevice], }); } } // Clear the challenge from the session req.session.challenge = undefined; res.send({ verified }); };
Nach dem Login kopieren

The verifyRegistrationCtrl() controller searches for a user in the database based on the provided username. If found, it retrieves the expected challenge from the session data. If there's no expected challenge, the function returns an error. It then sets up verification options and calls a function named verifyRegistrationResponse.

If an error occurs, it logs the error and sends a response with the error message. If the registration is successfully verified, the function updates the user's data with the information provided in the registration response. It adds the new device to the user's list of devices if it does not exist.

Finally, the challenge is cleared from the session, and a response indicates whether the registration was successfully verified.

Before we head back to the browser to test what we've done so far, return to the app.js file and add the following code to register the routes:

import router from "./routes/index.js"; // place this at the start of the file app.use("/api", router); // place this before the call to `app.listen()`
Nach dem Login kopieren

Now that we've assembled all the pieces for the registration functionality, we can return to the browser to test it out.

When you enter a username and click the "register" button, you should see a prompt similar to the one shown below:

Implementing WebAuthn for passwordless loginsTo create a new passkey, you can now scan the QR code with your Android or iOS device. Upon successfully creating the passkey, a response is sent from the startRegistration() method to the /register/verify endpoint. Still, you'll notice it fails because of the error sent from the API:

{ "error": "Unexpected registration response origin \"http://localhost:8030\", expected \"http://localhost\"" }
Nach dem Login kopieren

Why this is happening is because the origin that the verifyRegistrationResponse() method expected, which is http://localhost, is different from http://localhost:8010, was sent.

So, you might wonder why we can't just change it to http://localhost:8010. That’s because when we defined the origin in the controllers/index.js file, the relyingPartyID was set to "localhost", and we can't explicitly specify the port for the relying party ID.

An approach to get around this issue is to use a web tunneling service like tunnelmole or ngrok to expose our local server to the internet with a publicly accessible URL so we don't have to specify the port when defining the relyingPartyID.

Exposing your local server to the internet

Let's quickly set up tunnelmole to share the server on our local machine to a live URL.

First, let's install tunnelmole by entering the command below in your terminal:

sudo npm install -g tunnelmole
Nach dem Login kopieren

Next, enter the command below to make the server running locally available on the internet:

tmole 
Nach dem Login kopieren

You should see an output like this from your terminal if it was successful:Implementing WebAuthn for passwordless loginsYou can now use the tunnelmole URL as the origin:

const relyingPartyID = "randomstring.tunnelmole.net"; // use output from your terminal const origin = `https://${relyingPartyID}`; // webauthn only works with https
Nach dem Login kopieren

Everything should work as expected, so head back to your browser to start the registration process. Once you're done, an alert should pop up informing you that the registration was successful and that you can now log in:Implementing WebAuthn for passwordless logins

We've successfully set up the user registration feature. The only thing left to do is implement the logging-in functionality.

Building the login functionality

The login process will follow a similar flow to the registration process. First, we’ll request authentication options from the server to be passed to the authenticator on your device.

Afterward, a request will be sent to the server to verify the authenticator's response. If all the criteria are met, the user can log in successfully.

Head back to the public/script.js file, and include the function to handle when the "login" button is clicked:

async function handleLogin(evt) { errorDiv.textContent = ""; errorDiv.style.display = "none"; const userName = usernameInput.value; if (!browserSupportsWebAuthn()) { return alert("This browser does not support WebAuthn"); } const resp = await fetch(`/api/login/start?username=${userName}`, { credentials: "include", headers: { "ngrok-skip-browser-warning": "69420", }, }); if (!resp.ok) { const error = (await resp.json()).error; errorDiv.textContent = error; errorDiv.style.display = "block"; return; } let asseResp; try { asseResp = await startAuthentication(await resp.json()); } catch (error) { errorDiv.textContent = error.message; errorDiv.style.display = "block"; } if (!asseResp) { errorDiv.textContent = "Failed to connect with your device"; errorDiv.style.display = "block"; return; } const verificationResp = await fetch( `/api/login/verify?username=${userName}`, { credentials: "include", method: "POST", headers: { "Content-Type": "application/json", "ngrok-skip-browser-warning": "69420", }, body: JSON.stringify(asseResp), } ); const verificationJSON = await verificationResp.json(); if (verificationJSON && verificationJSON.verified) { const userName = verificationJSON.username; // Hide login form and show welcome message loginForm.style.display = "none"; welcomeMessage.style.display = "block"; usernameDisplay.textContent = userName; } else { errorDiv.textContent = "Oh no, something went wrong!"; errorDiv.style.display = "block"; } }
Nach dem Login kopieren

The function starts by clearing error messages and retrieving the user's username from the form. It checks if the browser supports WebAuthn; if it does, it sends a request to the server to initiate the login process.

If the response from the server is successful, it attempts to authenticate the user. Upon successful authentication, it hides the login form and displays a welcome message with the user's name. Otherwise, it displays an error message to the user.

Next, head back to the routes/index.js file and add the routes for logging in:

router.get("/login/start", generateAuthenticationOptionsCtrl); router.post("/login/verify", verifyAuthenticationCtrl);
Nach dem Login kopieren

Don't forget to update the imports, as you're including the code above. Let's continue by adding the code to generate the authentication options. Go to the controllers/index.js file and add the following code:

// Controller function to generate authentication options export const generateAuthenticationOptionsCtrl = async (req, res) => { const { username } = req.query; const user = await User.findOne({ username }); if (!user) { return res .status(404) .send({ error: "User with this username does not exist" }); } const options = await generateAuthenticationOptions({ rpID: relyingPartyID, timeout: 60000, allowCredentials: user.authenticators.map((authenticator) => ({ id: base64URLStringToBuffer(authenticator.credentialID), transports: authenticator.transports, type: "public-key", })), userVerification: "preferred", }); req.session.challenge = options.challenge; res.send(options); };
Nach dem Login kopieren

The generateAuthenticationOptionsCtrl() controller starts by extracting the username from the request query and searching for the user in the database. If found, it proceeds to generate authentication options crucial for the process.

These options include the relying party ID (rpID), timeout, allowed credentials derived from stored authenticators, and user verification option set to preferred. Then, it stores the challenge from the options in the session for authentication verification and sends them as a response to the browser.

Let's add the controller for verifying the authenticator's response for the final part of the auth flow:

// Controller function to verify authentication export const verifyAuthenticationCtrl = async (req, res) => { const body = req.body; const { username } = req.query; const user = await User.findOne({ username }); if (!user) { return res .status(404) .send({ error: "User with this username does not exist" }); } const passKey = await PassKey.findOne({ user: user._id, credentialID: body.id, }); if (!passKey) { return res .status(400) .send({ error: "Could not find passkey for this user" }); } const expectedChallenge = req.session.challenge; let dbAuthenticator; // Check if the authenticator exists in the user's data for (const authenticator of user.authenticators) { if (authenticator.credentialID === body.id) { dbAuthenticator = authenticator; dbAuthenticator.credentialPublicKey = base64URLStringToBuffer( authenticator.credentialPublicKey ); break; } } // If the authenticator is not found, return an error if (!dbAuthenticator) { return res.status(400).send({ error: "This authenticator is not registered with this site", }); } let verification; try { const verificationOptions = { response: body, expectedChallenge: `${expectedChallenge}`, expectedOrigin: origin, expectedRPID: relyingPartyID, authenticator: dbAuthenticator, requireUserVerification: false, }; verification = await verifyAuthenticationResponse(verificationOptions); } catch (error) { console.error(error); return res.status(400).send({ error: error.message }); } const { verified, authenticationInfo } = verification; if (verified) { // Update the authenticator's counter in the DB to the newest count in the authentication dbAuthenticator.counter = authenticationInfo.newCounter; const filter = { username }; const update = { $set: { "authenticators.$[element].counter": authenticationInfo.newCounter, }, }; const options = { arrayFilters: [{ "element.credentialID": dbAuthenticator.credentialID }], }; await User.updateOne(filter, update, options); } // Clear the challenge from the session req.session.challenge = undefined; res.send({ verified, username: user.username }); };
Nach dem Login kopieren

The verifyAuthenticationCtrl() controller first extracts data from the request body and query, including the username and authentication details. It then searches for the user in the database. If not found, it returns a 404 error.

Assuming the user exists, it proceeds to find the passkey associated with the user and provides authentication details. If no passkey is found, it returns a 400 error.

Then, the expected challenge value is retrieved from the session data and iterates over the user's authenticators to find a match.

After attempting the verification, if an error occurs, the error is logged to the console and a 400 error is returned. If the verification is successful, the authenticator's counter is updated in the database, and the challenge is cleared from the session. Finally, the response includes the verification status and the username.

Return to your browser to ensure that everything functions as expected. Below is a GIF demonstrating the entire authentication process:Implementing WebAuthn for passwordless logins

We've successfully implemented the WebAuthn authentication, providing our users with a fast, secure, and password-less way to authenticate themselves. With biometric information or physical security keys, users can access their accounts securely.

Benefits and limitations of WebAuthn

While WebAuthn presents a solution to modern authentication challenges, it's essential to understand its strengths and weaknesses. Below, we highlight the key advantages and potential drawbacks of adopting WebAuthn in your authentication strategy.

Benefits of WebAuthn

WebAuthn offers a higher security level than traditional password-based authentication methods because of how it leverages public key cryptography to mitigate the risks associated with password breaches and phishing attacks.

So, even in the event of a cyber attack, perpetrators will only have access to your public key which, on its own, is insufficient to gain access to your account.

Support for various authentication factors like biometric data and physical security keys provides the kind of flexibility that allows you to implement multi-factor authentication for added security.

Since WebAuthn is currently supported by most modern web browsers and platforms, this makes it accessible to many users. The authentication experience is also the same across various devices and operating systems to ensure consistency.

Limitations of WebAuthn

Integrating WebAuthn can be technically challenging for organizations with complex or legacy systems. Then imagine all of the types of devices your users may be using and any other associated technical limitations.

Another important limitation is the human aspect — how accessible is the authentication process for your users? Unfamiliarity with the technology can either put users off or require creating education and instructional resources.

Conclusion

In this article, we've seen how WebAuthn provides a passwordless authentication process that uses public-key cryptography under the hood for a secure and convenient login experience. With a practical example and clear explanations, we've covered how to set up WebAuthn in a web application to enjoy a smoother and safer way to authenticate in our apps.


LogRocket: Debug JavaScript errors more easily by understanding the context

Debugging code is always a tedious task. But the more you understand your errors, the easier it is to fix them.

LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to see exactly what the user did that led to an error.

Implementing WebAuthn for passwordless logins

LogRocket records console logs, page load times, stack traces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!

Try it for free.

Das obige ist der detaillierte Inhalt vonImplementierung von WebAuthn für passwortlose Anmeldungen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Quelle:dev.to
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage
Über uns Haftungsausschluss Sitemap
Chinesische PHP-Website:Online-PHP-Schulung für das Gemeinwohl,Helfen Sie PHP-Lernenden, sich schnell weiterzuentwickeln!