capacitor-biometric-authentication
Version:
Framework-agnostic biometric authentication library. Works with React, Vue, Angular, or vanilla JS. No providers required!
380 lines (329 loc) • 10.2 kB
text/typescript
/**
* Unified error handling utilities for biometric authentication
*
* This module consolidates error mapping logic from multiple adapters
* to provide consistent error handling across platforms.
*/
import {
BiometricError,
BiometricErrorCode,
BiometricAuthResult,
} from '../types';
/**
* Map a WebAuthn DOMException to a BiometricError
*/
export function mapDOMException(error: DOMException): BiometricError {
switch (error.name) {
case 'NotAllowedError':
return {
code: BiometricErrorCode.USER_CANCELLED,
message: 'User cancelled the authentication',
details: error,
};
case 'AbortError':
return {
code: BiometricErrorCode.USER_CANCELLED,
message: 'Authentication was aborted',
details: error,
};
case 'SecurityError':
return {
code: BiometricErrorCode.AUTHENTICATION_FAILED,
message: 'Security error during authentication',
details: error,
};
case 'InvalidStateError':
return {
code: BiometricErrorCode.AUTHENTICATION_FAILED,
message: 'Invalid state for authentication',
details: error,
};
case 'NotSupportedError':
return {
code: BiometricErrorCode.BIOMETRIC_UNAVAILABLE,
message: 'WebAuthn is not supported',
details: error,
};
case 'TimeoutError':
return {
code: BiometricErrorCode.TIMEOUT,
message: 'Authentication timed out',
details: error,
};
case 'ConstraintError':
return {
code: BiometricErrorCode.AUTHENTICATION_FAILED,
message: 'Authenticator constraint not satisfied',
details: error,
};
default:
return {
code: BiometricErrorCode.UNKNOWN_ERROR,
message: error.message || 'An unknown DOM error occurred',
details: error,
};
}
}
/**
* Map a generic Error to a BiometricError
*/
export function mapGenericError(error: Error): BiometricError {
const message = error.message.toLowerCase();
// Check for common error patterns
if (message.includes('cancelled') || message.includes('canceled')) {
return {
code: BiometricErrorCode.USER_CANCELLED,
message: 'User cancelled the operation',
details: error,
};
}
if (message.includes('timeout') || message.includes('timed out')) {
return {
code: BiometricErrorCode.TIMEOUT,
message: 'Operation timed out',
details: error,
};
}
if (message.includes('not available') || message.includes('unavailable')) {
return {
code: BiometricErrorCode.BIOMETRIC_UNAVAILABLE,
message: 'Biometric authentication is not available',
details: error,
};
}
if (message.includes('not supported')) {
return {
code: BiometricErrorCode.PLATFORM_NOT_SUPPORTED,
message: 'Operation is not supported on this platform',
details: error,
};
}
if (message.includes('not enrolled') || message.includes('no biometrics')) {
return {
code: BiometricErrorCode.NOT_ENROLLED,
message: 'No biometrics enrolled on this device',
details: error,
};
}
if (message.includes('locked out') || message.includes('lockout')) {
return {
code: BiometricErrorCode.LOCKED_OUT,
message: 'Biometric authentication is locked out due to too many attempts',
details: error,
};
}
return {
code: BiometricErrorCode.UNKNOWN_ERROR,
message: error.message || 'An unknown error occurred',
details: error,
};
}
/**
* Map any unknown error to a BiometricError
*/
export function mapWebAuthnError(error: unknown): BiometricError {
if (error instanceof DOMException) {
return mapDOMException(error);
}
if (error instanceof Error) {
return mapGenericError(error);
}
// Handle string errors
if (typeof error === 'string') {
return {
code: BiometricErrorCode.UNKNOWN_ERROR,
message: error,
details: error,
};
}
// Handle completely unknown errors
return {
code: BiometricErrorCode.UNKNOWN_ERROR,
message: 'An unknown error occurred',
details: error,
};
}
/**
* Map a native platform error (iOS/Android/Windows) to a BiometricError
*/
export function mapNativeError(error: Error, platform: string): BiometricError {
const message = error.message.toLowerCase();
// Windows/Electron specific error messages
if (platform === 'windows' || platform === 'electron') {
// Windows Hello cancellation
if (message.includes('cancelled') || message.includes('canceled') || message.includes('user refused')) {
return {
code: BiometricErrorCode.USER_CANCELLED,
message: 'User cancelled Windows Hello authentication',
details: error,
};
}
// Windows Hello not configured
if (message.includes('not configured') || message.includes('not set up')) {
return {
code: BiometricErrorCode.NOT_ENROLLED,
message: 'Windows Hello is not configured',
details: error,
};
}
// Windows Hello not available
if (message.includes('not available') || message.includes('not supported')) {
return {
code: BiometricErrorCode.NOT_AVAILABLE,
message: 'Windows Hello is not available on this device',
details: error,
};
}
// Windows Hello lockout
if (message.includes('locked') || message.includes('too many attempts')) {
return {
code: BiometricErrorCode.LOCKED_OUT,
message: 'Windows Hello is locked due to too many attempts',
details: error,
};
}
}
// iOS specific error messages
if (platform === 'ios') {
if (message.includes('user cancel') || message.includes('userCancel')) {
return {
code: BiometricErrorCode.USER_CANCELLED,
message: 'User cancelled authentication',
details: error,
};
}
if (message.includes('passcode not set') || message.includes('passcodeNotSet')) {
return {
code: BiometricErrorCode.NOT_ENROLLED,
message: 'Passcode is not set on this device',
details: error,
};
}
if (message.includes('biometry not available') || message.includes('biometryNotAvailable')) {
return {
code: BiometricErrorCode.NOT_AVAILABLE,
message: 'Biometry is not available on this device',
details: error,
};
}
if (message.includes('biometry not enrolled') || message.includes('biometryNotEnrolled')) {
return {
code: BiometricErrorCode.NOT_ENROLLED,
message: 'No biometrics are enrolled on this device',
details: error,
};
}
if (message.includes('biometry lockout') || message.includes('biometryLockout')) {
return {
code: BiometricErrorCode.LOCKED_OUT,
message: 'Biometric authentication is locked out',
details: error,
};
}
}
// Android specific error messages
if (platform === 'android') {
if (message.includes('ERROR_USER_CANCELED') || message.includes('user canceled')) {
return {
code: BiometricErrorCode.USER_CANCELLED,
message: 'User cancelled authentication',
details: error,
};
}
if (message.includes('ERROR_NO_BIOMETRICS') || message.includes('no biometrics')) {
return {
code: BiometricErrorCode.NOT_ENROLLED,
message: 'No biometrics are enrolled on this device',
details: error,
};
}
if (message.includes('ERROR_HW_NOT_PRESENT') || message.includes('hw not present')) {
return {
code: BiometricErrorCode.NOT_AVAILABLE,
message: 'Biometric hardware is not available',
details: error,
};
}
if (message.includes('ERROR_HW_UNAVAILABLE') || message.includes('hw unavailable')) {
return {
code: BiometricErrorCode.NOT_AVAILABLE,
message: 'Biometric hardware is currently unavailable',
details: error,
};
}
if (message.includes('ERROR_LOCKOUT') || message.includes('lockout')) {
return {
code: BiometricErrorCode.LOCKED_OUT,
message: 'Biometric authentication is locked out',
details: error,
};
}
if (message.includes('ERROR_NEGATIVE_BUTTON') || message.includes('negative button')) {
return {
code: BiometricErrorCode.USER_CANCELLED,
message: 'User pressed the negative button',
details: error,
};
}
}
// Fall back to generic error mapping
return mapGenericError(error);
}
/**
* Create a failed BiometricAuthResult from an error
*/
export function createErrorResult(error: unknown, platform?: string): BiometricAuthResult {
let biometricError: BiometricError;
if (platform && error instanceof Error) {
biometricError = mapNativeError(error, platform);
} else {
biometricError = mapWebAuthnError(error);
}
return {
success: false,
error: biometricError,
};
}
/**
* Check if an error is a user cancellation
*/
export function isUserCancellation(error: unknown): boolean {
if (error instanceof DOMException) {
return error.name === 'NotAllowedError' || error.name === 'AbortError';
}
if (error instanceof Error) {
const message = error.message.toLowerCase();
return message.includes('cancel') || message.includes('abort');
}
return false;
}
/**
* Check if an error is a timeout
*/
export function isTimeout(error: unknown): boolean {
if (error instanceof DOMException) {
return error.name === 'TimeoutError';
}
if (error instanceof Error) {
const message = error.message.toLowerCase();
return message.includes('timeout') || message.includes('timed out');
}
return false;
}
/**
* Check if an error indicates biometrics are unavailable
*/
export function isBiometricUnavailable(error: unknown): boolean {
if (error instanceof DOMException) {
return error.name === 'NotSupportedError';
}
if (error instanceof Error) {
const message = error.message.toLowerCase();
return (
message.includes('not available') ||
message.includes('unavailable') ||
message.includes('not supported')
);
}
return false;
}