DEV Community

Deepak Gupta
Deepak Gupta

Posted on • Originally published at guptadeepak.com on

Implementing FIDO2 Authentication: A Developer's Step-by-Step Guide

  • Intro
  • Why FIDO2?
  • Implementation Overview
  • Step-by-Step Guide
  • Common Challenges & Solutions
  • Testing Your Implementation
  • Security Best Practices

Introduction to FIDO2 Authentication

Implementing FIDO2 Authentication: A Developer's Step-by-Step Guide

FIDO2 is the latest set of specifications from the FIDO Alliance, aiming to enable passwordless authentication. It comprises two main components:

  • WebAuthn API : A web standard published by the World Wide Web Consortium (W3C) that allows web applications to use public-key cryptography instead of passwords.
  • Client to Authenticator Protocol (CTAP): A protocol that enables an external authenticator (like a hardware security key) to communicate with the client (like a web browser).

Key Benefits of FIDO2:

  • Enhanced Security : Uses asymmetric cryptography, reducing the risk of credential theft.
  • Improved User Experience : Eliminates the need for passwords, making authentication seamless.
  • Phishing Resistance : Credentials are bound to specific origins, mitigating phishing attacks.

Why FIDO2?

Before diving into the implementation, let's understand why FIDO2 is worth your time:

No More Password Headaches

  • Zero password storage
  • No reset workflows needed
  • Reduced support costs

Superior Security

  • Phishing-resistant
  • Uses public key cryptography
  • Eliminates credential database risks

Better User Experience

  • Fast biometric authentication
  • No passwords to remember
  • Works across devices

Implementation Overview

Here's what we'll build:

  1. User registration with FIDO2 credentials
  2. Passwordless login using those credentials
  3. Secure session management

Implementing FIDO2 Authentication: A Developer's Step-by-Step Guide
FIDO Authentication Flow

What You'll Need

// Required packages for Node.js
npm install fido2-lib express body-parser

Enter fullscreen mode Exit fullscreen mode

Hardware Requirements

  • Authenticator Devices : FIDO2-compatible security keys (e.g., YubiKey 5 Series) or biometric devices like fingerprint scanners.
  • Development Machine : A computer capable of running a web server and accessing the internet.
  • Test Devices : Multiple browsers and devices for cross-platform testing.

Software Requirements

  • Programming Language : Knowledge of JavaScript for client-side and a server-side language like Node.js, Python, or Java.
  • Web Server : Apache, Nginx, or any server capable of handling HTTPS requests.
  • Databases : MySQL, PostgreSQL, MongoDB, or any database for storing user credentials.
  • Libraries and Frameworks :
    • Client-Side : Support for the WebAuthn API.
    • Server-Side : FIDO2 server libraries compatible with your programming language.

Dependencies and Tools

  • SSL Certificates : HTTPS is required for WebAuthn.
  • Browser Support : Latest versions of Chrome, Firefox, Edge, or Safari.
  • Development Tools : Code editor (e.g., Visual Studio Code), Postman for API testing.

Basic Architecture

┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ │ │ │ │ │
│ Browser │ ←──► │ Server │ ←──► │ Database │
│ (WebAuthn) │ │ (FIDO2Lib) │ │ │
│ │ │ │ │ │
└──────────────┘ └──────────────┘ └──────────────┘

Enter fullscreen mode Exit fullscreen mode

Step-by-Step Guide

1. Server Setup

First, let's set up our Express server with FIDO2 capabilities:

const express = require('express');
const { Fido2Lib } = require('fido2-lib');
const app = express();

// Initialize FIDO2
const f2l = new Fido2Lib({
  timeout: 60000,
  rpId: "example.com",
  rpName: "FIDO Example App",
  challengeSize: 32,
  attestation: "none"
});

app.use(express.json());

Enter fullscreen mode Exit fullscreen mode

2. Registration Endpoint

Create an endpoint to start the registration process:

app.post('/auth/register-begin', async (req, res) => {
  try {
    const user = {
      id: crypto.randomBytes(32),
      name: req.body.username,
      displayName: req.body.displayName
    };

    const registrationOptions = await f2l.attestationOptions();

    // Add user info to the options
    registrationOptions.user = user;
    registrationOptions.challenge = Buffer.from(registrationOptions.challenge);

    // Store challenge for verification
    req.session.challenge = registrationOptions.challenge;
    req.session.username = user.name;

    res.json(registrationOptions);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

Enter fullscreen mode Exit fullscreen mode

3. Client-Side Registration

Here's the frontend JavaScript to handle registration:

async function registerUser() {
  // 1. Get registration options from server
  const response = await fetch('/auth/register-begin', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ username: 'user@example.com' })
  });
  const options = await response.json();

  // 2. Create credentials using WebAuthn
  const credential = await navigator.credentials.create({
    publicKey: {
      ...options,
      challenge: base64ToBuffer(options.challenge),
      user: {
        ...options.user,
        id: base64ToBuffer(options.user.id)
      }
    }
  });

  // 3. Send credentials to server
  await fetch('/auth/register-complete', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      id: credential.id,
      rawId: bufferToBase64(credential.rawId),
      response: {
        attestationObject: bufferToBase64(
          credential.response.attestationObject
        ),
        clientDataJSON: bufferToBase64(
          credential.response.clientDataJSON
        )
      }
    })
  });
}

// Helper functions
function bufferToBase64(buffer) {
  return btoa(String.fromCharCode(...new Uint8Array(buffer)));
}

function base64ToBuffer(base64) {
  return Uint8Array.from(atob(base64), c => c.charCodeAt(0));
}

Enter fullscreen mode Exit fullscreen mode

4. Authentication Flow

