capacitor-biometric-authentication
Version:
Framework-agnostic biometric authentication library. Works with React, Vue, Angular, or vanilla JS. No providers required!
338 lines (287 loc) • 9.67 kB
text/typescript
import {
BiometricAuthAdapter,
BiometricAuthConfiguration,
BiometricAuthOptions,
BiometricAuthResult,
BiometricAuthState,
BiometricErrorCode,
BiometryType
} from './types';
import { PlatformDetector } from './platform-detector';
export class BiometricAuthCore {
private static instance: BiometricAuthCore;
private config: BiometricAuthConfiguration = {
adapter: 'auto',
debug: false,
sessionDuration: 3600, // 1 hour in seconds (consistent with web.ts)
};
private sessionTimeoutId: ReturnType<typeof setTimeout> | null = null;
private adapters = new Map<string, BiometricAuthAdapter>();
private currentAdapter: BiometricAuthAdapter | null = null;
private state: BiometricAuthState = {
isAuthenticated: false
};
private platformDetector = PlatformDetector.getInstance();
private subscribers = new Set<(state: BiometricAuthState) => void>();
// Store initialization promise to prevent race conditions
// Methods can await this to ensure adapter is ready before use
private initPromise: Promise<void>;
private constructor() {
// Store the promise so we can await it in methods
this.initPromise = this.initialize().catch(err => {
if (this.config.debug) {
console.warn('[BiometricAuth] Initialization failed:', err);
}
});
}
/**
* Ensures the adapter is initialized before use.
* This prevents race conditions where methods are called before
* the async initialization completes.
*/
private async ensureInitialized(): Promise<void> {
await this.initPromise;
}
static getInstance(): BiometricAuthCore {
if (!BiometricAuthCore.instance) {
BiometricAuthCore.instance = new BiometricAuthCore();
}
return BiometricAuthCore.instance;
}
private async initialize() {
// Detect platform and load appropriate adapter
const platformInfo = this.platformDetector.detect();
if (this.config.debug) {
console.warn('[BiometricAuth] Platform detected:', platformInfo);
}
// Load adapter based on platform
await this.loadAdapter(platformInfo.name);
}
private async loadAdapter(platform: string) {
try {
// Try custom adapters first
if (this.config.customAdapters?.[platform]) {
this.currentAdapter = this.config.customAdapters[platform];
return;
}
// Try to load from registered adapters
if (this.adapters.has(platform)) {
this.currentAdapter = this.adapters.get(platform)!;
return;
}
// Dynamic import based on platform
switch (platform) {
case 'web':
const { WebAdapter } = await import('../adapters/WebAdapter');
this.currentAdapter = new WebAdapter();
break;
case 'ios':
case 'android':
// Check if Capacitor is available
if (this.platformDetector.detect().isCapacitor) {
const { CapacitorAdapter } = await import('../adapters/CapacitorAdapter');
this.currentAdapter = new CapacitorAdapter();
} else if (this.platformDetector.detect().isCordova) {
// For Cordova, we might need a separate adapter in the future
throw new Error('Cordova support not yet implemented. Please use Capacitor for native biometric authentication.');
}
break;
case 'electron':
const { ElectronAdapter } = await import('../adapters/ElectronAdapter');
this.currentAdapter = new ElectronAdapter();
break;
default:
throw new Error(`Platform ${platform} not supported`);
}
} catch (error) {
if (this.config.debug) {
console.warn('[BiometricAuth] Failed to load adapter:', error);
}
// Fallback to web adapter if available
if (platform !== 'web' && this.platformDetector.detect().isWeb) {
const { WebAdapter } = await import('../adapters/WebAdapter');
this.currentAdapter = new WebAdapter();
}
}
}
configure(config: Partial<BiometricAuthConfiguration>) {
this.config = { ...this.config, ...config };
// Re-initialize if adapter changed
if (config.adapter && config.adapter !== 'auto') {
this.loadAdapter(config.adapter);
}
}
registerAdapter(name: string, adapter: BiometricAuthAdapter) {
this.adapters.set(name, adapter);
}
async isAvailable(): Promise<boolean> {
await this.ensureInitialized();
if (!this.currentAdapter) {
return false;
}
try {
return await this.currentAdapter.isAvailable();
} catch (error) {
if (this.config.debug) {
console.warn('[BiometricAuth] isAvailable error:', error);
}
return false;
}
}
async getSupportedBiometrics(): Promise<BiometryType[]> {
await this.ensureInitialized();
if (!this.currentAdapter) {
return [];
}
try {
return await this.currentAdapter.getSupportedBiometrics();
} catch (error) {
if (this.config.debug) {
console.warn('[BiometricAuth] getSupportedBiometrics error:', error);
}
return [];
}
}
async authenticate(options?: BiometricAuthOptions): Promise<BiometricAuthResult> {
await this.ensureInitialized();
if (!this.currentAdapter) {
return {
success: false,
error: {
code: BiometricErrorCode.PLATFORM_NOT_SUPPORTED,
message: 'No biometric adapter available for this platform'
}
};
}
try {
const result = await this.currentAdapter.authenticate(options);
if (result.success) {
this.updateState({
isAuthenticated: true,
sessionId: result.sessionId,
lastAuthTime: Date.now(),
biometryType: result.biometryType,
error: undefined
});
// Set up session timeout
if (this.config.sessionDuration && this.config.sessionDuration > 0) {
// Clear any existing timeout to prevent memory leaks
if (this.sessionTimeoutId !== null) {
clearTimeout(this.sessionTimeoutId);
}
// Convert seconds to milliseconds for setTimeout
this.sessionTimeoutId = setTimeout(() => {
this.logout();
}, this.config.sessionDuration * 1000);
}
} else {
this.updateState({
isAuthenticated: false,
error: result.error
});
}
return result;
} catch (error) {
const errorResult: BiometricAuthResult = {
success: false,
error: {
code: BiometricErrorCode.UNKNOWN_ERROR,
message: error instanceof Error ? error.message : 'Unknown error occurred',
details: error
}
};
this.updateState({
isAuthenticated: false,
error: errorResult.error
});
return errorResult;
}
}
async deleteCredentials(): Promise<void> {
await this.ensureInitialized();
if (!this.currentAdapter) {
throw new Error('No biometric adapter available');
}
await this.currentAdapter.deleteCredentials();
this.logout();
}
async hasCredentials(): Promise<boolean> {
await this.ensureInitialized();
if (!this.currentAdapter) {
return false;
}
try {
return await this.currentAdapter.hasCredentials();
} catch (error) {
if (this.config.debug) {
console.warn('[BiometricAuth] hasCredentials error:', error);
}
return false;
}
}
logout() {
// Clear session timeout to prevent memory leaks
if (this.sessionTimeoutId !== null) {
clearTimeout(this.sessionTimeoutId);
this.sessionTimeoutId = null;
}
this.updateState({
isAuthenticated: false,
sessionId: undefined,
lastAuthTime: undefined,
biometryType: undefined,
error: undefined
});
}
getState(): BiometricAuthState {
return { ...this.state };
}
isAuthenticated(): boolean {
if (!this.state.isAuthenticated || !this.state.lastAuthTime) {
return false;
}
// Check if session is still valid (sessionDuration is in seconds)
if (this.config.sessionDuration && this.config.sessionDuration > 0) {
const elapsedMs = Date.now() - this.state.lastAuthTime;
const sessionDurationMs = this.config.sessionDuration * 1000;
if (elapsedMs > sessionDurationMs) {
this.logout();
return false;
}
}
return true;
}
subscribe(callback: (state: BiometricAuthState) => void): () => void {
this.subscribers.add(callback);
// Return unsubscribe function
return () => {
this.subscribers.delete(callback);
};
}
private updateState(newState: Partial<BiometricAuthState>) {
this.state = { ...this.state, ...newState };
// Notify subscribers
this.subscribers.forEach(callback => {
callback(this.getState());
});
}
// Utility methods for common use cases
async requireAuthentication(callback: () => void | Promise<void>, options?: BiometricAuthOptions) {
if (!this.isAuthenticated()) {
const result = await this.authenticate(options);
if (!result.success) {
throw new Error(result.error?.message || 'Authentication failed');
}
}
return callback();
}
async withAuthentication<T>(callback: () => T | Promise<T>, options?: BiometricAuthOptions): Promise<T> {
if (!this.isAuthenticated()) {
const result = await this.authenticate(options);
if (!result.success) {
throw new Error(result.error?.message || 'Authentication failed');
}
}
return callback();
}
}