Building fiber-webauthn: Passwordless Authentication for Fiber

The modern web demands better security, and traditional password-based authentication simply isn’t cutting it anymore. Data breaches, password reuse, and phishing attacks have made it clear that we need a fundamental shift in how we handle user authentication. This is where WebAuthn comes in – a revolutionary web standard that promises truly passwordless authentication.

After working with various authentication systems over the years, I became fascinated with FIDO2 and WebAuthn’s potential for creating secure, user-friendly authentication experiences. This post details our complete journey of implementing WebAuthn support for the Fiber framework, from initial research to production deployment.

The Problem with Traditional Authentication

Before diving into the technical implementation, it’s worth understanding why we needed to move beyond passwords in the first place. Traditional password-based authentication suffers from several fundamental issues:

Security Vulnerabilities: Passwords are inherently vulnerable to a wide range of attacks including brute force, dictionary attacks, credential stuffing, and social engineering. Even with proper hashing and salting, passwords remain a weak link in the security chain.

User Experience Issues: Users struggle with password management, often reusing weak passwords across multiple sites. The cognitive burden of remembering unique, strong passwords for dozens of services is simply too high for most people.

Scalability Problems: As applications grow, managing password resets, account recovery, and security policies becomes increasingly complex and costly.

Compliance Challenges: Many industries require specific password policies and audit trails, making compliance a constant struggle with traditional systems.

Why WebAuthn?

WebAuthn provides several key advantages that address these fundamental issues:

Strong Security Through Public-Key Cryptography: Instead of shared secrets (passwords), WebAuthn uses asymmetric cryptography. The private key never leaves the user’s device, making it virtually impossible for attackers to steal credentials even if they compromise the server.

Protection Against Phishing Attacks: WebAuthn authenticators verify the origin of authentication requests, making it nearly impossible for phishing sites to trick users into providing their credentials.

Support for Multiple Authenticators: Users can register multiple authenticators including security keys, biometric sensors, and trusted platform modules (TPMs), providing flexibility and backup options.

No Password Management Required: Users never need to remember, create, or manage passwords, eliminating the weakest link in traditional authentication.

Better User Experience: Modern authenticators provide seamless experiences through biometrics, hardware keys, or platform authenticators built into devices.

Understanding the WebAuthn Flow

Before implementing our solution, it’s crucial to understand how WebAuthn works at a high level. The WebAuthn specification defines two main ceremonies:

Registration Ceremony

  1. Initiation: The user visits the registration page and chooses to register a new authenticator
  2. Challenge Generation: The server generates a cryptographic challenge and sends it to the client along with credential creation options
  3. Authenticator Interaction: The browser interacts with the selected authenticator (biometric sensor, security key, etc.)
  4. Credential Creation: The authenticator generates a new key pair and creates an attestation
  5. Verification: The server verifies the attestation and stores the public key

Authentication Ceremony

  1. Initiation: The user attempts to sign in
  2. Challenge Generation: The server generates a new challenge and sends credential request options
  3. Authenticator Interaction: The browser prompts the user to interact with their registered authenticator
  4. Assertion Creation: The authenticator signs the challenge with the private key
  5. Verification: The server verifies the assertion using the stored public key

Technical Implementation

Now let’s dive into the actual implementation of WebAuthn support for Fiber. Our goal was to create a middleware that would be easy to integrate into existing Fiber applications while providing all the necessary WebAuthn functionality.

1. Core Setup and Configuration

The foundation of our WebAuthn implementation is a robust configuration system that handles all the necessary parameters:

type Config struct {
    RPDisplayName string        // Human-readable relying party name
    RPID         string        // Relying party identifier (domain)
    RPOrigin     string        // Expected origin for requests
    Timeout      time.Duration // Timeout for ceremonies
    Attestation  string        // Attestation preference
    UserStore    UserStore     // User storage interface
    CredStore    CredentialStore // Credential storage interface
}

type WebAuthn struct {
    config         *Config
    userStore      UserStore
    credStore      CredentialStore
    sessionStore   SessionStore
    challengeStore ChallengeStore
}

func NewWebAuthn(config *Config) (*WebAuthn, error) {
    if config.Timeout == 0 {
        config.Timeout = 60 * time.Second
    }
    
    if config.Attestation == "" {
        config.Attestation = "none"
    }
    
    // Validate required fields
    if config.RPID == "" || config.RPOrigin == "" {
        return nil, errors.New("RPID and RPOrigin are required")
    }
    
    return &WebAuthn{
        config:         config,
        userStore:      config.UserStore,
        credStore:      config.CredStore,
        sessionStore:   NewMemorySessionStore(),
        challengeStore: NewMemoryChallengeStore(),
    }, nil
}

2. User and Credential Management

WebAuthn requires careful management of users and their associated credentials. We designed interfaces that can be implemented with any storage backend:

type User interface {
    WebAuthnID() []byte
    WebAuthnName() string
    WebAuthnDisplayName() string
    WebAuthnIcon() string
    WebAuthnCredentials() []Credential
}

type Credential struct {
    ID              []byte
    PublicKey       []byte
    AttestationType string
    Authenticator   Authenticator
    CreatedAt       time.Time
    LastUsed        time.Time
}

type UserStore interface {
    GetUser(userID string) (User, error)
    CreateUser(user User) error
    UpdateUser(user User) error
}

type CredentialStore interface {
    GetCredentials(userID string) ([]Credential, error)
    StoreCredential(userID string, credential Credential) error
    UpdateCredential(credential Credential) error
    DeleteCredential(credentialID []byte) error
}

3. Registration Flow Implementation

The registration flow is where new authenticators are registered with a user’s account:

func (w *WebAuthn) BeginRegistration(c *fiber.Ctx) error {
    user := getUserFromContext(c)
    if user == nil {
        return fiber.NewError(fiber.StatusUnauthorized, "User not authenticated")
    }
    
    // Generate registration options
    options, sessionData, err := w.generateRegistrationOptions(user)
    if err != nil {
        return fiber.NewError(fiber.StatusInternalServerError, err.Error())
    }
    
    // Store session data
    sessionID := generateSessionID()
    err = w.sessionStore.StoreSession(sessionID, sessionData)
    if err != nil {
        return fiber.NewError(fiber.StatusInternalServerError, "Failed to store session")
    }
    
    // Set session cookie
    c.Cookie(&fiber.Cookie{
        Name:     "webauthn_session",
        Value:    sessionID,
        HTTPOnly: true,
        Secure:   true,
        SameSite: "Strict",
        MaxAge:   300, // 5 minutes
    })
    
    return c.JSON(options)
}

func (w *WebAuthn) generateRegistrationOptions(user User) (*protocol.CredentialCreation, *SessionData, error) {
    // Get existing credentials to exclude
    existingCreds := user.WebAuthnCredentials()
    excludeCredentials := make([]protocol.CredentialDescriptor, len(existingCreds))
    
    for i, cred := range existingCreds {
        excludeCredentials[i] = protocol.CredentialDescriptor{
            Type:         protocol.PublicKeyCredentialType,
            CredentialID: cred.ID,
        }
    }
    
    // Generate challenge
    challenge, err := generateChallenge()
    if err != nil {
        return nil, nil, err
    }
    
    // Create credential creation options
    options := &protocol.CredentialCreation{
        Response: protocol.PublicKeyCredentialCreationOptions{
            Challenge: challenge,
            RP: protocol.RelyingPartyEntity{
                Name: w.config.RPDisplayName,
                ID:   w.config.RPID,
            },
            User: protocol.UserEntity{
                ID:          user.WebAuthnID(),
                Name:        user.WebAuthnName(),
                DisplayName: user.WebAuthnDisplayName(),
            },
            PubKeyCredParams: []protocol.CredentialParameter{
                {Type: protocol.PublicKeyCredentialType, Algorithm: webauthncose.AlgES256},
                {Type: protocol.PublicKeyCredentialType, Algorithm: webauthncose.AlgPS256},
                {Type: protocol.PublicKeyCredentialType, Algorithm: webauthncose.AlgRS256},
            },
            AuthenticatorSelection: protocol.AuthenticatorSelection{
                RequireResidentKey: protocol.ResidentKeyDiscouraged(),
                UserVerification:   protocol.VerificationPreferred,
            },
            ExcludeCredentials: excludeCredentials,
            Attestation:        protocol.ConveyancePreference(w.config.Attestation),
            Timeout:            int(w.config.Timeout.Milliseconds()),
        },
    }
    
    sessionData := &SessionData{
        Challenge:         challenge,
        UserID:           string(user.WebAuthnID()),
        Timeout:          time.Now().Add(w.config.Timeout),
        UserVerification: options.Response.AuthenticatorSelection.UserVerification,
    }
    
    return options, sessionData, nil
}

