Goodbye Passwords: Implementing Passkeys (WebAuthn) with Node.js and SimpleWebAuthn

Development tutorial - IT technology blog
Development tutorial - IT technology blog

The Burden of “Forgotten Passwords”

Have you ever calculated how much time users spend just resetting their passwords? For developers, managing passwords isn’t easy either. We have to worry about brute-force protection, phishing prevention, and choosing between Bcrypt or Argon2 to hash data as securely as possible.

In reality, traditional passwords are becoming obsolete. No matter how complex the requirements, users still tend to reuse old passwords or easily fall for phishing sites. Passkeys (based on WebAuthn) have emerged as a definitive solution. They allow users to log in using fingerprints, FaceID, or device PINs without ever sending sensitive strings to the server.

Why are Passkeys Significantly More Secure?

This mechanism is based on Public Key Cryptography. Your server only stores the Public Key. When a user logs in, their device uses a Private Key stored within a secure chip to sign an authentication message.

According to reports from the FIDO Alliance, systems using Passkeys can reduce login times by up to 50%. In our team’s latest project, the results were impressive. Even if a hacker breaches the database, it’s useless because the Private Key remains on the customer’s phone.

Comparison of Authentication Methods

Criteria Traditional Passwords 2FA (OTP/SMS) Passkey (WebAuthn)
Experience Poor (must remember and type) Annoying (wait for SMS, enter code) Seamless (fingerprint/FaceID touch)
Phishing Protection No Partial Absolute
Implementation Difficulty Low Medium Medium

Environment Setup

For the fastest implementation, I recommend using the @simplewebauthn library. It handles the conversion of binary data to JSON very cleanly.

Start by initializing the project:

mkdir node-passkey-demo
cd node-passkey-demo
npm init -y
npm install express @simplewebauthn/server @simplewebauthn/browser express-session

Practical Implementation Steps

1. Passkey Registration

First, the server needs to generate a set of Options to send to the Client. The most important part is the challenge. This is a random string used to prevent replay attacks.

const { generateRegistrationOptions } = require('@simplewebauthn/server');

app.get('/generate-registration-options', (req, res) => {
  const options = generateRegistrationOptions({
    rpName: 'My App',
    rpID: 'localhost',
    userID: 'user123',
    userName: '[email protected]',
    attestationType: 'none',
    authenticatorSelection: {
      residentKey: 'required',
      userVerification: 'preferred',
    },
  });

  req.session.currentChallenge = options.challenge;
  res.json(options);
});

In the browser, you trigger the operating system’s authentication dialog with the following code:

import { startRegistration } from '@simplewebauthn/browser';

async function register() {
  const resp = await fetch('/generate-registration-options');
  const options = await resp.json();

  // Browser displays fingerprint/FaceID scan
  const regResp = await startRegistration(options);

  await fetch('/verify-registration', {
    method: 'POST',
    body: JSON.stringify(regResp),
    headers: { 'Content-Type': 'application/json' }
  });
}

2. Verification and Key Storage

Once the user finishes scanning their fingerprint, the server must verify that signature. If valid, store the PublicKey and CredentialID in the database for future logins.

const { verifyRegistrationResponse } = require('@simplewebauthn/server');

app.post('/verify-registration', async (req, res) => {
  const verification = await verifyRegistrationResponse({
    response: req.body,
    expectedChallenge: req.session.currentChallenge,
    expectedOrigin: 'http://localhost:3000',
    expectedRPID: 'localhost',
  });

  if (verification.verified) {
    // Save registrationInfo.credentialPublicKey to DB
    res.status(200).send({ ok: true });
  }
});

Practical Lessons from Implementation

While working with WebAuthn, I’ve noted three important considerations:

  • HTTPS is mandatory: WebAuthn will refuse to operate over HTTP. The only exception is localhost for easy debugging.
  • Don’t parse data manually: The returned keys are often in a difficult ArrayBuffer format. Rely on a library to avoid unfortunate security logic errors.
  • Always have a backup plan: Don’t rush to remove passwords immediately. Implement Passkeys as a parallel option so users can gradually adapt.

Are Passkeys Worth the Investment?

If you are building financial apps, SaaS, or e-commerce platforms, Passkeys are a major step forward. They don’t just upgrade security; they create a premium login experience.

I tried applying this to a landing page and saw a significant increase in user retention. Simply put, they no longer dread typing long strings on a phone screen. A passwordless future is right in front of us, and mastering it now will be a major advantage for your career.

Share: