WebAuthn/Passkey Implementation Guide

This document covers the WebAuthn (Web Authentication) and Passkey implementation in the PVPipe ms-auth service, providing modern passwordless authentication for web applications.

Table of Contents

  1. WebAuthn Overview
  2. Architecture Design
  3. Implementation Components
  4. API Endpoints
  5. Database Schema
  6. Security Considerations
  7. Client Integration
  8. Testing and Validation
  9. Deployment Guidelines

WebAuthn Overview

What is WebAuthn?

WebAuthn (Web Authentication API) is a W3C standard that enables strong, passwordless authentication using public-key cryptography. It allows users to authenticate using:

  • Platform authenticators: Built-in biometrics (TouchID, FaceID, Windows Hello)
  • External authenticators: Security keys (YubiKey, Titan Security Key)
  • Passkeys: Synced credentials across devices via cloud providers

Benefits

  • Security: Phishing-resistant, cryptographically secure
  • User Experience: No passwords to remember or type
  • Privacy: Biometric data never leaves the device
  • Cross-platform: Works across different devices and browsers

Use Cases in PVPipe

  1. Primary Authentication: Replace username/password login
  2. Step-up Authentication: Additional verification for sensitive operations
  3. Cross-device Authentication: Seamless login across devices
  4. Recovery Authentication: Alternative when mobile biometric is unavailable

Architecture Design

WebAuthn Flow

┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│   Web Browser   │    │   ms-auth API    │    │   Authenticator │
└─────────────────┘    └──────────────────┘    └─────────────────┘
         │                       │                       │
    1.   │ Begin Registration    │                       │
         │──────────────────────►│                       │
         │                       │                       │
    2.   │◄──────────────────────│ PublicKeyCredential   │
         │       Options         │      Creation         │
         │                       │                       │
    3.   │ navigator.credentials │                       │
         │   .create(options)    │                       │
         │──────────────────────────────────────────────►│
         │                       │                       │
    4.   │◄──────────────────────────────────────────────│
         │           Attestation Response                 │
         │                       │                       │
    5.   │ Complete Registration │                       │
         │──────────────────────►│                       │
         │                       │                       │
    6.   │◄──────────────────────│ Registration Success  │
         │                       │                       │

Authentication Flow

┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│   Web Browser   │    │   ms-auth API    │    │   Authenticator │
└─────────────────┘    └──────────────────┘    └─────────────────┘
         │                       │                       │
    1.   │ Begin Authentication  │                       │
         │──────────────────────►│                       │
         │                       │                       │
    2.   │◄──────────────────────│ PublicKeyCredential   │
         │       Options         │      Request          │
         │                       │                       │
    3.   │ navigator.credentials │                       │
         │    .get(options)      │                       │
         │──────────────────────────────────────────────►│
         │                       │                       │
    4.   │◄──────────────────────────────────────────────│
         │           Assertion Response                   │
         │                       │                       │
    5.   │ Complete Login        │                       │
         │──────────────────────►│                       │
         │                       │                       │
    6.   │◄──────────────────────│ JWT Tokens            │
         │                       │                       │

Implementation Components

WebAuthnService

Core service handling WebAuthn operations:

type WebAuthnService struct {
    db          *sql.DB
    redis       *redis.Client
    auditSvc    *AuditService
    rpID        string
    rpName      string
    rpOrigin    string
}

type WebAuthnCredential struct {
    ID            string    `db:"id"`
    UserID        int64     `db:"user_id"`
    CredentialID  []byte    `db:"credential_id"`
    PublicKey     []byte    `db:"public_key"`
    Algorithm     int       `db:"algorithm"`
    SignCount     uint32    `db:"sign_count"`
    Transports    []string  `db:"transports"`
    Name          string    `db:"name"`
    CreatedAt     time.Time `db:"created_at"`
    LastUsedAt    *time.Time `db:"last_used_at"`
}

// Registration methods
func (w *WebAuthnService) BeginRegistration(ctx context.Context, userID int64, username string, displayName string) (*protocol.CredentialCreation, error)
func (w *WebAuthnService) FinishRegistration(ctx context.Context, userID int64, credential *protocol.ParsedCredentialCreationData) (*WebAuthnCredential, error)

