WebAuthn Demo

Welcome, !

You are now logged in.

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  

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 functionsimport {  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 websiteconst relyingPartyName = \\\"WebAuthn Demo\\\";// A unique identifier for your websiteconst relyingPartyID = \\\"localhost\\\";// The URL at which registrations and authentications should occurconst origin = `http://${relyingPartyID}`;// Controller function to generate registration optionsexport 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 registrationexport 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 fileapp.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   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 

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

const relyingPartyID = \\\"randomstring.tunnelmole.net\\\"; // use output from your terminalconst 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  

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 optionsexport 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 authenticationexport 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  

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

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.

","image":"http://www.luping.net/uploads/20241011/17286278456708c485bdf7d.png","datePublished":"2024-11-08T22:00:47+08:00","dateModified":"2024-11-08T22:00:47+08:00","author":{"@type":"Person","name":"luping.net","url":"https://www.luping.net/articlelist/0_1.html"}}
”工欲善其事,必先利其器。“—孔子《论语.录灵公》
首页 > 编程 > 实施 WebAuthn 以实现无密码登录

实施 WebAuthn 以实现无密码登录

发布于2024-11-08
浏览:397

Written by Oghenetega Denedo✏️

Remembering and storing passwords can be such a hassle for our users — imagine if logging in was overall easier for everyone. That's where WebAuthn, or Web Authentication API, comes in. WebAuthn aims to offer a future without passwords.

In this article, we'll cover what makes WebAuthn work, breaking down how it uses public key cryptography to keep things secure. We'll also guide you through integrating WebAuthn for a simple web app to learn how to use the API practically.

Like any solution, WebAuthn has its good and not-so-great sides. We'll review its advantages and disadvantages so you can determine if it's the best fit for your authentication needs. Come along as we attempt to say goodbye to password headaches and explore the promise of a seamless login experience with WebAuthn.

Things to know before learning about WebAuthn

Before we walk through implementing passwordless logins with WebAuthn, it's essential that you have the following prerequisites in place:

  • Node.js installed on your machine
  • An Android or iOS device that is compatible with WebAuthn for testing purposes
  • Basic familiarity with Node.js and Express.js
  • MongoDB database to store user credentials and passkeys

If you’re already familiar with what WebAuthn is and how it works, feel free to skip to the implementation section. If you feel like you need a refresher, then the below should help set the foundation straight.

What is WebAuthn?

WebAuthn is a web standard initiated out of the need for secure and passwordless authentication in web applications to tackle the major shortcomings of using passwords.

The project was published by the World Wide Web Consortium (W3C) in collaboration with the FIDO (Fast Identity Online) with the aim of creating a standardized interface that works across devices and operating systems for authenticating users.

On a practical level, WebAuthn is made up of three essential components: the relying party, the WebAuthn client, and the authenticator.

The relying party is the online service or application that requests authentication for the user.

The WebAuthn client acts as an intermediary between the user and the relying party — it’s embedded in any compatible web browser or mobile app that supports WebAuthn.

The authenticator is the device or method used to verify the user's identity, such as a fingerprint scanner, a facial recognition system, or a hardware security key.

How does WebAuthn work?

When registering for an account on a website supporting WebAuthn, you start a signup process that involves using an authenticator such as your fingerprint scanner on your phone. This results in generating a public key stored in the relying party’s database and a private key safely stored on your device via a secure hardware layer.

Since the website won’t request a password when attempting to log in. What really happens is that after initiating the log in a challenge is sent to your device. This challenge usually contains information like the website address to confirm that you are logging in from the website the relying party expects.

After receiving the challenge from the website, your device uses your private key to create a signed response. This response shows that you own the corresponding public key stored by the website without disclosing the private key itself.

The relying party validates the stored public key upon receiving your signed response. If the signature aligns, the website can ascertain that you are the real user and grants you access. No passwords were exchanged, and your private key remained securely on your device.

How to implement passwordless authentication with WebAuthn

Now that we've covered the fundamental concepts of WebAuthn, we can see how all this plays out in practice. The application we'll be building will be a simple Express.js app with a couple of API endpoints to handle the registration and login, a basic HTML page containing the login and registration form.

Project Setup

First, you'll need to clone the project from GitHub, which contains the starter code so we don't have much scaffolding to do.

In your terminal, enter the commands below:

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

git checkout start-here # note: make sure you're on the starter branch

If you want to view the final solution, check into the final-solution or main branch.

Next, install the project dependencies:

npm install

Next, create a new file, .env, at the project's root. Copy the contents of the .env.sample into it, and supply the appropriate values:

# .env
PORT=8000
MONGODB_URL=

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:




  WebAuthn Demo

WebAuthn Demo

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 

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.

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.

版本声明 本文转载于:https://dev.to/logrocket/implementing-webauthn-for-passwordless-logins-50o0?1如有侵犯,请联系[email protected]删除
最新教程 更多>
  • Bootstrap 4 Beta 中的列偏移发生了什么?
    Bootstrap 4 Beta 中的列偏移发生了什么?
    Bootstrap 4 Beta:列偏移的删除和恢复Bootstrap 4 在其 Beta 1 版本中引入了重大更改柱子偏移了。然而,随着 Beta 2 的后续发布,这些变化已经逆转。从 offset-md-* 到 ml-auto在 Bootstrap 4 Beta 1 中, offset-md-*...
    编程 发布于2024-11-15
  • 如何在 MySQL 中向日期字段添加一年?
    如何在 MySQL 中向日期字段添加一年?
    更新MySQL中的日期字段:添加一年在MySQL中,可以使用number=number 1来实现增量更新数值句法。但是,在使用日期字段时,需要采用不同的方法来添加特定的持续时间。向日期字段添加一年:增加日期字段一年后,您可以使用 DATE_ADD 函数(或使用 INTERVAL 的 ADDDATE)...
    编程 发布于2024-11-15
  • 大批
    大批
    方法是可以在对象上调用的 fns 数组是对象,因此它们在 JS 中也有方法。 slice(begin):将数组的一部分提取到新数组中,而不改变原始数组。 let arr = ['a','b','c','d','e']; // Usecase: Extract till index p...
    编程 发布于2024-11-15
  • 如何在 PHP 中将字符串拆分为不同的元素?
    如何在 PHP 中将字符串拆分为不同的元素?
    PHP:使用拆分分隔字符串当遇到由特定分隔符分隔的字符串时,将其拆分为不同的元素成为常见要求。在 PHP 中,可以使用 explode() 函数轻松完成此任务。例如,让我们考虑字符串“a.b”。使用带有分隔符“.”的explode(),可以将该字符串有效地拆分为两个不同的部分:$parts = ex...
    编程 发布于2024-11-15
  • 如何使用 MySQL 查找今天生日的用户?
    如何使用 MySQL 查找今天生日的用户?
    如何使用 MySQL 识别今天生日的用户使用 MySQL 确定今天是否是用户的生日涉及查找生日匹配的所有行今天的日期。这可以通过一个简单的 MySQL 查询来实现,该查询将存储为 UNIX 时间戳的生日与今天的日期进行比较。以下 SQL 查询将获取今天有生日的所有用户: FROM USERS ...
    编程 发布于2024-11-15
  • 在 Java 8 及更高版本中如何安全地将 Long 转换为 Int?
    在 Java 8 及更高版本中如何安全地将 Long 转换为 Int?
    在 Java 中安全地将 Long 转换为 Int(针对 Java 8 更新)在 Java 中,从 long 转换为 int 可能会导致在数据丢失中。为了解决这个问题,一种安全且惯用的方法是必不可少的。Java 8 的 Math.toIntExact() 方法在 Java 8 之前,推荐的安全强制转...
    编程 发布于2024-11-15
  • 标题可以是:

Python 异常处理:\',\' 和 \'as\' 之间有什么区别?
    标题可以是: Python 异常处理:\',\' 和 \'as\' 之间有什么区别?
    理解 Python 异常处理中 ',' 和 'as' 的区别使用 Python 的错误处理时,您可能会在 except 语句中遇到两种语法: ' 、”和“如”。本文旨在阐明这两种语法之间的区别并指导它们的正确使用。带有逗号的语法try: pass ex...
    编程 发布于2024-11-15
  • 当 PHPMyAdmin 中存在数据库时,为什么我在 PHP 中收到“未知数据库错误”?
    当 PHPMyAdmin 中存在数据库时,为什么我在 PHP 中收到“未知数据库错误”?
    当 PHPMyAdmin 中存在数据库时排除 PHP 中的“未知数据库错误”使用 PHP 连接 MySQL 数据库时,开发者可能会遇到即使 PHPMyAdmin 中存在数据库,也会出现“未知数据库错误”。此问题可能归因于多种因素。拼写错误彻底检查您在 PHP 代码中尝试连接的数据库名称。确保拼写正确...
    编程 发布于2024-11-15
  • 如何防止 Chrome 中页面加载时触发 CSS 转换?
    如何防止 Chrome 中页面加载时触发 CSS 转换?
    在页面加载时抑制 CSS 转换启动在某些情况下,CSS 转换可能会在页面加载期间无意中激活,从而导致元素闪烁。在元素上应用颜色过渡时可能会出现此问题。根据提供的示例:CSS: p.green { color: green; transition: color .2s; } p.green...
    编程 发布于2024-11-15
  • 在 C++ 中将参数传递给函数时,“std::forward”如何帮助保留原始引用类型?
    在 C++ 中将参数传递给函数时,“std::forward”如何帮助保留原始引用类型?
    使用 std::forward:精确转发参数将参数传递给 C 中的函数时,考虑使用的引用修饰符至关重要定义函数参数。 std::forward 的使用提供了处理参数引用的灵活性。std::forward 的优点在 C 0x 中,std::forward 用于显式地将参数移至函数。当函数接受通用引用 ...
    编程 发布于2024-11-15
  • 如何在 PHP 中提取网站预览?
    如何在 PHP 中提取网站预览?
    PHP 中的网页抓取:预览提取的分步指南在广阔的数字环境中导航时,我们经常遇到实例我们可能需要一种有效的方法从外部网页检索关键信息。在 Web 开发领域,抓取技术使我们能够自动化此过程,无缝提取特定数据点以用于分析或显示目的。一种流行的 Web 抓取编程语言是 PHP,它是一种广泛使用的服务器端脚本...
    编程 发布于2024-11-15
  • 如何在 PHP 中组合两个关联数组,同时保留唯一 ID 并处理重复名称?
    如何在 PHP 中组合两个关联数组,同时保留唯一 ID 并处理重复名称?
    在 PHP 中组合关联数组在 PHP 中,将两个关联数组组合成一个数组是一项常见任务。考虑以下请求:问题描述:提供的代码定义了两个关联数组,$array1和$array2。目标是创建一个新数组 $array3,它合并两个数组中的所有键值对。 此外,提供的数组具有唯一的 ID,而名称可能重合。要求是构...
    编程 发布于2024-11-15
  • 用于检查字典键的“has_key()”与“in”:在 Python 中应该使用哪个?
    用于检查字典键的“has_key()”与“in”:在 Python 中应该使用哪个?
    比较 Python 字典的 'has_key()' 和 'in'使用 Python 字典时,使用 'has_key()' 之间的选择()' 函数和用于键检查的 'in' 运算符出现。了解每种方法的差异和优点对于高效编写代码至关重...
    编程 发布于2024-11-15
  • 为什么我注入的 CSS 在我的内容脚本中不起作用?
    为什么我注入的 CSS 在我的内容脚本中不起作用?
    内容脚本中的 CSS 注入问题疑难解答通过内容脚本将自定义 CSS 注入网页可能是扩展浏览器功能的有用技术。但是,如果注入的 CSS 不可见或不应用,则可能会令人沮丧。本文旨在解决可能出现此问题的原因并提供潜在的解决方案。症状:您已将内容脚本配置为注入 CSS 文件,但它确实如此不会出现在目标网页上...
    编程 发布于2024-11-15
  • 如何确定特定表的MySQL引擎类型?
    如何确定特定表的MySQL引擎类型?
    确定特定表的 MySQL 引擎类型MySQL 数据库中拥有多个具有不同存储引擎的表可能导致需要确定特定表的引擎类型。要解决此问题:执行以下查询:SHOW TABLE STATUS WHERE Name = 'xxx'将 'xxx' 替换为您要检查的具体表名。分析结果:查询将返回包含各...
    编程 发布于2024-11-15

免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。

Copyright© 2022 湘ICP备2022001581号-3