首頁  >  文章  >  web前端  >  實作 WebAuthn 以實現無密碼登入

實作 WebAuthn 以實現無密碼登入

WBOY
WBOY原創
2024-08-29 13:07:17410瀏覽

作者 Oghenetega Denedo✏️

記住和儲存密碼對我們的用戶來說可能是一件很麻煩的事情——想像一下,如果登入對每個人來說都更容易的話。這就是 WebAuthn(或 Web 驗證 API)的用武之地。 WebAuthn 旨在提供一個沒有密碼的未來。

在本文中,我們將介紹 WebAuthn 的工作原理,詳細介紹它如何使用公鑰加密技術來確保安全。我們還將指導您將 WebAuthn 整合到一個簡單的 Web 應用程式中,以了解如何實際使用該 API。

與任何解決方案一樣,WebAuthn 有其好的一面和不太好的一面。我們將回顧其優點和缺點,以便您確定它是否最適合您的身份驗證需求。歡迎與我們一起嘗試告別令人頭痛的密碼問題,並探索 WebAuthn 無縫登入體驗的前景。

學習 WebAuthn 之前需要了解的事項

在我們逐步使用 WebAuthn 實作無密碼登入之前,您必須符合以下先決條件:

  • Node.js 安裝在您的電腦上
  • 與 WebAuthn 相容於測試目的的 Android 或 iOS 裝置
  • 基本上熟悉 Node.js 和 Express.js
  • 用於儲存使用者憑證和密碼的 MongoDB 資料庫

如果您已經熟悉 WebAuthn 是什麼及其工作原理,請隨時跳到實作部分。如果您覺得需要複習一下,那麼以下內容應該有助於奠定基礎。

什麼是 WebAuthn?

WebAuthn 是一個 Web 標準,出於 Web 應用程式中安全和無密碼身份驗證的需要而發起,以解決使用密碼的主要缺點。

該專案由萬維網聯盟 (W3C) 與 FIDO(快速身份線上)合作發布,旨在創建一個跨裝置和作業系統的標準化介面,用於對使用者進行身份驗證。

在實務層面上,WebAuthn 由三個基本元件組成:依賴方、WebAuthn 用戶端和驗證器。

依賴方是請求使用者驗證的線上服務或應用程式。

WebAuthn 用戶端充當使用者和依賴方之間的中介 - 它嵌入在任何支援 WebAuthn 的相容 Web 瀏覽器或行動應用程式中。

身份驗證器是用於驗證使用者身分的裝置或方法,例如指紋掃描器、臉部辨識系統或硬體安全金鑰。

WebAuthn 是如何運作的?

在支援 WebAuthn 的網站上註冊帳戶時,您將開始一個註冊過程,其中涉及使用身份驗證器,例如手機上的指紋掃描器。這會產生儲存在依賴方資料庫中的公鑰和透過安全硬體層安全地儲存在您的裝置上的私鑰。

由於網站在嘗試登入時不會要求密碼。真正發生的是,在啟動登入後,挑戰會發送到您的裝置。此質詢通常包含網站地址等信息,以確認您正在從依賴方期望的網站登入。

收到來自網站的質詢後,您的裝置將使用您的私鑰建立簽署回應。此回應表明您擁有網站儲存的相應公鑰,而無需洩露私鑰本身。

依賴方在收到您簽署的回應後驗證儲存的公鑰。如果簽名一致,網站可以確定您是真正的使用者並授予您存取權限。沒有交換密碼,您的私鑰安全地保留在您的裝置上。

如何使用WebAuthn實作無密碼身份驗證

現在我們已經介紹了 WebAuthn 的基本概念,我們可以看到這一切在實踐中如何發揮作用。我們將建立的應用程式將是一個簡單的 Express.js 應用程序,帶有幾個 API 端點來處理註冊和登錄,這是一個包含登入和註冊表單的基本 HTML 頁面。

項目設定

首先,您需要從 GitHub 複製項目,其中包含起始程式碼,因此我們不需要做太多腳手架。

在您的終端機中,輸入以下命令:

git clone https://github.com/josephden16/webauthn-demo.git

git checkout start-here # 注意:確保您位於啟動分支

如果您想查看最終解決方案,請查看最終解決方案或主分支。

接下來,安裝專案相依性:

npm install

接下來,在專案的根目錄中建立一個新檔案 .env。將 .env.sample 的內容複製到其中,並提供適當的值:

# .env
PORT=8000
MONGODB_URL=<YOUR MONGODB CONNECTION STRING>

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

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:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>WebAuthn Demo</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">
  <script src="https://cdn.tailwindcss.com"></script>
  <script src="https://unpkg.com/@simplewebauthn/browser/dist/bundle/index.umd.min.js"></script>
  <style>
    .font-inter {
      font-family: 'Inter', sans-serif;
    }
  </style>
</head>

<body class="font-inter min-h-screen flex flex-col items-center p-24">
  <h1 class="font-bold text-3xl mb-10">WebAuthn Demo</h1>
  <div id="content">
    <div>
      <div id="error" role="alert"
        class="bg-red-600 text-white p-2 w-full my-3 rounded-md text-center font-bold hidden"></div>
      <div id="loginForm" class="text-center">
        <input id="username" autocomplete="webauthn" type="text" placeholder="Username"
          class="w-[20rem] px-4 py-2 mb-4 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
        <div class="flex justify-center space-x-4">
          <button id="registerBtn" type="button"
            class="bg-green-500 hover:bg-green-600 text-white font-semibold py-2 px-4 rounded-md">Register</button>
          <button id="loginBtn" type="button"
            class="bg-blue-500 hover:bg-blue-600 text-white font-semibold py-2 px-4 rounded-md">Login</button>
        </div>
      </div>
      <div id="welcomeMessage" class="hidden text-center">
        <h1 class="text-3xl font-bold mb-4">Welcome, <span id="usernameDisplay"></span>!</h1>
        <p class="text-lg text-gray-600">You are now logged in.</p>
      </div>
    </div>
  </div>
</body>

</html>

So, we've just added a simple login and registration form for users to sign in with WebAuthn. Also, if you check the element, 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);
});

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!";
  }
}

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;

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;

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;

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);
};

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 });
};

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()`

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 logins   To 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\""
}

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

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

tmole <port>

You should see an output like this from your terminal if it was successful: Implementing WebAuthn for passwordless logins You 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

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";
  }
}

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);

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);
};

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 });
};

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.

因此,即使發生網路攻擊,犯罪者也只能存取您的公鑰,而公鑰本身不足以存取您的帳戶。

對生物識別資料和實體安全金鑰等各種身份驗證因素的支援提供了靈活性,使您可以實施多因素身份驗證以提高安全性。

由於 WebAuthn 目前受到大多數現代 Web 瀏覽器和平台的支持,因此許多用戶都可以使用它。各種裝置和作業系統之間的身份驗證體驗也是相同的,以確保一致性。

WebAuthn 的局限性

對於擁有複雜或遺留系統的組織來說,整合 WebAuthn 在技術上可能具有挑戰性。然後想像一下您的用戶可能使用的所有類型的設備以及任何其他相關的技術限制。

另一個重要的限制是人為因素-使用者的身份驗證過程有多容易存取?不熟悉該技術可能會讓使用者望而卻步,或需要創建教育和教學資源。

結論

在本文中,我們了解了 WebAuthn 如何提供無密碼身份驗證過程,該過程在幕後使用公鑰加密技術來提供安全便捷的登入體驗。透過實際範例和清晰的解釋,我們介紹瞭如何在 Web 應用程式中設定 WebAuthn,以便在我們的應用程式中享受更流暢、更安全的身份驗證方式。


LogRocket:透過了解上下文更輕鬆地調試 JavaScript 錯誤

偵錯程式碼總是一項乏味的任務。但你越了解自己的錯誤,就越容易糾正它們。

LogRocket 讓您以新的、獨特的方式理解這些錯誤。我們的前端監控解決方案追蹤使用者與 JavaScript 前端的互動,使您能夠準確查看使用者的操作導致了錯誤。

Implementing WebAuthn for passwordless logins

LogRocket 記錄控制台日誌、頁面載入時間、堆疊追蹤、帶有標頭 + 正文的慢速網路請求/回應、瀏覽器元資料和自訂日誌。了解 JavaScript 程式碼的影響從未如此簡單!

免費試用。

以上是實作 WebAuthn 以實現無密碼登入的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn