Skip to content

JonnyHeavey/ngx-webauthn

Repository files navigation

NgxWebauthn

A powerful Angular library for WebAuthn (Web Authentication API) integration that provides a clean, type-safe abstraction over the native WebAuthn API. Features direct support for standard WebAuthn types with an optional preset system for common scenarios.

Features

  • 🔐 Complete WebAuthn Support: Full registration and authentication flows
  • 🛡️ Type Safety: Direct support for native WebAuthn types with full TypeScript support
  • 📱 Cross-Platform: Works with platform authenticators, security keys, and mobile devices
  • 🔄 RxJS Integration: Observable-based API for reactive applications
  • 🧩 Flexible API: Use native WebAuthn options directly or simplified presets
  • Error Handling: Structured error types with meaningful messages
  • 🎯 Preset System: Optional pre-configured setups for common patterns (passkeys, 2FA, device-bound)
  • 📖 Transparent: All preset configurations are exported as inspectable constants

🌟 Try the live demo 🌟

Quick Start

Installation

npm install ngx-webauthn

Setup

Add the provider to your Angular application:

// main.ts
import { provideWebAuthn } from 'ngx-webauthn';

bootstrapApplication(AppComponent, {
  providers: [
    provideWebAuthn({
      defaultTimeout: 60000, // Optional configuration
    }),
  ],
});

Simple Usage

Choose between native WebAuthn types or simplified presets:

⚠️ Security Note

Important: In production, WebAuthn challenges and RP configuration should come from your server.

The examples in this documentation generate challenges and configure RP data client-side for demonstration purposes. However, in a production environment:

  • Challenges must be generated by your server to prevent replay attacks
  • RP configuration should come from your server's configuration
  • User data should be validated and provided by your backend

For a complete example of proper server-side integration, see the demo application's backend.

import { Component, inject } from '@angular/core';
import { WebAuthnService } from 'ngx-webauthn';

@Component({...})
export class MyComponent {
  private webAuthn = inject(WebAuthnService);

  // Native WebAuthn approach - full control
  registerNative() {
    const rpConfig = {
      name: 'My App',
      id: 'myapp.com'
    };

    const user = {
      id: new TextEncoder().encode('user123'),
      name: 'john.doe@example.com',
      displayName: 'John Doe',
    };

    const challenge = crypto.getRandomValues(new Uint8Array(32));

    this.webAuthn.register({
      rp: rpConfig,
      user: user,
      challenge: challenge,
      pubKeyCredParams: [{ type: 'public-key', alg: -7 }],
    }).subscribe({
      next: (result) => console.log('Registration successful:', result),
      error: (error) => console.error('Registration failed:', error)
    });
  }

  // Preset approach - simplified
  registerPreset() {
    const rpConfig = { name: 'My App' };

    this.webAuthn.register({
      username: 'john.doe@example.com',
      preset: 'passkey',
      rp: rpConfig
    }).subscribe({
      next: (result) => console.log('Registration successful:', result),
      error: (error) => console.error('Registration failed:', error)
    });
  }
}

For detailed usage examples and advanced patterns, see Usage Examples.

Usage Examples

Basic Usage with Native WebAuthn Types

The library provides direct support for standard WebAuthn types, giving you full control over the authentication process:

import { Component, inject } from '@angular/core';
import { WebAuthnService } from 'ngx-webauthn';

@Component({...})
export class MyComponent {
  private webAuthn = inject(WebAuthnService);

  // Using native WebAuthn creation options
  registerWithNativeOptions() {
    const rpConfig = {
      name: 'My App',
      id: 'myapp.com'
    };

    const user = {
      id: new TextEncoder().encode('user123'),
      name: 'john.doe@example.com',
      displayName: 'John Doe',
    };

    const challenge = crypto.getRandomValues(new Uint8Array(32));

    const creationOptions: PublicKeyCredentialCreationOptions = {
      rp: rpConfig,
      user: user,
      challenge: challenge,
      pubKeyCredParams: [
        { type: 'public-key', alg: -7 },  // ES256
        { type: 'public-key', alg: -257 }, // RS256
      ],
      authenticatorSelection: {
        authenticatorAttachment: 'platform',
        userVerification: 'required',
        residentKey: 'required',
      },
      timeout: 60000,
      attestation: 'direct',
    };

    this.webAuthn.register(creationOptions).subscribe({
      next: (result) => console.log('Registration successful:', result),
      error: (error) => console.error('Registration failed:', error)
    });
  }

  // Using JSON WebAuthn options (base64url encoded)
  registerWithJsonOptions() {
    const rpConfig = {
      name: 'My App',
      id: 'myapp.com'
    };

    const user = {
      id: 'dXNlcjEyMw', // base64url encoded 'user123'
      name: 'john.doe@example.com',
      displayName: 'John Doe',
    };

    const challenge = 'Y2hhbGxlbmdlMTIzNDU2Nzg5MA'; // base64url encoded challenge

    const jsonOptions: PublicKeyCredentialCreationOptionsJSON = {
      rp: rpConfig,
      user: user,
      challenge: challenge,
      pubKeyCredParams: [
        { type: 'public-key', alg: -7 },
        { type: 'public-key', alg: -257 },
      ],
      authenticatorSelection: {
        authenticatorAttachment: 'cross-platform',
        userVerification: 'preferred',
        residentKey: 'discouraged',
      },
      timeout: 60000,
      attestation: 'none',
    };

    this.webAuthn.register(jsonOptions).subscribe({
      next: (result) => console.log('Registration successful:', result),
      error: (error) => console.error('Registration failed:', error)
    });
  }

  // Authentication with native options
  authenticateWithNativeOptions() {
    const challenge = crypto.getRandomValues(new Uint8Array(32));

    const allowCredentials = [
      {
        type: 'public-key',
        id: new TextEncoder().encode('credential-id'),
        transports: ['usb', 'nfc'],
      },
    ];

    const requestOptions: PublicKeyCredentialRequestOptions = {
      challenge: challenge,
      allowCredentials: allowCredentials,
      userVerification: 'preferred',
      timeout: 60000,
    };

    this.webAuthn.authenticate(requestOptions).subscribe({
      next: (result) => console.log('Authentication successful:', result),
      error: (error) => console.error('Authentication failed:', error)
    });
  }
}

Simplified Preset System

For common scenarios, the library provides an optional preset system that handles the complexity for you:

import { Component, inject } from '@angular/core';
import { WebAuthnService } from 'ngx-webauthn';

@Component({...})
export class MyComponent {
  private webAuthn = inject(WebAuthnService);

  // Simple passkey registration using presets
  registerPasskey() {
    const rpConfig = { name: 'My App' };

    this.webAuthn.register({
      username: 'john.doe@example.com',
      preset: 'passkey',
      rp: rpConfig
    }).subscribe({
      next: (result) => console.log('Registration successful:', result),
      error: (error) => console.error('Registration failed:', error)
    });
  }

  // Second factor registration using presets
  registerSecondFactor() {
    const rpConfig = { name: 'My App' };

    this.webAuthn.register({
      username: 'john.doe@example.com',
      preset: 'externalSecurityKey',
      rp: rpConfig
    }).subscribe({
      next: (result) => console.log('Second factor registered:', result),
      error: (error) => console.error('Registration failed:', error)
    });
  }

  // Simple authentication using presets
  authenticate() {
    this.webAuthn.authenticate({
      preset: 'passkey'
    }).subscribe({
      next: (result) => console.log('Authentication successful:', result),
      error: (error) => console.error('Authentication failed:', error)
    });
  }
}

Common Integration Patterns

Passwordless Authentication with Passkeys

Implement a modern passwordless authentication flow using passkeys:

@Injectable({ providedIn: 'root' })
export class AuthService {
  private webAuthn = inject(WebAuthnService);

  // Register a new passkey for passwordless login
  registerPasskey(email: string, displayName: string) {
    const rpConfig = { name: 'My App', id: window.location.hostname };

    return this.webAuthn.register({
      username: email,
      displayName,
      preset: 'passkey',
      rp: rpConfig,
    });
  }

  // Authenticate with passkey (passwordless)
  loginWithPasskey() {
    return this.webAuthn.authenticate({
      preset: 'passkey',
    });
  }
}

Two-Factor Authentication (2FA) with Security Keys

Add a second factor using external security keys:

@Component({...})
export class TwoFactorSetupComponent {
  private webAuthn = inject(WebAuthnService);

  // Register security key as second factor
  addSecurityKey(userId: string) {
    const rpConfig = { name: 'My App' };

    this.webAuthn.register({
      username: userId,
      preset: 'externalSecurityKey',
      rp: rpConfig
    }).subscribe({
      next: (result) => {
        // Send credential ID to server for storage
        this.saveCredentialToServer(result.credentialId);
      },
      error: (error) => console.error('Security key registration failed:', error)
    });
  }

  // Authenticate with security key (after password)
  verifySecurityKey(credentialId: string) {
    const allowCredentials = [credentialId];

    this.webAuthn.authenticate({
      preset: 'externalSecurityKey',
      allowCredentials: allowCredentials
    }).subscribe({
      next: (result) => {
        // Verify signature with server
        this.verifyWithServer(result);
      }
    });
  }
}

Device-Bound Credentials for High Security

Create device-bound credentials that require platform authenticators:

@Component({...})
export class HighSecurityComponent {
  private webAuthn = inject(WebAuthnService);

  // Register device-bound credential
  registerDeviceBound(userId: string) {
    const rpConfig = { name: 'Secure App' };

    this.webAuthn.register({
      username: userId,
      preset: 'platformAuthenticator',
      rp: rpConfig
    }).subscribe({
      next: (result) => {
        // Credential is bound to this device only
        console.log('Device-bound credential registered');
      }
    });
  }

  // Authenticate with device-bound credential
  authenticateDeviceBound() {
    this.webAuthn.authenticate({
      preset: 'platformAuthenticator'
    }).subscribe({
      next: (result) => {
        // User verified with biometrics/PIN
        console.log('Device-bound authentication successful');
      }
    });
  }
}

Advanced Topics

Available Presets

For convenience, the library includes presets for common WebAuthn scenarios:

passkey

Modern passwordless, cross-device credentials

  • Requires resident keys (discoverable credentials)
  • Prefers user verification but doesn't require it
  • Works with both platform and cross-platform authenticators
  • Supports credential syncing across devices

externalSecurityKey

External security key as second factor after password

  • Discourages resident keys (server-side credential storage)
  • Prefers user verification
  • Favors cross-platform authenticators (USB/NFC security keys)
  • Credentials typically not synced between devices

platformAuthenticator

High-security, platform authenticator credentials

  • Requires platform authenticators (built-in biometrics/PIN)
  • Requires resident keys for discoverability
  • Requires user verification (biometric/PIN)
  • Credentials bound to specific device (no syncing)

Preset with Overrides

const username = 'john.doe@example.com';

this.webAuthn
  .register({
    username: username,
    preset: 'passkey',
    // Override preset defaults with native WebAuthn options
    authenticatorSelection: {
      userVerification: 'required',
    },
    timeout: 30000,
  })
  .subscribe((result) => {
    console.log('Custom passkey registered:', result);
  });

Inspecting Presets

All presets are exported as constants for transparency:

import { PASSKEY_PRESET, EXTERNAL_SECURITY_KEY_PRESET, PLATFORM_AUTHENTICATOR_PRESET } from 'ngx-webauthn';

console.log('Passkey configuration:', PASSKEY_PRESET);
// Output: { authenticatorSelection: { residentKey: 'required', ... }, ... }

Native WebAuthn Options Support

The library provides full support for both native WebAuthn types and their JSON equivalents:

Registration Options

// Native ArrayBuffer-based options
const rpConfig = { name: 'My App', id: 'myapp.com' };

const user = {
  id: new TextEncoder().encode('user-id'),
  name: 'user@example.com',
  displayName: 'User Name',
};

const challenge = crypto.getRandomValues(new Uint8Array(32));

const excludeCredentials = [
  {
    type: 'public-key',
    id: new TextEncoder().encode('existing-credential-id'),
    transports: ['usb', 'nfc'],
  },
];

const nativeOptions: PublicKeyCredentialCreationOptions = {
  rp: rpConfig,
  user: user,
  challenge: challenge,
  pubKeyCredParams: [{ type: 'public-key', alg: -7 }],
  excludeCredentials: excludeCredentials,
  authenticatorSelection: {
    authenticatorAttachment: 'platform',
    userVerification: 'required',
    residentKey: 'required',
  },
  timeout: 60000,
  attestation: 'direct',
};

// JSON base64url-encoded options
const rpConfigJson = { name: 'My App', id: 'myapp.com' };

const userJson = {
  id: 'dXNlci1pZA', // base64url encoded 'user-id'
  name: 'user@example.com',
  displayName: 'User Name',
};

const challengeJson = 'Y2hhbGxlbmdlLWRhdGE'; // base64url encoded challenge

const excludeCredentialsJson = [
  {
    type: 'public-key',
    id: 'ZXhpc3RpbmctY3JlZGVudGlhbC1pZA', // base64url encoded
    transports: ['usb', 'nfc'],
  },
];

const jsonOptions: PublicKeyCredentialCreationOptionsJSON = {
  rp: rpConfigJson,
  user: userJson,
  challenge: challengeJson,
  pubKeyCredParams: [{ type: 'public-key', alg: -7 }],
  excludeCredentials: excludeCredentialsJson,
  authenticatorSelection: {
    authenticatorAttachment: 'platform',
    userVerification: 'required',
    residentKey: 'required',
  },
  timeout: 60000,
  attestation: 'direct',
};

Authentication Options

// Native ArrayBuffer-based options
const challenge = crypto.getRandomValues(new Uint8Array(32));

const allowCredentials = [
  {
    type: 'public-key',
    id: new TextEncoder().encode('credential-id'),
    transports: ['internal', 'usb'],
  },
];

const nativeRequest: PublicKeyCredentialRequestOptions = {
  challenge: challenge,
  allowCredentials: allowCredentials,
  userVerification: 'preferred',
  timeout: 60000,
};

// JSON base64url-encoded options
const challengeJson = 'Y2hhbGxlbmdlLWRhdGE'; // base64url encoded

const allowCredentialsJson = [
  {
    type: 'public-key',
    id: 'Y3JlZGVudGlhbC1pZA', // base64url encoded
    transports: ['internal', 'usb'],
  },
];

const jsonRequest: PublicKeyCredentialRequestOptionsJSON = {
  challenge: challengeJson,
  allowCredentials: allowCredentialsJson,
  userVerification: 'preferred',
  timeout: 60000,
};

Error Handling

The library provides specific error types for better error handling:

import { UserCancelledError, AuthenticatorError, UnsupportedOperationError, InvalidOptionsError, SecurityError, TimeoutError } from 'ngx-webauthn';

this.webAuthn.register(creationOptions).subscribe({
  next: (result) => {
    // Handle success
  },
  error: (error) => {
    if (error instanceof UserCancelledError) {
      console.log('User cancelled the operation');
    } else if (error instanceof AuthenticatorError) {
      console.log('Authenticator error:', error.message);
    } else if (error instanceof UnsupportedOperationError) {
      console.log('Operation not supported:', error.message);
    }
    // ... handle other error types
  },
});

Configuration Options

The library accepts configuration options when providing the service:

provideWebAuthn({
  defaultTimeout: 60000, // Default timeout in milliseconds
});

Demo Application

Explore the library's capabilities through an interactive demo showcasing:

  • Browser support detection
  • Native WebAuthn option usage
  • Preset-based credential registration
  • Authentication with different configurations
  • Credential management interface
  • Real-time feedback and error handling

Quick Local Demo

# Start the demo app
npx nx serve demo

Visit http://localhost:4200 to explore the demo.

Enhanced Demo with Backend

The demo supports dual-mode operation:

Mock Mode (Default)

  • Uses client-side generated challenges
  • No backend required
  • Suitable for basic testing and demonstrations

Remote Mode (Localhost Only)

  • Uses server-generated cryptographically secure challenges
  • Requires the WebAuthn backend service
  • Provides realistic integration testing
  • Demonstrates production-ready WebAuthn flow

To run with backend:

# Terminal 1: Start the backend
npm run backend:start

# Terminal 2: Start the demo on port 4201
npx nx serve demo --port=4201

Then open http://localhost:4201 and enable the "Use Remote Backend" toggle.

For backend details, see apps/webauthn-backend/README.md

For comprehensive testing instructions, see WEBAUTHN-INTEGRATION-TEST-REPORT.md

Library Development

Build the Library

npx nx build ngx-webauthn

Run Tests

npx nx test ngx-webauthn

Lint

npx nx lint ngx-webauthn

Project Structure

  • libs/ngx-webauthn/ - Main library source code
    • src/lib/presets/ - Preset configurations
    • src/lib/model/ - TypeScript interfaces
    • src/lib/services/ - Core WebAuthn service
    • src/lib/utils/ - Utility functions
    • src/lib/errors/ - Error classes
  • apps/demo/ - Interactive demo application

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Add tests for new functionality
  5. Run the test suite
  6. Submit a pull request

License

MIT License - see LICENSE file for details.

Links & Resources

Documentation & Specifications

Framework & Tools

Demo Deployment

The demo application is automatically deployed to GitHub Pages on every push to the main branch.

Live Demo: https://jonnyheavey.github.io/ngx-webauthn/

Deployment Workflow:

  • Trigger: Automatic on main branch pushes + manual dispatch
  • Build Process: Library → Demo (with GitHub Pages base href)
  • Deployment: GitHub Actions → GitHub Pages
  • URL: https://jonnyheavey.github.io/ngx-webauthn/

Note: The demo uses WebAuthn which requires HTTPS. GitHub Pages provides this automatically, making it suitable for real WebAuthn testing. Credentials created on localhost during development won't work on the GitHub Pages domain due to WebAuthn's origin-based security model.

About

An Angular library that provides a clean, type-safe abstraction over the native WebAuthn API

Resources

Contributing

Stars

Watchers

Forks

Contributors