// Authentication methods  
func (w *WebAuthnService) BeginAuthentication(ctx context.Context, userID int64) (*protocol.CredentialAssertion, error)
func (w *WebAuthnService) BeginAuthenticationByEmail(ctx context.Context, email string) (*protocol.CredentialAssertion, error)
func (w *WebAuthnService) FinishAuthentication(ctx context.Context, userID int64, credential *protocol.ParsedCredentialAssertionData) (*TokenResponse, error)

Challenge Management

WebAuthn challenges are stored temporarily with security controls:

type WebAuthnChallenge struct {
    ID            string                 `json:"id"`
    UserID        int64                  `json:"user_id"`
    Challenge     []byte                 `json:"challenge"`
    Type          string                 `json:"type"` // "registration" or "authentication"
    Options       interface{}            `json:"options"`
    ExpiresAt     time.Time             `json:"expires_at"`
    IPAddress     string                `json:"ip_address"`
    UserAgent     string                `json:"user_agent"`
}

func (w *WebAuthnService) storeChallengeSession(ctx context.Context, challenge *WebAuthnChallenge) error {
    data, err := json.Marshal(challenge)
    if err != nil {
        return err
    }
    
    key := fmt.Sprintf("webauthn:challenge:%s", challenge.ID)
    ttl := time.Until(challenge.ExpiresAt)
    
    return w.redis.Set(ctx, key, data, ttl).Err()
}

func (w *WebAuthnService) getChallengeSession(ctx context.Context, challengeID string) (*WebAuthnChallenge, error) {
    key := fmt.Sprintf("webauthn:challenge:%s", challengeID)
    data, err := w.redis.Get(ctx, key).Result()
    if err != nil {
        return nil, err
    }
    
    var challenge WebAuthnChallenge
    err = json.Unmarshal([]byte(data), &challenge)
    return &challenge, err
}

Credential Management

Managing WebAuthn credentials with proper security:

func (w *WebAuthnService) GetUserCredentials(ctx context.Context, userID int64) ([]WebAuthnCredential, error) {
    query := `
        SELECT id, user_id, credential_id, public_key, algorithm, sign_count, 
               transports, name, created_at, last_used_at
        FROM webauthn_credentials 
        WHERE user_id = $1 AND is_active = true
        ORDER BY last_used_at DESC NULLS LAST, created_at DESC
    `
    
    rows, err := w.db.QueryContext(ctx, query, userID)
    if err != nil {
        return nil, err
    }
    defer rows.Close()
    
    var credentials []WebAuthnCredential
    for rows.Next() {
        var cred WebAuthnCredential
        var transports string
        
        err := rows.Scan(
            &cred.ID, &cred.UserID, &cred.CredentialID, &cred.PublicKey,
            &cred.Algorithm, &cred.SignCount, &transports, &cred.Name,
            &cred.CreatedAt, &cred.LastUsedAt,
        )
        if err != nil {
            return nil, err
        }
        
        // Parse transports JSON
        json.Unmarshal([]byte(transports), &cred.Transports)
        credentials = append(credentials, cred)
    }
    
    return credentials, nil
}

func (w *WebAuthnService) UpdateSignCount(ctx context.Context, credentialID []byte, signCount uint32) error {
    query := `
        UPDATE webauthn_credentials 
        SET sign_count = $1, last_used_at = NOW(), updated_at = NOW()
        WHERE credential_id = $2
    `
    
    _, err := w.db.ExecContext(ctx, query, signCount, credentialID)
    return err
}

Security Validation

Comprehensive validation for WebAuthn operations:

func (w *WebAuthnService) validateRegistrationRequest(req *RegistrationRequest) error {
    // Validate credential name
    if len(req.Name) == 0 || len(req.Name) > 100 {
        return errors.New("credential name must be 1-100 characters")
    }
    
    // Validate user verification requirement
    if req.UserVerification != "" && 
       req.UserVerification != "required" && 
       req.UserVerification != "preferred" && 
       req.UserVerification != "discouraged" {
        return errors.New("invalid user verification requirement")
    }
    
    // Validate authenticator selection
    if req.AuthenticatorSelection != nil {
        if req.AuthenticatorSelection.ResidentKey != "" &&
           req.AuthenticatorSelection.ResidentKey != "required" &&
           req.AuthenticatorSelection.ResidentKey != "preferred" &&
           req.AuthenticatorSelection.ResidentKey != "discouraged" {
            return errors.New("invalid resident key requirement")
        }
    }
    
    return nil
}

func (w *WebAuthnService) validateOrigin(origin string) error {
    if origin != w.rpOrigin {
        return fmt.Errorf("invalid origin: expected %s, got %s", w.rpOrigin, origin)
    }
    return nil
}

func (w *WebAuthnService) validateRPID(rpID string) error {
    if rpID != w.rpID {
        return fmt.Errorf("invalid RP ID: expected %s, got %s", w.rpID, rpID)
    }
    return nil
}

API Endpoints

Registration Endpoints

POST /api/v1/webauthn/register/begin

Initiates WebAuthn credential registration.

Request:

{
  "name": "My Security Key",
  "userVerification": "preferred",
  "authenticatorSelection": {
    "residentKey": "preferred",
    "userVerification": "preferred",
    "authenticatorAttachment": "platform"
  }
}

Response:

{
  "data": {
    "challengeId": "webauthn_challenge_uuid",
    "publicKey": {
      "challenge": "base64-challenge",
      "rp": {
        "name": "PVPipe",
        "id": "pvpipe.com"
      },
      "user": {
        "id": "base64-user-id",
        "name": "user@example.com",
        "displayName": "John Doe"
      },
      "pubKeyCredParams": [
        {"type": "public-key", "alg": -7},
        {"type": "public-key", "alg": -257}
      ],
      "timeout": 300000,
      "attestation": "none",
      "authenticatorSelection": {
        "residentKey": "preferred",
        "userVerification": "preferred"
      }
    }
  }
}

POST /api/v1/webauthn/register/finish

Completes WebAuthn credential registration.

Request:

{
  "challengeId": "webauthn_challenge_uuid",
  "credential": {
    "id": "credential-id",
    "rawId": "base64-raw-id",
    "type": "public-key",
    "response": {
      "attestationObject": "base64-attestation",
      "clientDataJSON": "base64-client-data"
    }
  }
}

Response:

{
  "data": {
    "success": true,
    "credentialId": "credential-uuid",
    "credential": {
      "id": "credential-uuid",
      "name": "My Security Key",
      "algorithm": -7,
      "transports": ["internal"],
      "createdAt": "2025-08-02T15:30:00Z"
    }
  }
}

Authentication Endpoints

POST /api/v1/webauthn/login/begin

Initiates WebAuthn authentication.

Request:

{
  "email": "user@example.com",
  "userVerification": "preferred"
}

Response:

{
  "data": {
    "challengeId": "webauthn_login_challenge_uuid",
    "publicKey": {
      "challenge": "base64-challenge",
      "timeout": 300000,
      "rpId": "pvpipe.com",
      "allowCredentials": [
        {
          "type": "public-key",
          "id": "base64-credential-id",
          "transports": ["internal"]
        }
      ],
      "userVerification": "preferred"
    }
  }
}

POST /api/v1/webauthn/login/finish

Completes WebAuthn authentication.

Request:

{
  "challengeId": "webauthn_login_challenge_uuid",
  "credential": {
    "id": "credential-id",
    "rawId": "base64-raw-id",
    "type": "public-key",
    "response": {
      "authenticatorData": "base64-auth-data",
      "clientDataJSON": "base64-client-data",
      "signature": "base64-signature"
    }
  }
}

Response:

{
  "data": {
    "success": true,
    "tokens": {
      "accessToken": "jwt-access-token",
      "refreshToken": "jwt-refresh-token",
      "accessTokenExpiresAt": "2025-08-02T23:30:00Z",
      "refreshTokenExpiresAt": "2025-08-05T15:30:00Z"
    }
  }
}

Credential Management

GET /api/v1/webauthn/credentials

Lists user's WebAuthn credentials.

Response:

{
  "data": {
    "credentials": [
      {
        "id": "cred-uuid-1",
        "name": "My Security Key",
        "algorithm": -7,
        "transports": ["usb", "nfc"],
        "createdAt": "2025-08-01T10:00:00Z",
        "lastUsedAt": "2025-08-02T14:30:00Z"
      },
      {
        "id": "cred-uuid-2", 
        "name": "TouchID",
        "algorithm": -7,
        "transports": ["internal"],
        "createdAt": "2025-07-15T09:15:00Z",
        "lastUsedAt": "2025-08-02T08:45:00Z"
      }
    ]
  }
}

DELETE /api/v1/webauthn/credentials/{id}

Removes a WebAuthn credential.

Response:

{
  "data": {
    "success": true,
    "message": "Credential removed successfully"
  }
}

Utility Endpoints

POST /api/v1/auth/check-passkeys

Checks if a user has registered passkeys (public endpoint).

Request:

{
  "email": "user@example.com"
}

Response:

{
  "hasPasskeys": true
}

This endpoint is useful for frontend applications to determine whether to show passkey login options. It returns false for non-existent users to prevent user enumeration.

GET /api/v1/auth/passkey/prompt-status

Gets the passkey registration prompt status for the authenticated user.

Response:

{
  "promptStatus": "pending",
  "lastPrompted": null
}

Possible status values:

  • pending: User hasn't been prompted yet
  • accepted: User accepted and registered a passkey
  • declined: User explicitly declined registration
  • dismissed: User dismissed the prompt without deciding

PUT /api/v1/auth/passkey/prompt-status

Updates the passkey registration prompt status.

Request:

{
  "status": "declined"
}

Response:

{
  "success": true,
  "message": "Prompt status updated successfully"
}

These utility endpoints enable better UX by:

  • Checking passkey availability before attempting authentication
  • Managing user prompts to avoid annoying repeated registration requests
  • Tracking user engagement with passkey features

PUT /api/v1/webauthn/credentials/{id}

Updates credential name.

Request:

{
  "name": "New Credential Name"
}

Database Schema

WebAuthn Tables

webauthn_credentials

Stores WebAuthn credential information:

CREATE TABLE webauthn_credentials (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    credential_id BYTEA NOT NULL UNIQUE,
    public_key BYTEA NOT NULL,
    algorithm INTEGER NOT NULL, -- COSE Algorithm Identifier
    sign_count INTEGER NOT NULL DEFAULT 0,
    transports TEXT[] NOT NULL DEFAULT '{}',
    aaguid UUID,
    name VARCHAR(100) NOT NULL,
    is_active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    last_used_at TIMESTAMP WITH TIME ZONE
);

CREATE INDEX idx_webauthn_credentials_user_active ON webauthn_credentials(user_id, is_active);
CREATE INDEX idx_webauthn_credentials_credential_id ON webauthn_credentials(credential_id);
CREATE INDEX idx_webauthn_credentials_last_used ON webauthn_credentials(last_used_at DESC);

webauthn_challenges

Temporary challenge storage (alternative to Redis):

CREATE TABLE webauthn_challenges (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
    challenge BYTEA NOT NULL,
    challenge_type VARCHAR(20) NOT NULL CHECK (challenge_type IN ('registration', 'authentication')),
    options JSONB,
    ip_address VARCHAR(45),
    user_agent TEXT,
    expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_webauthn_challenges_expires ON webauthn_challenges(expires_at);
CREATE INDEX idx_webauthn_challenges_user_type ON webauthn_challenges(user_id, challenge_type, created_at);

-- Cleanup expired challenges
CREATE OR REPLACE FUNCTION cleanup_expired_webauthn_challenges()
RETURNS void AS $
BEGIN
    DELETE FROM webauthn_challenges WHERE expires_at < NOW();
END;
$ LANGUAGE plpgsql;

-- Schedule cleanup (adjust based on your job scheduler)
-- SELECT cron.schedule('cleanup-webauthn-challenges', '*/5 * * * *', 'SELECT cleanup_expired_webauthn_challenges();');

webauthn_audit_logs

Audit trail for WebAuthn operations:

CREATE TABLE webauthn_audit_logs (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    event_type VARCHAR(50) NOT NULL, -- 'registration', 'authentication', 'credential_delete'
    user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
    credential_id BYTEA,
    success BOOLEAN NOT NULL,
    error_message TEXT,
    ip_address VARCHAR(45),
    user_agent TEXT,
    details JSONB
);

CREATE INDEX idx_webauthn_audit_timestamp ON webauthn_audit_logs(timestamp DESC);
CREATE INDEX idx_webauthn_audit_user_event ON webauthn_audit_logs(user_id, event_type, timestamp);
CREATE INDEX idx_webauthn_audit_success ON webauthn_audit_logs(success, event_type, timestamp);

Security Considerations

Origin Validation

Strict origin validation prevents phishing attacks:

func (w *WebAuthnService) validateOrigin(clientData *protocol.CollectedClientData) error {
    expectedOrigin := w.rpOrigin
    
    if clientData.Origin != expectedOrigin {
        return fmt.Errorf("invalid origin: expected %s, got %s", 
            expectedOrigin, clientData.Origin)
    }
    
    return nil
}

Challenge Uniqueness

Each challenge must be unique and time-limited:

func (w *WebAuthnService) generateChallenge() ([]byte, error) {
    challenge := make([]byte, 32) // 256 bits
    _, err := rand.Read(challenge)
    if err != nil {
        return nil, fmt.Errorf("failed to generate challenge: %w", err)
    }
    return challenge, nil
}

Sign Counter Validation

Prevents credential cloning attacks:

func (w *WebAuthnService) validateSignCounter(credentialID []byte, newSignCount uint32) error {
    var currentSignCount uint32
    
    query := "SELECT sign_count FROM webauthn_credentials WHERE credential_id = $1"
    err := w.db.QueryRow(query, credentialID).Scan(&currentSignCount)
    if err != nil {
        return err
    }
    
    // Sign counter should increase (or stay same for some authenticators)
    if newSignCount < currentSignCount {
        return errors.New("invalid sign counter: potential credential cloning detected")
    }
    
    return nil
}

Rate Limiting

Protect against brute force attacks:

func (w *WebAuthnService) checkRateLimit(ctx context.Context, userID int64, operation string) error {
    key := fmt.Sprintf("webauthn:rate_limit:%s:%d", operation, userID)
    
    count, err := w.redis.Incr(ctx, key).Result()
    if err != nil {
        return err
    }
    
    // Set expiration on first attempt
    if count == 1 {
        w.redis.Expire(ctx, key, 15*time.Minute)
    }
    
    // Allow 5 attempts per 15 minutes
    if count > 5 {
        return errors.New("rate limit exceeded")
    }
    
    return nil
}

Resident Key Security

Handle resident credentials carefully:

type AuthenticatorSelection struct {
    ResidentKey              string `json:"residentKey,omitempty"`
    UserVerification         string `json:"userVerification,omitempty"`
    AuthenticatorAttachment  string `json:"authenticatorAttachment,omitempty"`
}

func (w *WebAuthnService) buildAuthenticatorSelection(req *RegistrationRequest) *AuthenticatorSelection {
    selection := &AuthenticatorSelection{
        UserVerification: "preferred",
    }
    
    if req.AuthenticatorSelection != nil {
        if req.AuthenticatorSelection.ResidentKey != "" {
            selection.ResidentKey = req.AuthenticatorSelection.ResidentKey
        }
        if req.AuthenticatorSelection.UserVerification != "" {
            selection.UserVerification = req.AuthenticatorSelection.UserVerification
        }
        if req.AuthenticatorSelection.AuthenticatorAttachment != "" {
            selection.AuthenticatorAttachment = req.AuthenticatorSelection.AuthenticatorAttachment
        }
    }
    
    return selection
}

Client Integration

JavaScript Implementation

Registration Flow

class WebAuthnService {
    async registerCredential(name, options = {}) {
        try {
            // 1. Begin registration
            const beginResponse = await fetch('/api/v1/webauthn/register/begin', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${this.accessToken}`
                },
                body: JSON.stringify({
                    name: name,
                    userVerification: options.userVerification || 'preferred',
                    authenticatorSelection: options.authenticatorSelection
                })
            });
            
            const { data } = await beginResponse.json();
            const challengeId = data.challengeId;
            const creationOptions = data.publicKey;
            
            // 2. Convert base64 values to ArrayBuffer
            creationOptions.challenge = this.base64ToArrayBuffer(creationOptions.challenge);
            creationOptions.user.id = this.base64ToArrayBuffer(creationOptions.user.id);
            
            // 3. Create credential
            const credential = await navigator.credentials.create({
                publicKey: creationOptions
            });
            
            if (!credential) {
                throw new Error('Failed to create credential');
            }
            
            // 4. Prepare credential for server
            const credentialData = {
                challengeId: challengeId,
                credential: {
                    id: credential.id,
                    rawId: this.arrayBufferToBase64(credential.rawId),
                    type: credential.type,
                    response: {
                        attestationObject: this.arrayBufferToBase64(credential.response.attestationObject),
                        clientDataJSON: this.arrayBufferToBase64(credential.response.clientDataJSON)
                    }
                }
            };
            
            // 5. Complete registration
            const finishResponse = await fetch('/api/v1/webauthn/register/finish', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${this.accessToken}`
                },
                body: JSON.stringify(credentialData)
            });
            
            const result = await finishResponse.json();
            
            if (result.data.success) {
                return result.data.credential;
            } else {
                throw new Error('Registration failed');
            }
            
        } catch (error) {
            console.error('WebAuthn registration error:', error);
            throw error;
        }
    }

    async authenticateWithWebAuthn(email, options = {}) {
        try {
            // 1. Begin authentication
            const beginResponse = await fetch('/api/v1/webauthn/login/begin', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    email: email,
                    userVerification: options.userVerification || 'preferred'
                })
            });
            
            const { data } = await beginResponse.json();
            const challengeId = data.challengeId;
            const requestOptions = data.publicKey;
            
            // 2. Convert base64 values
            requestOptions.challenge = this.base64ToArrayBuffer(requestOptions.challenge);
            if (requestOptions.allowCredentials) {
                requestOptions.allowCredentials.forEach(cred => {
                    cred.id = this.base64ToArrayBuffer(cred.id);
                });
            }
            
            // 3. Get assertion
            const assertion = await navigator.credentials.get({
                publicKey: requestOptions
            });
            
            if (!assertion) {
                throw new Error('Authentication failed');
            }
            
            // 4. Prepare assertion for server
            const assertionData = {
                challengeId: challengeId,
                credential: {
                    id: assertion.id,
                    rawId: this.arrayBufferToBase64(assertion.rawId),
                    type: assertion.type,
                    response: {
                        authenticatorData: this.arrayBufferToBase64(assertion.response.authenticatorData),
                        clientDataJSON: this.arrayBufferToBase64(assertion.response.clientDataJSON),
                        signature: this.arrayBufferToBase64(assertion.response.signature),
                        userHandle: assertion.response.userHandle ? 
                            this.arrayBufferToBase64(assertion.response.userHandle) : null
                    }
                }
            };
            
            // 5. Complete authentication
            const finishResponse = await fetch('/api/v1/webauthn/login/finish', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(assertionData)
            });
            
            const result = await finishResponse.json();
            
            if (result.data.success) {
                // Store tokens
                this.storeTokens(result.data.tokens);
                return result.data.tokens;
            } else {
                throw new Error('Authentication failed');
            }
            
        } catch (error) {
            console.error('WebAuthn authentication error:', error);
            throw error;
        }
    }
    
    // Utility methods
    base64ToArrayBuffer(base64) {
        const binary = atob(base64.replace(/-/g, '+').replace(/_/g, '/'));
        const bytes = new Uint8Array(binary.length);
        for (let i = 0; i < binary.length; i++) {
            bytes[i] = binary.charCodeAt(i);
        }
        return bytes.buffer;
    }
    
    arrayBufferToBase64(buffer) {
        const bytes = new Uint8Array(buffer);
        let binary = '';
        for (let i = 0; i < bytes.byteLength; i++) {
            binary += String.fromCharCode(bytes[i]);
        }
        return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
    }
}

Feature Detection

class WebAuthnSupport {
    static isSupported() {
        return !!(navigator.credentials && 
                 navigator.credentials.create && 
                 navigator.credentials.get &&
                 window.PublicKeyCredential);
    }
    
    static async isPlatformAuthenticatorAvailable() {
        if (!this.isSupported()) return false;
        
        try {
            return await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
        } catch (e) {
            return false;
        }
    }
    
    static async isConditionalMediationSupported() {
        if (!this.isSupported()) return false;
        
        try {
            return await PublicKeyCredential.isConditionalMediationAvailable();
        } catch (e) {
            return false;
        }
    }
    
    static getSupportedFeatures() {
        return {
            webauthn: this.isSupported(),
            conditionalMediation: null, // Will be set async
            platformAuthenticator: null, // Will be set async
            userVerification: this.isSupported()
        };
    }
}

// Initialize feature detection
WebAuthnSupport.getSupportedFeatures().then(async (features) => {
    features.conditionalMediation = await WebAuthnSupport.isConditionalMediationSupported();
    features.platformAuthenticator = await WebAuthnSupport.isPlatformAuthenticatorAvailable();
    console.log('WebAuthn features:', features);
});

React Component Example

import React, { useState, useEffect } from 'react';
import { WebAuthnService, WebAuthnSupport } from './webauthn-service';