4. Completing Registration

Once the client generates a credential, we need to verify and store it:

func (w *WebAuthn) FinishRegistration(c *fiber.Ctx) error {
    // Get session data
    sessionID := c.Cookies("webauthn_session")
    sessionData, err := w.sessionStore.GetSession(sessionID)
    if err != nil {
        return fiber.NewError(fiber.StatusUnauthorized, "Invalid session")
    }
    
    // Parse the credential creation response
    var response protocol.CredentialCreationResponse
    if err := c.BodyParser(&response); err != nil {
        return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
    }
    
    // Verify the credential
    credential, err := w.verifyRegistrationResponse(response, sessionData)
    if err != nil {
        return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Registration verification failed: %s", err.Error()))
    }
    
    // Store the credential
    err = w.credStore.StoreCredential(sessionData.UserID, *credential)
    if err != nil {
        return fiber.NewError(fiber.StatusInternalServerError, "Failed to store credential")
    }
    
    // Clear session
    w.sessionStore.DeleteSession(sessionID)
    c.ClearCookie("webauthn_session")
    
    return c.JSON(fiber.Map{
        "status": "ok",
        "message": "Registration successful",
    })
}

func (w *WebAuthn) verifyRegistrationResponse(response protocol.CredentialCreationResponse, sessionData *SessionData) (*Credential, error) {
    // Verify the response structure
    if response.PublicKeyCredential.Credential.ID == nil {
        return nil, errors.New("missing credential ID")
    }
    
    // Decode the attestation object
    attestationObject, err := response.PublicKeyCredential.Response.GetAttestationObject()
    if err != nil {
        return nil, fmt.Errorf("failed to parse attestation object: %w", err)
    }
    
    // Verify the client data
    clientData, err := response.PublicKeyCredential.Response.GetClientData()
    if err != nil {
        return nil, fmt.Errorf("failed to parse client data: %w", err)
    }
    
    // Verify challenge
    if !bytes.Equal(clientData.Challenge, sessionData.Challenge) {
        return nil, errors.New("challenge mismatch")
    }
    
    // Verify origin
    if clientData.Origin != w.config.RPOrigin {
        return nil, errors.New("origin mismatch")
    }
    
    // Verify type
    if clientData.Type != protocol.CreateCeremony {
        return nil, errors.New("ceremony type mismatch")
    }
    
    // Extract and verify the authenticator data
    authData := attestationObject.AuthData
    if err := w.verifyAuthenticatorData(authData, sessionData); err != nil {
        return nil, fmt.Errorf("authenticator data verification failed: %w", err)
    }
    
    // Extract the public key
    publicKey, err := w.extractPublicKey(authData.AttData)
    if err != nil {
        return nil, fmt.Errorf("failed to extract public key: %w", err)
    }
    
    // Create credential object
    credential := &Credential{
        ID:              response.PublicKeyCredential.Credential.ID,
        PublicKey:       publicKey,
        AttestationType: attestationObject.Format,
        Authenticator: Authenticator{
            AAGUID:      authData.AttData.AAGUID,
            SignCount:   authData.Counter,
            CloneWarning: false,
        },
        CreatedAt: time.Now(),
        LastUsed:  time.Now(),
    }
    
    return credential, nil
}

5. Authentication Flow Implementation

The authentication flow allows users to sign in using their registered authenticators:

func (w *WebAuthn) BeginLogin(c *fiber.Ctx) error {
    // In a real implementation, you might identify the user first
    // For this example, we'll allow usernameless authentication
    username := c.Query("username")
    
    var allowedCredentials []protocol.CredentialDescriptor
    var userID string
    
    if username != "" {
        user, err := w.userStore.GetUser(username)
        if err != nil {
            return fiber.NewError(fiber.StatusNotFound, "User not found")
        }
        
        userID = string(user.WebAuthnID())
        credentials := user.WebAuthnCredentials()
        allowedCredentials = make([]protocol.CredentialDescriptor, len(credentials))
        
        for i, cred := range credentials {
            allowedCredentials[i] = protocol.CredentialDescriptor{
                Type:         protocol.PublicKeyCredentialType,
                CredentialID: cred.ID,
            }
        }
    }
    
    // Generate authentication options
    options, sessionData, err := w.generateAuthenticationOptions(userID, allowedCredentials)
    if err != nil {
        return fiber.NewError(fiber.StatusInternalServerError, err.Error())
    }
    
    // Store session data
    sessionID := generateSessionID()
    err = w.sessionStore.StoreSession(sessionID, sessionData)
    if err != nil {
        return fiber.NewError(fiber.StatusInternalServerError, "Failed to store session")
    }
    
    // Set session cookie
    c.Cookie(&fiber.Cookie{
        Name:     "webauthn_session",
        Value:    sessionID,
        HTTPOnly: true,
        Secure:   true,
        SameSite: "Strict",
        MaxAge:   300, // 5 minutes
    })
    
    return c.JSON(options)
}

func (w *WebAuthn) FinishLogin(c *fiber.Ctx) error {
    // Get session data
    sessionID := c.Cookies("webauthn_session")
    sessionData, err := w.sessionStore.GetSession(sessionID)
    if err != nil {
        return fiber.NewError(fiber.StatusUnauthorized, "Invalid session")
    }
    
    // Parse the credential assertion response
    var response protocol.CredentialAssertionResponse
    if err := c.BodyParser(&response); err != nil {
        return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
    }
    
    // Verify the assertion
    credential, err := w.verifyAuthenticationResponse(response, sessionData)
    if err != nil {
        return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Authentication verification failed: %s", err.Error()))
    }
    
    // Update credential usage
    credential.LastUsed = time.Now()
    w.credStore.UpdateCredential(*credential)
    
    // Create user session
    userSession := &UserSession{
        UserID:       sessionData.UserID,
        CredentialID: credential.ID,
        CreatedAt:    time.Now(),
        ExpiresAt:    time.Now().Add(24 * time.Hour),
    }
    
    sessionToken, err := w.createUserSession(userSession)
    if err != nil {
        return fiber.NewError(fiber.StatusInternalServerError, "Failed to create user session")
    }
    
    // Clear WebAuthn session
    w.sessionStore.DeleteSession(sessionID)
    c.ClearCookie("webauthn_session")
    
    // Set user session cookie
    c.Cookie(&fiber.Cookie{
        Name:     "user_session",
        Value:    sessionToken,
        HTTPOnly: true,
        Secure:   true,
        SameSite: "Strict",
        MaxAge:   int(24 * time.Hour.Seconds()),
    })
    
    return c.JSON(fiber.Map{
        "status": "ok",
        "message": "Authentication successful",
    })
}

Integration with Fiber Applications

Here’s how to integrate our WebAuthn middleware with your Fiber application:

package main

import (
    "log"
    
    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/middleware/cors"
    "github.com/gofiber/fiber/v2/middleware/logger"
    "your-package/webauthn"
)

func main() {
    app := fiber.New(fiber.Config{
        ErrorHandler: func(c *fiber.Ctx, err error) error {
            code := fiber.StatusInternalServerError
            if e, ok := err.(*fiber.Error); ok {
                code = e.Code
            }
            return c.Status(code).JSON(fiber.Map{
                "error": err.Error(),
            })
        },
    })
    
    // Middleware
    app.Use(logger.New())
    app.Use(cors.New(cors.Config{
        AllowOrigins:     "https://yourdomain.com",
        AllowCredentials: true,
    }))
    
    // Initialize WebAuthn
    webAuthn, err := webauthn.New(&webauthn.Config{
        RPDisplayName: "Your App",
        RPID:         "yourdomain.com",
        RPOrigin:     "https://yourdomain.com",
        UserStore:    NewUserStore(),
        CredStore:    NewCredentialStore(),
    })
    if err != nil {
        log.Fatal(err)
    }
    
    // WebAuthn routes
    app.Post("/webauthn/register/begin", webAuthn.BeginRegistration)
    app.Post("/webauthn/register/finish", webAuthn.FinishRegistration)
    app.Post("/webauthn/login/begin", webAuthn.BeginLogin)
    app.Post("/webauthn/login/finish", webAuthn.FinishLogin)
    
    // Protected routes
    app.Use("/protected", webAuthn.RequireAuth())
    app.Get("/protected/profile", func(c *fiber.Ctx) error {
        user := c.Locals("user").(User)
        return c.JSON(user)
    })
    
    log.Fatal(app.Listen(":3000"))
}

Frontend Implementation

The frontend implementation requires careful handling of the WebAuthn JavaScript API:

class WebAuthnClient {
    constructor(baseURL) {
        this.baseURL = baseURL;
    }
    
    async register() {
        try {
            // Begin registration
            const response = await fetch(`${this.baseURL}/webauthn/register/begin`, {
                method: 'POST',
                credentials: 'include',
            });
            
            if (!response.ok) {
                throw new Error('Registration initiation failed');
            }
            
            const options = await response.json();
            
            // Convert challenge and user ID from base64
            options.challenge = this.base64ToArrayBuffer(options.challenge);
            options.user.id = this.base64ToArrayBuffer(options.user.id);
            
            // Convert exclude credentials
            if (options.excludeCredentials) {
                options.excludeCredentials.forEach(cred => {
                    cred.id = this.base64ToArrayBuffer(cred.id);
                });
            }
            
            // Create credential
            const credential = await navigator.credentials.create({
                publicKey: options
            });
            
            // Convert response for transmission
            const credentialResponse = {
                id: credential.id,
                rawId: this.arrayBufferToBase64(credential.rawId),
                response: {
                    attestationObject: this.arrayBufferToBase64(credential.response.attestationObject),
                    clientDataJSON: this.arrayBufferToBase64(credential.response.clientDataJSON),
                },
                type: credential.type
            };
            
            // Finish registration
            const finishResponse = await fetch(`${this.baseURL}/webauthn/register/finish`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(credentialResponse),
                credentials: 'include',
            });
            
            if (!finishResponse.ok) {
                throw new Error('Registration completion failed');
            }
            
            return await finishResponse.json();
            
        } catch (error) {
            console.error('Registration failed:', error);
            throw error;
        }
    }
    
    async login(username) {
        try {
            // Begin login
            const response = await fetch(`${this.baseURL}/webauthn/login/begin?username=${username}`, {
                method: 'POST',
                credentials: 'include',
            });
            
            if (!response.ok) {
                throw new Error('Login initiation failed');
            }
            
            const options = await response.json();
            
            // Convert challenge from base64
            options.challenge = this.base64ToArrayBuffer(options.challenge);
            
            // Convert allowed credentials
            if (options.allowCredentials) {
                options.allowCredentials.forEach(cred => {
                    cred.id = this.base64ToArrayBuffer(cred.id);
                });
            }
            
            // Get assertion
            const assertion = await navigator.credentials.get({
                publicKey: options
            });
            
            // Convert response for transmission
            const assertionResponse = {
                id: assertion.id,
                rawId: this.arrayBufferToBase64(assertion.rawId),
                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,
                },
                type: assertion.type
            };
            
            // Finish login
            const finishResponse = await fetch(`${this.baseURL}/webauthn/login/finish`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(assertionResponse),
                credentials: 'include',
            });
            
            if (!finishResponse.ok) {
                throw new Error('Login completion failed');
            }
            
            return await finishResponse.json();
            
        } catch (error) {
            console.error('Login failed:', error);
            throw error;
        }
    }
    
    base64ToArrayBuffer(base64) {
        const binary = atob(base64);
        const buffer = new ArrayBuffer(binary.length);
        const view = new Uint8Array(buffer);
        for (let i = 0; i < binary.length; i++) {
            view[i] = binary.charCodeAt(i);
        }
        return buffer;
    }
    
    arrayBufferToBase64(buffer) {
        const binary = String.fromCharCode(...new Uint8Array(buffer));
        return btoa(binary);
    }
}