Server-side authentication endpoint:

app.post('/auth/login-begin', async (req, res) => {
  try {
    const assertionOptions = await f2l.assertionOptions();

    // Get user's registered credentials from database
    const user = await db.getUser(req.body.username);
    assertionOptions.allowCredentials = user.credentials.map(cred => ({
      id: cred.credentialId,
      type: 'public-key'
    }));

    req.session.challenge = assertionOptions.challenge;
    req.session.username = req.body.username;

    res.json(assertionOptions);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

Enter fullscreen mode Exit fullscreen mode

Client-side authentication:

async function loginUser() {
  // 1. Get authentication options
  const response = await fetch('/auth/login-begin', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ username: 'user@example.com' })
  });
  const options = await response.json();

  // 2. Get assertion from authenticator
  const assertion = await navigator.credentials.get({
    publicKey: {
      ...options,
      challenge: base64ToBuffer(options.challenge),
      allowCredentials: options.allowCredentials.map(cred => ({
        ...cred,
        id: base64ToBuffer(cred.id)
      }))
    }
  });

  // 3. Verify with server
  await fetch('/auth/login-complete', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      id: assertion.id,
      rawId: bufferToBase64(assertion.rawId),
      response: {
        authenticatorData: bufferToBase64(
          assertion.response.authenticatorData
        ),
        clientDataJSON: bufferToBase64(
          assertion.response.clientDataJSON
        ),
        signature: bufferToBase64(
          assertion.response.signature
        )
      }
    })
  });
}

Enter fullscreen mode Exit fullscreen mode

Common Challenges & Solutions

1. Browser Compatibility

// Check if WebAuthn is supported
if (!window.PublicKeyCredential) {
  console.log('WebAuthn not supported');
  // Fall back to traditional authentication
  return;
}

// Check if user verifying platform authenticator is available
const available = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
if (!available) {
  console.log('Platform authenticator not available');
  // Consider security key instead
}

Enter fullscreen mode Exit fullscreen mode

2. Error Handling

// Client-side error handling
try {
  const credential = await navigator.credentials.create({/*...*/});
} catch (error) {
  switch (error.name) {
    case 'NotAllowedError':
      console.log('User declined to create credential');
      break;
    case 'SecurityError':
      console.log('Origin not secure');
      break;
    default:
      console.error('Unknown error:', error);
  }
}

Enter fullscreen mode Exit fullscreen mode

3. Base64 URL Encoding

function base64UrlEncode(buffer) {
  const base64 = bufferToBase64(buffer);
  return base64.replace(/\+/g, '-')
               .replace(/\//g, '_')
               .replace(/=/g, '');
}

Enter fullscreen mode Exit fullscreen mode

Testing Your Implementation

1. Basic Test Suite

describe('FIDO2 Authentication', () => {
  it('should generate registration options', async () => {
    const response = await fetch('/auth/register-begin', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username: 'test@example.com' })
    });
    const options = await response.json();

    expect(options).toHaveProperty('challenge');
    expect(options).toHaveProperty('rp');
    expect(options.rp.name).toBe('FIDO Example App');
  });
});

Enter fullscreen mode Exit fullscreen mode

2. Virtual Authenticator Testing

// Using Chrome's Virtual Authenticator Environment
const virtualAuthenticatorOptions = {
  protocol: 'ctap2',
  transport: 'internal',
  hasResidentKey: true,
  hasUserVerification: true,
  isUserConsenting: true
};

const authenticator = await driver.addVirtualAuthenticator(
  virtualAuthenticatorOptions
);

Enter fullscreen mode Exit fullscreen mode

Security Best Practices

  1. Always Use HTTPS
if (window.location.protocol !== 'https:') {
  throw new Error('FIDO2 requires HTTPS');
}

Enter fullscreen mode Exit fullscreen mode
  1. Validate Origin
const expectedOrigin = 'https://example.com';
const clientDataJSON = JSON.parse(
  new TextDecoder().decode(credential.response.clientDataJSON)
);
if (clientDataJSON.origin !== expectedOrigin) {
  throw new Error('Invalid origin');
}

Enter fullscreen mode Exit fullscreen mode
  1. Challenge Verification
if (!timingSafeEqual(
  storedChallenge,
  credential.response.challenge
)) {
  throw new Error('Challenge mismatch');
}

Enter fullscreen mode Exit fullscreen mode

Production Checklist

✅ HTTPS configured

✅ Error handling implemented

✅ Browser support detection

✅ Backup authentication method

✅ Rate limiting enabled

✅ Logging system in place

✅ Security headers configured

Next Steps

  1. Implement user presence verification
  2. Add transaction confirmation
  3. Set up backup authentication methods
  4. Configure audit logging
  5. Implement rate limiting

Resources:

Need help? Join Discord community for support.

Image of Timescale

Timescale – the developer's data platform for modern apps, built on PostgreSQL

Timescale Cloud is PostgreSQL optimized for speed, scale, and performance. Over 3 million IoT, AI, crypto, and dev tool apps are powered by Timescale. Try it free today! No credit card required.

Try free

Top comments (0)

Heroku

This site is powered by Heroku

Heroku was created by developers, for developers. Get started today and find out why Heroku has been the platform of choice for brands like DEV for over a decade.

Sign Up

👋 Kindness is contagious

Dive into an ocean of knowledge with this thought-provoking post, revered deeply within the supportive DEV Community. Developers of all levels are welcome to join and enhance our collective intelligence.

Saying a simple "thank you" can brighten someone's day. Share your gratitude in the comments below!

On DEV, sharing ideas eases our path and fortifies our community connections. Found this helpful? Sending a quick thanks to the author can be profoundly valued.

Okay