function WebAuthnLogin({ onLogin }) {
    const [isSupported, setIsSupported] = useState(false);
    const [isPlatformAvailable, setIsPlatformAvailable] = useState(false);
    const [isLoading, setIsLoading] = useState(false);
    const [email, setEmail] = useState('');
    
    useEffect(() => {
        const checkSupport = async () => {
            setIsSupported(WebAuthnSupport.isSupported());
            setIsPlatformAvailable(await WebAuthnSupport.isPlatformAuthenticatorAvailable());
        };
        checkSupport();
    }, []);
    
    const handleWebAuthnLogin = async () => {
        if (!email) {
            alert('Please enter your email address');
            return;
        }
        
        setIsLoading(true);
        
        try {
            const webauthnService = new WebAuthnService();
            const tokens = await webauthnService.authenticateWithWebAuthn(email);
            onLogin(tokens);
        } catch (error) {
            alert(`WebAuthn login failed: ${error.message}`);
        } finally {
            setIsLoading(false);
        }
    };
    
    if (!isSupported) {
        return <div>WebAuthn is not supported in this browser</div>;
    }
    
    return (
        <div className="webauthn-login">
            <h3>Passwordless Login</h3>
            
            <div className="form-group">
                <label htmlFor="email">Email Address</label>
                <input
                    type="email"
                    id="email"
                    value={email}
                    onChange={(e) => setEmail(e.target.value)}
                    placeholder="Enter your email"
                />
            </div>
            
            <button 
                onClick={handleWebAuthnLogin}
                disabled={isLoading}
                className="webauthn-button"
            >
                {isLoading ? 'Authenticating...' : 
                 isPlatformAvailable ? 'Login with Touch/Face ID' : 'Login with Security Key'}
            </button>
            
            <div className="webauthn-help">
                <small>
                    {isPlatformAvailable 
                        ? 'Use your device\'s built-in biometric authentication'
                        : 'Use your security key or other authenticator'
                    }
                </small>
            </div>
        </div>
    );
}

export default WebAuthnLogin;

Testing and Validation

Unit Tests

func TestWebAuthnService_BeginRegistration(t *testing.T) {
    service := setupTestWebAuthnService(t)
    
    userID := int64(123)
    username := "test@example.com"
    displayName := "Test User"
    
    options, err := service.BeginRegistration(context.Background(), userID, username, displayName)
    
    assert.NoError(t, err)
    assert.NotNil(t, options)
    assert.Equal(t, "pvpipe.com", options.Response.RelyingParty.ID)
    assert.Equal(t, "PVPipe", options.Response.RelyingParty.Name)
    assert.Equal(t, username, options.Response.User.Name)
    assert.Equal(t, displayName, options.Response.User.DisplayName)
    assert.NotEmpty(t, options.Response.Challenge)
    assert.Contains(t, options.Response.Parameters, protocol.CredentialParameter{Type: "public-key", Algorithm: -7})
}

func TestWebAuthnService_ValidateOrigin(t *testing.T) {
    service := setupTestWebAuthnService(t)
    
    tests := []struct {
        name        string
        origin      string
        expectError bool
    }{
        {"Valid origin", "https://pvpipe.com", false},
        {"Invalid origin", "https://evil.com", true},
        {"HTTP origin", "http://pvpipe.com", true},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := service.validateOrigin(tt.origin)
            if tt.expectError {
                assert.Error(t, err)
            } else {
                assert.NoError(t, err)
            }
        })
    }
}

Integration Tests

// Browser automation tests using Playwright
const { test, expect } = require('@playwright/test');

test.describe('WebAuthn Integration', () => {
    test('should complete registration flow', async ({ page, context }) => {
        // Grant permissions for WebAuthn
        await context.grantPermissions(['clipboard-read']);
        
        // Navigate to registration page
        await page.goto('/register');
        
        // Fill in user details
        await page.fill('[name="email"]', 'test@example.com');
        await page.fill('[name="password"]', 'testpassword123');
        await page.click('button[type="submit"]');
        
        // Wait for redirect to WebAuthn setup
        await expect(page.locator('h2')).toContainText('Setup Passwordless Login');
        
        // Click setup WebAuthn
        await page.click('button:has-text("Setup Touch ID")');
        
        // Handle WebAuthn credential creation
        await page.evaluate(() => {
            // Mock successful credential creation
            const mockCredential = {
                id: 'test-credential-id',
                rawId: new Uint8Array([1, 2, 3, 4]),
                type: 'public-key',
                response: {
                    attestationObject: new Uint8Array([5, 6, 7, 8]),
                    clientDataJSON: new Uint8Array([9, 10, 11, 12])
                }
            };
            
            // Override navigator.credentials.create
            navigator.credentials.create = async () => mockCredential;
        });
        
        // Wait for success message
        await expect(page.locator('.success-message')).toContainText('WebAuthn setup complete');
    });
    
    test('should complete login flow', async ({ page, context }) => {
        // Setup existing credential
        await setupTestCredential(page);
        
        // Navigate to login
        await page.goto('/login');
        
        // Enter email
        await page.fill('[name="email"]', 'test@example.com');
        
        // Click WebAuthn login
        await page.click('button:has-text("Login with Touch ID")');
        
        // Mock WebAuthn authentication
        await page.evaluate(() => {
            const mockAssertion = {
                id: 'test-credential-id',
                rawId: new Uint8Array([1, 2, 3, 4]),
                type: 'public-key',
                response: {
                    authenticatorData: new Uint8Array([1, 2, 3, 4]),
                    clientDataJSON: new Uint8Array([5, 6, 7, 8]),
                    signature: new Uint8Array([9, 10, 11, 12])
                }
            };
            
            navigator.credentials.get = async () => mockAssertion;
        });
        
        // Verify successful login
        await expect(page.locator('.dashboard')).toBeVisible();
    });
});

Deployment Guidelines

Configuration

# Environment variables for WebAuthn
WEBAUTHN_ENABLED: "true"
WEBAUTHN_RP_ID: "pvpipe.com"
WEBAUTHN_RP_NAME: "PVPipe"
WEBAUTHN_RP_ORIGIN: "https://pvpipe.com"
WEBAUTHN_TIMEOUT: "300000" # 5 minutes in milliseconds
WEBAUTHN_REQUIRE_USER_VERIFICATION: "preferred"

HTTPS Requirements

WebAuthn requires HTTPS in production:

server {
    listen 443 ssl http2;
    server_name pvpipe.com;
    
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
    
    # Security headers for WebAuthn
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'";
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;
    
    location /api/v1/webauthn/ {
        proxy_pass http://ms-auth-backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Database Migrations

-- Migration: Create WebAuthn tables
BEGIN;

CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

-- WebAuthn credentials table
CREATE TABLE webauthn_credentials (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    credential_id BYTEA NOT NULL UNIQUE,
    public_key BYTEA NOT NULL,
    algorithm INTEGER NOT NULL,
    sign_count INTEGER NOT NULL DEFAULT 0,
    transports TEXT[] NOT NULL DEFAULT '{}',
    aaguid UUID,
    name VARCHAR(100) NOT NULL,
    is_active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    last_used_at TIMESTAMP WITH TIME ZONE
);

-- Indexes
CREATE INDEX idx_webauthn_credentials_user_active ON webauthn_credentials(user_id, is_active);
CREATE INDEX idx_webauthn_credentials_credential_id ON webauthn_credentials(credential_id);
CREATE INDEX idx_webauthn_credentials_last_used ON webauthn_credentials(last_used_at DESC);

-- Audit logs
CREATE TABLE webauthn_audit_logs (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    event_type VARCHAR(50) NOT NULL,
    user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
    credential_id BYTEA,
    success BOOLEAN NOT NULL,
    error_message TEXT,
    ip_address VARCHAR(45),
    user_agent TEXT,
    details JSONB
);

CREATE INDEX idx_webauthn_audit_timestamp ON webauthn_audit_logs(timestamp DESC);
CREATE INDEX idx_webauthn_audit_user_event ON webauthn_audit_logs(user_id, event_type, timestamp);
CREATE INDEX idx_webauthn_audit_success ON webauthn_audit_logs(success, event_type, timestamp);

COMMIT;

Monitoring and Metrics

// Prometheus metrics for WebAuthn
var (
    webauthnRegistrations = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "webauthn_registrations_total",
            Help: "Total number of WebAuthn registrations",
        },
        []string{"success"},
    )
    
    webauthnAuthentications = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "webauthn_authentications_total", 
            Help: "Total number of WebAuthn authentications",
        },
        []string{"success"},
    )
    
    webauthnDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name: "webauthn_operation_duration_seconds",
            Help: "Duration of WebAuthn operations",
            Buckets: prometheus.DefBuckets,
        },
        []string{"operation"},
    )
)

This comprehensive WebAuthn implementation provides modern, secure, passwordless authentication that integrates seamlessly with the existing PVPipe authentication system while providing an enhanced user experience and improved security posture.