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.
- 🔐 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
npm install ngx-webauthnAdd the provider to your Angular application:
// main.ts
import { provideWebAuthn } from 'ngx-webauthn';
bootstrapApplication(AppComponent, {
providers: [
provideWebAuthn({
defaultTimeout: 60000, // Optional configuration
}),
],
});Choose between native WebAuthn types or simplified presets:
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.
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)
});
}
}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)
});
}
}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',
});
}
}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);
}
});
}
}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');
}
});
}
}For convenience, the library includes presets for common WebAuthn scenarios:
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
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
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)
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);
});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', ... }, ... }The library provides full support for both native WebAuthn types and their JSON equivalents:
// 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',
};// 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,
};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
},
});The library accepts configuration options when providing the service:
provideWebAuthn({
defaultTimeout: 60000, // Default timeout in milliseconds
});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
# Start the demo app
npx nx serve demoVisit http://localhost:4200 to explore the demo.
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=4201Then 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
npx nx build ngx-webauthnnpx nx test ngx-webauthnnpx nx lint ngx-webauthnlibs/ngx-webauthn/- Main library source codesrc/lib/presets/- Preset configurationssrc/lib/model/- TypeScript interfacessrc/lib/services/- Core WebAuthn servicesrc/lib/utils/- Utility functionssrc/lib/errors/- Error classes
apps/demo/- Interactive demo application
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new functionality
- Run the test suite
- Submit a pull request
MIT License - see LICENSE file for details.
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.