# Step-by-Step Guide to WebAuthn-Based 2FA Using SimpleWebAuthn Library

## Introduction

WebAuthn is a modern authentication standard that enables passwordless and phishing-resistant login using biometrics (Face ID, fingerprints), security keys, or device PINs. It enhances security by eliminating reliance on traditional passwords, reducing the risk of credential theft and phishing attacks.

In this blog, I will guide you through integrating **SimpleWebAuthn** into your existing project with a step-by-step approach, making it easier to implement secure authentication.

### **How SimpleWebAuthn Works**

WebAuthn uses **public-key cryptography** and a **challenge-response mechanism** for secure authentication. Instead of passwords, users authenticate with **biometrics or security keys**, ensuring only legitimate users can log in.

#### **Registration (Credential Creation)**

During registration, the server generates a **challenge** and sends it to the frontend. The user verifies their identity using **biometrics, a security key, or a device PIN**. The authenticator (device or security key) then generates a **key pair** consisting of a **public key** and a **private key**. The **public key** and signed challenge are sent back to the server, where they are securely stored as the user’s credential.

#### **Authentication (Login)**

When a user attempts to log in, the server generates a **new challenge** and sends it to the frontend. The user verifies their identity using their previously registered authentication method. The authenticator **signs** the challenge using the **private key** stored on the device and returns the response. The server then verifies the signature using the stored **public key**. If the signature is valid, the user is authenticated successfully.

This process ensures that credentials are **bound to the user’s device** and cannot be phished or stolen like traditional passwords, making authentication significantly more secure.Prerequisites

Before starting, ensure you have:

* **A Node.js backend** (Express preferred)
    
* **A React frontend** (or any other frontend framework)
    
* **Basic knowledge of authentication** (JWT, sessions, etc.)
    

## Step 1: Install SimpleWebAuthn

To get started, install the required dependencies in your project.

### Backend Installation:

```plaintext
npm install @simplewebauthn/server
```

### Frontend Installation:

```plaintext
npm install @simplewebauthn/browser
```

## Step 2: Setting Up Backend API

Create a `.env` file inside the `server` folder and define your WebAuthn settings.

#### Deployment:

```plaintext
RP_ID=yourdomain.com  # Do not include 'http://', only the domain/subdomain name (e.g. example.com)
ORIGIN=http://example.com 
RP_NAME=random_name
```

#### Localhost:

```plaintext
RP_ID=localhost
ORIGIN=http://localhost:5173 # Your localhost frontend
RP_NAME=random_name
```

Now, in your `index.ts` (or `server.ts`) main file, import `'dotenv/config'` to load environment variables:

```typescript
import 'dotenv/config';

import express from "express;
const app=express();

app.use('/api/user,userRouter);
app.use('/api/passkey,passkeyRouter);

app.listen(3000,()=>console.log("Server listening on port 3000");
```

### Create Models

In `server/src/models`, define the necessary data models.

#### 1\. User Model

```typescript
import mongoose from "mongoose"

const userSchema = new mongoose.Schema<IUserDocument>({
  username: { type: String, required: true },
  email: { type: String, required: true, unique: true, match: [/^\S+@\S+\.\S+$/] },
  password: { type: String, required: true },
  twoFactorAuth: { type: boolean, default: false },
  passkey: [{ type: mongoose.Schema.Types.ObjectId, ref: "Passkey" }],
}, { timestamps: true });

export const User = mongoose.model<IUserDocument>("User", userSchema);
```

#### 2\. Challenge Model

```typescript
import mongoose from "mongoose"

const ChallengeSchema = new mongoose.Schema<IChallenge>(
  {
    userId: { type: Schema.Types.ObjectId, ref: "User", required: true },
    payload: { type: String, required: true },
    createdAt: { type: Date, default: Date.now, expires: 300 }, // 5 minutes
  },{ timestamps: true });

export const Challenge = mongoose.model<IChallenge>("Challenge",ChallengeSchema);
```

#### 3\. Passkey Model

```typescript
import mongoose from "mongoose"

const PasskeySchema = new mongoose.Schema<IPasskey>({
  userId: { type: Schema.Types.ObjectId, ref: "User", required: true },
  credentialID: { type: String, required: true, unique: true },
  publicKey: { type: Buffer, required: true },
  transports: { type: [String], enum: ["usb", "nfc", "ble", "internal", "hybrid"] },
  counter: { type: Number, required: true, default: 0 },
});

export const Passkey = mongoose.model<IPasskey>("Passkey", PasskeySchema);
```

### Create Controllers

In `server/src/controllers`, define define the necessary controllers.

#### 1\. Registration Passkey Endpoint

```typescript
export const registerPasskey = async (req: AuthRequest, res: Response) => {
  try {
    const userId: string = req._id!;
    const user: UserInfo = await User.findById(userId);
    const userPasskeys: IPasskey[] = await Passkey.find({ userId }); 
    //Return an error if the user or passkey is missing.

    const options: PublicKeyCredentialCreationOptionsJSON =
      await generateRegistrationOptions({
        rpName: process.env.RP_NAME as string,
        rpID: process.env.RP_ID as string,
        userID: isoUint8Array.fromUTF8String(userId),
        userName: user.username,
        excludeCredentials: userPasskeys.map((passkey) => ({
          id: passkey.credentialID,
        })),
      }); // Return an error if registration fails.

    await Challenge.create({ userId, payload: options.challenge });
    res.status(200).json({ options, success: true });
  } catch (error) {}
};
```

#### 2\. Verification Passkey Endpoint

```typescript
export const verifyPasskey = async (req: AuthRequest, res: Response) => {
  try {
    const { credential }: { credential: RegistrationResponseJSON } = req.body;
    const userId: string = req._id!;
    const user: UserInfo = await User.findById(userId);
    const challenge: ChallengeInfo = await Challenge.findOne({ userId });
    //Return an error if the user or challenge is missing.

    const verificationResult: VerifiedRegistrationResponse =
      await verifyRegistrationResponse({
        response: credential,
        expectedChallenge: challenge.payload,
        expectedOrigin: process.env.ORIGIN!,
        expectedRPID: process.env.RP_ID!,
      }); // Return an error if verification fails.

    const {id,publicKey,counter,transports} = verificationResult.registrationInfo.credential;

    const passkey: IPasskey = await Passkey.create({
      userId,
      credentialID: id,
      publicKey: Buffer.from(publicKey),
      counter: counter,
      transports: transports,
    });

    await Challenge.findOneAndDelete({ userId });
    user.passkeys?.push(passkey.id);
    await user.save();

    res.status(200).json({ message: "Registration successful", success: true });
  } catch (error) {}
};
```

#### 3\. Login Passkey Endpoint

```typescript
export const loginWithPasskey = async (req: AuthRequest, res: Response) => {
  try {
    const userId: string = req._id!;
    const user: UserInfo = await User.findById(userId);
    const userPasskeys: IPasskey[] = await Passkey.find({ userId }); 
    //Return an error if the user or passkey is missing.

    const options: PublicKeyCredentialRequestOptionsJSON =
      await generateAuthenticationOptions({
        rpID: process.env.RP_ID!,
        allowCredentials: userPasskeys.map((passkey) => ({
          id: passkey.credentialID,
          transports: passkey.transports,
        })),
      }); // Return an error if authentication fails.

    await Challenge.create({ userId, payload: options.challenge });
    res.status(200).json({ options, success: true });
  } catch (error) {}
};
```

#### 4\. Login Verification Passkey Endpoint

```typescript
export const verifyWithPasskey = async (req: AuthRequest, res: Response) => {
  try {
    const { credential }: { credential: AuthenticationResponseJSON } = req.body;
    const userId = req._id!;
    const user: UserInfo = await User.findById(userId); 
    const challenge: ChallengeInfo = await Challenge.findOne({ userId });
    const passkey: PasskeyInfo = await Passkey.findOne({ userId, credentialID: credential.id });
    ////Return an error if the user, passkey or challenge is missing.

    const verificationResult: VerifiedAuthenticationResponse =
      await verifyAuthenticationResponse({
        expectedChallenge: challenge.payload,
        expectedOrigin: process.env.ORIGIN!,
        expectedRPID: process.env.RP_ID!,
        response: credential,
        credential: {
          id: passkey.credentialID,
          publicKey: new Uint8Array(Buffer.from(passkey.publicKey)),
          counter: passkey.counter,
          transports: passkey.transports,
        },
      });  // Return an error if verification fails.

    if (!verificationResult.verified) return res.status(400).json({ message: "Authentication failed", success: false });
    
    // Prevent replay attacks using counter
    if ( verificationResult.authenticationInfo?.newCounter > 0 && verificationResult.authenticationInfo.newCounter <= passkey.counter ){
      return res.status(400).json({ message: "Counter replay detected", success: false });
    }
    // Update stored counter with the new counter from the authenticator
    passkey.counter = verificationResult.authenticationInfo.newCounter;
    await passkey.save();

    await Challenge.findOneAndDelete({ userId });
    const token: string = await user.generateToken();

    res.status(200).cookie("token", token ).json({ message: "Login successful with passkey", success: true });
  } catch (error) {}
};
```

### Create Routes

In `server/src/routes`, define the required routes.

```typescript
import express from "express";
import { isAuthenticated } from "../middlewares/auth";
import {loginWithPasskey,registerPasskey,verifyPasskey,verifyWithPasskey} from "../controllers/passkey.controller";

const router = express.Router();

router.route("/register").post(isAuthenticated, registerPasskey);
router.route("/verify").post(isAuthenticated, verifyPasskey);
router.route("/login-passkey").post(isAuthenticated, loginWithPasskey);
router.route("/verify-passkey").post(isAuthenticated, verifyWithPasskey);

export default router;
```

## Step 3: Implementing WebAuthn on Frontend

### 1.Login With Passkey

```typescript
  const handleRegisterPasskey = async () => {
    try {
      const data = await fetch("/api/passkey/register", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
      });
      const response = await data.json();

      const registerPasskey = await startRegistration({ optionsJSON: response.options });
      if (!registerPasskey) return toast.error("Invalid while register passkey");

      const verifyPasskey = await fetch("/api/passkey/verify", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ credential: registerPasskey }),
      });

      if (verifyPasskey) return toast.success("Passkey register successfully.");
    } catch (error) {}
  };
```

### 2.Verify With Passkey

```typescript
  const handleLoginWithPasskey = async ({ message, user }: { message: string; user: UserInfo;}) => {
    const data = await fetch("/api/passkey/login-passkey", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
    });
    const response = await data.json();

    const loginPasskey = await startAuthentication({ optionsJSON: response.options });
    if (!loginPasskey) return toast.error("Invalid while login passkey");

    const verifyPasskeyResponse = await fetch("/api/passkey/verify-passkey", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ credential: loginPasskey }),
    });
    const verifyPasskey = await verifyPasskeyResponse.json();

    if (verifyPasskey.success) { //change according to your code base
      toast.success(message);
      dispatch(setUser(user)); 
      navigate("/");
    }
  };
```

---

## Videos and Screenshots

[https://drive.google.com/drive/folders/1VhzQiVkNvrmzy3Na49BtLJ0i7bKd-dym?usp=sharing](https://drive.google.com/drive/folders/1VhzQiVkNvrmzy3Na49BtLJ0i7bKd-dym?usp=sharing)

---

## Conclusion

By following these steps, you have successfully integrated WebAuthn authentication using SimpleWebAuthn. This enhances security by enabling **passwordless authentication**, improving user experience, and reducing phishing risks.

For more details on **SimpleWebAuthn**, check the [**official documentation**](https://simplewebauthn.dev/). If you face issues, explore **community discussions** or connect with **WebAuthn developers** for support.
