パスワードなしのログインのための WebAuthn の実装

WBOY
リリース: 2024-08-29 13:07:17
オリジナル
407 人が閲覧しました

オゲネテガ・デネド著✏️

ユーザーにとって、パスワードを覚えて保存するのは非常に面倒な作業です。ログインが誰にとっても全体的に簡単になったらどうでしょうか。そこで、WebAuthn (Web Authentication API) が登場します。WebAuthn は、パスワードのない未来を提供することを目指しています。

この記事では、WebAuthn が機能する理由を説明し、WebAuthn が公開キー暗号化を使用して安全を保つ方法を詳しく説明します。また、API を実際に使用する方法を学ぶために、単純な Web アプリに WebAuthn を統合する方法についても説明します。

他のソリューションと同様、WebAuthn にも良い面とあまり良くない面があります。認証のニーズに最適かどうかを判断できるよう、その利点と欠点を確認します。パスワードに関する悩みに別れを告げ、WebAuthn によるシームレスなログイン エクスペリエンスの約束を探求してみませんか。

WebAuthn について学ぶ前に知っておくべきこと

WebAuthn を使用したパスワードなしのログインの実装について説明する前に、次の前提条件を満たしていることが重要です。

  • マシンにインストールされている Node.js
  • テスト目的で WebAuthn と互換性のある Android または iOS デバイス
  • Node.js と Express.js に関する基本的な知識
  • ユーザーの認証情報とパスキーを保存する MongoDB データベース

WebAuthn とは何か、そしてその仕組みをすでに理解している場合は、実装セクションに進んでください。復習が必要な場合は、以下を参考にして基礎を整えてください。

WebAuthnとは何ですか?

WebAuthn は、パスワード使用の主要な欠点に対処するために、Web アプリケーションでの安全でパスワードのない認証の必要性から開始された Web 標準です。

このプロジェクトは、FIDO (Fast Identity Online) と協力してワールド ワイド ウェブ コンソーシアム (W3C) によって公開され、ユーザーを認証するためにさまざまなデバイスやオペレーティング システムにわたって機能する標準化されたインターフェイスを作成することを目的としています。

実際のレベルでは、WebAuthn は、証明書利用者、WebAuthn クライアント、および認証子の 3 つの重要なコンポーネントで構成されています。

証明書利用者は、ユーザーの認証を要求するオンライン サービスまたはアプリケーションです。

WebAuthn クライアントは、ユーザーと証明書利用者の間の仲介者として機能します。これは、WebAuthn をサポートする互換性のある Web ブラウザーまたはモバイル アプリに組み込まれています。

認証システムは、指紋スキャナー、顔認識システム、ハードウェア セキュリティ キーなど、ユーザーの身元を確認するために使用されるデバイスまたは方法です。

WebAuthn はどのように機能しますか?

WebAuthn をサポートする Web サイトでアカウントに登録する場合は、携帯電話の指紋スキャナーなどの認証システムを使用するサインアップ プロセスを開始します。これにより、公開キーが生成され、証明書利用者のデータベースに保存され、秘密キーが安全なハードウェア層を介してデバイスに安全に保存されます。

Web サイトはログインしようとするときにパスワードを要求しません。実際に起こるのは、ログインの開始後にチャレンジがデバイスに送信されることです。通常、このチャレンジには、証明書利用者が期待する Web サイトからログインしていることを確認するための Web サイト アドレスなどの情報が含まれています。

Web サイトからチャレンジを受信した後、デバイスは秘密キーを使用して署名付き応答を作成します。この応答は、秘密キー自体は開示せずに、Web サイトによって保存されている対応する公開キーを所有していることを示します。

証明書利用者は、署名された応答を受信すると、保存されている公開鍵を検証します。署名が一致すると、Web サイトはあなたが本物のユーザーであることを確認し、アクセスを許可します。パスワードは交換されず、秘密キーはデバイス上に安全に残りました。

WebAuthn を使用してパスワードなしの認証を実装する方法

WebAuthn の基本的な概念を説明したので、これらすべてが実際にどのように機能するかを見てみましょう。私たちが構築するアプリケーションは、登録とログインを処理するためのいくつかの API エンドポイントを備えたシンプルな Express.js アプリです。ログインと登録フォームを含む基本的な HTML ページです。

プロジェクトのセットアップ

まず、GitHub からプロジェクトのクローンを作成する必要があります。これにはスターター コードが含まれているため、スキャフォールディングを行う必要はあまりありません。

ターミナルで以下のコマンドを入力します:

git clone https://github.com/josephden16/webauthn-demo.git
ログイン後にコピー

git checkout start-here # 注: starter ブランチ上にいることを確認してください

最終的なソリューションを表示したい場合は、final-solution または main ブランチにチェックインしてください。

次に、プロジェクトの依存関係をインストールします。

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 の統合は、複雑なシステムやレガシー システムを備えた組織にとって技術的に困難な場合があります。次に、ユーザーが使用している可能性のあるすべての種類のデバイスと、それに関連するその他の技術的制限を想像してください。

もう 1 つの重要な制限は人的側面です。ユーザーにとって認証プロセスはどの程度アクセスしやすいのでしょうか?このテクノロジーに慣れていないと、ユーザーが不安になったり、教育や指導リソースの作成が必要になったりする可能性があります。

結論

この記事では、安全で便利なログイン エクスペリエンスを実現するために、WebAuthn が内部で公開キー暗号化を使用するパスワードなしの認証プロセスを提供する方法について説明しました。実用的な例と明確な説明により、Web アプリケーションで WebAuthn を設定し、アプリでの認証をよりスムーズかつ安全に行う方法を説明しました。


LogRocket: コンテキストを理解することで JavaScript エラーをより簡単にデバッグします

コードのデバッグは常に面倒な作業です。しかし、間違いを理解すればするほど、修正が容易になります。

LogRocket を使用すると、これらのエラーを新しい独自の方法で理解できます。当社のフロントエンド監視ソリューションは、JavaScript フロントエンドに対するユーザーの関与を追跡し、エラーを引き起こしたユーザーの行動を正確に確認できるようにします。

Implementing WebAuthn for passwordless logins

LogRocket は、コンソール ログ、ページの読み込み時間、スタック トレース、ヘッダーと本文を含む遅いネットワーク リクエスト/レスポンス、ブラウザーのメタデータ、カスタム ログを記録します。 JavaScript コードの影響を理解するのがこれまでになく簡単になります!

無料でお試しください。

以上がパスワードなしのログインのための WebAuthn の実装の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ソース:dev.to
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート
私たちについて 免責事項 Sitemap
PHP中国語ウェブサイト:福祉オンライン PHP トレーニング,PHP 学習者の迅速な成長を支援します!