Security Considerations

Implementing WebAuthn requires careful attention to security at every level:

1. Credential Storage Security

type SecureCredentialStore struct {
    db     *sql.DB
    cipher cipher.AEAD
}

func (s *SecureCredentialStore) StoreCredential(userID string, credential Credential) error {
    // Encrypt sensitive data
    encryptedKey, err := s.encrypt(credential.PublicKey)
    if err != nil {
        return err
    }
    
    query := `
        INSERT INTO credentials (id, user_id, public_key, attestation_type, created_at)
        VALUES (?, ?, ?, ?, ?)
    `
    
    _, err = s.db.Exec(query, 
        credential.ID, 
        userID, 
        encryptedKey, 
        credential.AttestationType, 
        credential.CreatedAt,
    )
    
    return err
}

func (s *SecureCredentialStore) encrypt(data []byte) ([]byte, error) {
    nonce := make([]byte, s.cipher.NonceSize())
    if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
        return nil, err
    }
    
    ciphertext := s.cipher.Seal(nonce, nonce, data, nil)
    return ciphertext, nil
}

2. Rate Limiting and Abuse Prevention

func (w *WebAuthn) RateLimitMiddleware() fiber.Handler {
    store := memory.New()
    
    return limiter.New(limiter.Config{
        Max:          5,
        Expiration:   time.Minute,
        Storage:      store,
        KeyGenerator: func(c *fiber.Ctx) string {
            return c.IP()
        },
        LimitReached: func(c *fiber.Ctx) error {
            return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
                "error": "Too many requests",
            })
        },
    })
}

3. Session Management

type SecureSessionStore struct {
    sessions sync.Map
    cleanup  *time.Ticker
}

func NewSecureSessionStore() *SecureSessionStore {
    store := &SecureSessionStore{
        cleanup: time.NewTicker(time.Minute),
    }
    
    go store.cleanupExpired()
    return store
}

func (s *SecureSessionStore) cleanupExpired() {
    for range s.cleanup.C {
        now := time.Now()
        s.sessions.Range(func(key, value interface{}) bool {
            if session, ok := value.(*SessionData); ok {
                if session.Timeout.Before(now) {
                    s.sessions.Delete(key)
                }
            }
            return true
        })
    }
}

Testing and Quality Assurance

Comprehensive testing is crucial for WebAuthn implementations:

func TestWebAuthnRegistration(t *testing.T) {
    // Setup test configuration
    config := &Config{
        RPDisplayName: "Test App",
        RPID:         "localhost",
        RPOrigin:     "http://localhost:3000",
        UserStore:    NewMockUserStore(),
        CredStore:    NewMockCredentialStore(),
    }
    
    webAuthn, err := NewWebAuthn(config)
    require.NoError(t, err)
    
    // Test registration initiation
    app := fiber.New()
    app.Post("/register/begin", webAuthn.BeginRegistration)
    
    req := httptest.NewRequest("POST", "/register/begin", nil)
    resp, err := app.Test(req)
    require.NoError(t, err)
    
    assert.Equal(t, fiber.StatusOK, resp.StatusCode)
    
    // Parse response
    var options protocol.CredentialCreation
    err = json.NewDecoder(resp.Body).Decode(&options)
    require.NoError(t, err)
    
    // Verify options
    assert.NotEmpty(t, options.Response.Challenge)
    assert.Equal(t, "localhost", options.Response.RP.ID)
    assert.Equal(t, "Test App", options.Response.RP.Name)
}

func TestChallengeGeneration(t *testing.T) {
    // Test challenge uniqueness
    challenges := make(map[string]bool)
    
    for i := 0; i < 1000; i++ {
        challenge, err := generateChallenge()
        require.NoError(t, err)
        
        challengeStr := base64.URLEncoding.EncodeToString(challenge)
        assert.False(t, challenges[challengeStr], "Duplicate challenge generated")
        challenges[challengeStr] = true
    }
}

Performance Optimization

WebAuthn operations can be computationally expensive, so optimization is important:

1. Connection Pooling

type OptimizedWebAuthn struct {
    *WebAuthn
    pool *sync.Pool
}

func NewOptimizedWebAuthn(config *Config) *OptimizedWebAuthn {
    w, _ := NewWebAuthn(config)
    
    return &OptimizedWebAuthn{
        WebAuthn: w,
        pool: &sync.Pool{
            New: func() interface{} {
                return &VerificationContext{
                    hasher: sha256.New(),
                    buffer: make([]byte, 1024),
                }
            },
        },
    }
}

func (w *OptimizedWebAuthn) verifyWithPool(data []byte) error {
    ctx := w.pool.Get().(*VerificationContext)
    defer w.pool.Put(ctx)
    
    // Use pooled resources for verification
    ctx.hasher.Reset()
    ctx.hasher.Write(data)
    
    return nil
}

2. Caching Strategies

type CachedCredentialStore struct {
    store CredentialStore
    cache *lru.Cache
}

func (c *CachedCredentialStore) GetCredentials(userID string) ([]Credential, error) {
    if cached, ok := c.cache.Get(userID); ok {
        return cached.([]Credential), nil
    }
    
    creds, err := c.store.GetCredentials(userID)
    if err != nil {
        return nil, err
    }
    
    c.cache.Add(userID, creds)
    return creds, nil
}

Monitoring and Observability

Production WebAuthn implementations need comprehensive monitoring:

type WebAuthnMetrics struct {
    registrationAttempts prometheus.Counter
    registrationSuccesses prometheus.Counter
    authenticationAttempts prometheus.Counter
    authenticationSuccesses prometheus.Counter
    verificationDuration prometheus.Histogram
}

func (w *WebAuthn) instrumentedVerification(response protocol.CredentialAssertionResponse) error {
    start := time.Now()
    defer func() {
        w.metrics.verificationDuration.Observe(time.Since(start).Seconds())
    }()
    
    w.metrics.authenticationAttempts.Inc()
    
    err := w.verifyAuthenticationResponse(response, nil)
    if err == nil {
        w.metrics.authenticationSuccesses.Inc()
    }
    
    return err
}

Future Improvements and Roadmap

Our WebAuthn implementation is continuously evolving. Here are some planned improvements:

1. Enhanced Authenticator Support

  • Support for additional attestation formats
  • Better handling of platform-specific authenticators
  • Integration with enterprise authenticator management systems

2. Advanced Security Features

  • Implement credential backup and recovery mechanisms
  • Add support for authenticator attestation verification
  • Enhance fraud detection and risk assessment

3. Developer Experience

  • Create comprehensive documentation and examples
  • Build developer tools for testing and debugging
  • Provide integration guides for popular frameworks

4. Performance and Scalability

  • Implement distributed session storage
  • Add support for credential caching strategies
  • Optimize cryptographic operations

Resources and Further Reading

To deepen your understanding of WebAuthn and our implementation:

Conclusion

Implementing WebAuthn in Fiber provides a robust foundation for passwordless authentication in modern web applications. The combination of strong security guarantees and improved user experience makes it an excellent choice for applications that prioritize both security and usability.

Our implementation demonstrates that with careful attention to security, performance, and developer experience, WebAuthn can be successfully integrated into Go applications using the Fiber framework. The modular design allows for easy customization and extension while maintaining security best practices.

The future of web authentication is passwordless, and WebAuthn is leading the way. By implementing these technologies today, we’re building more secure and user-friendly applications for tomorrow.

Security is not just about strong algorithms; it’s about creating a seamless user experience that encourages secure behavior. WebAuthn represents the perfect marriage of these principles.


This implementation is part of our ongoing commitment to improving web security and user experience. We continue to contribute to the open-source community and welcome feedback and contributions from fellow developers.