capacitor-biometric-authentication
Version:
Framework-agnostic biometric authentication library. Works with React, Vue, Angular, or vanilla JS. No providers required!
440 lines • 20.1 kB
JavaScript
import { WebPlugin } from '@capacitor/core';
import { BiometricType, BiometricUnavailableReason, BiometricErrorCode, } from './definitions';
import { mergeCreateOptions, mergeGetOptions, arrayBufferToBase64, storeCredentialId, getStoredCredentialIds, clearStoredCredentialIds, } from './utils/webauthn';
export class BiometricAuthWeb extends WebPlugin {
constructor() {
super(...arguments);
this.config = {
sessionDuration: 3600, // 1 hour default
requireAuthenticationForEveryAccess: false,
fallbackMethods: [],
};
this.sessions = new Map();
}
// Helper function for base64url encoding
arrayBufferToBase64URL(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
async isAvailable() {
// Check if Web Authentication API is available
if (!window.PublicKeyCredential) {
return {
available: false,
reason: BiometricUnavailableReason.NOT_SUPPORTED,
errorMessage: 'Web Authentication API is not supported in this browser',
};
}
try {
// Check if platform authenticator is available
const available = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
if (!available) {
return {
available: false,
reason: BiometricUnavailableReason.NO_HARDWARE,
errorMessage: 'No platform authenticator available',
};
}
return {
available: true,
};
}
catch (error) {
return {
available: false,
reason: BiometricUnavailableReason.HARDWARE_UNAVAILABLE,
errorMessage: error instanceof Error ? error.message : 'Unknown error',
};
}
}
async getSupportedBiometrics() {
const result = await this.isAvailable();
if (!result.available) {
return {
biometrics: [],
};
}
// Web Authentication API doesn't specify biometric types
// Return generic biometric authentication as supported
return {
biometrics: [
BiometricType.FINGERPRINT,
BiometricType.FACE_AUTHENTICATION,
],
};
}
async authenticate(options) {
var _a, _b, _c, _d, _e, _f;
try {
// Check availability first
const availability = await this.isAvailable();
if (!availability.available) {
return {
success: false,
error: {
code: BiometricErrorCode.NOT_AVAILABLE,
message: availability.errorMessage ||
'Biometric authentication not available',
},
};
}
// If WebAuthn options are provided, use them directly
if ((_a = options === null || options === void 0 ? void 0 : options.webAuthnOptions) === null || _a === void 0 ? void 0 : _a.get) {
return this.authenticateWithWebAuthnOptions(options);
}
// Get stored credentials for the user
const userId = ((_c = (_b = options === null || options === void 0 ? void 0 : options.webAuthnOptions) === null || _b === void 0 ? void 0 : _b.get) === null || _c === void 0 ? void 0 : _c.rpId) ||
((_f = (_e = (_d = options === null || options === void 0 ? void 0 : options.webAuthnOptions) === null || _d === void 0 ? void 0 : _d.create) === null || _e === void 0 ? void 0 : _e.user) === null || _f === void 0 ? void 0 : _f.name);
const storedCredentialIds = getStoredCredentialIds(userId);
// If no stored credentials and user wants to save credentials, register instead
if (storedCredentialIds.length === 0 && (options === null || options === void 0 ? void 0 : options.saveCredentials)) {
return this.register(options);
}
// If we have stored credentials, try to authenticate with them
if (storedCredentialIds.length > 0) {
return this.authenticateWithCredentials(options);
}
// No credentials found, register new ones
return this.register(options);
}
catch (error) {
return this.handleWebAuthnError(error);
}
}
async authenticateWithWebAuthnOptions(options) {
var _a, _b;
try {
// Use the provided WebAuthn options directly
const getOptions = mergeGetOptions((_a = options.webAuthnOptions) === null || _a === void 0 ? void 0 : _a.get);
// Get the credential
const credential = (await navigator.credentials.get({
publicKey: getOptions,
}));
if (credential &&
credential.response instanceof AuthenticatorAssertionResponse) {
// Generate session token and include credential data
const sessionId = crypto.randomUUID();
const credentialId = arrayBufferToBase64(credential.rawId);
// Create enhanced token with credential data for backend verification
const credentialData = {
id: credential.id,
rawId: this.arrayBufferToBase64URL(credential.rawId),
response: {
authenticatorData: this.arrayBufferToBase64URL(credential.response.authenticatorData),
clientDataJSON: this.arrayBufferToBase64URL(credential.response.clientDataJSON),
signature: this.arrayBufferToBase64URL(credential.response.signature),
userHandle: credential.response.userHandle
? this.arrayBufferToBase64URL(credential.response.userHandle)
: undefined,
},
type: credential.type,
clientExtensionResults: JSON.stringify(((_b = credential.getClientExtensionResults) === null || _b === void 0 ? void 0 : _b.call(credential)) || {}),
authenticatorAttachment: credential.authenticatorAttachment,
};
const token = btoa(JSON.stringify({
credentialId,
timestamp: Date.now(),
sessionId,
type: 'authentication',
credentialData, // Include full credential data
}));
// Store session
const expiresAt = Date.now() + (this.config.sessionDuration || 3600) * 1000;
this.sessions.set(sessionId, { token, expiresAt });
// Clean up expired sessions
this.cleanupExpiredSessions();
return {
success: true,
token,
sessionId,
};
}
return {
success: false,
error: {
code: BiometricErrorCode.AUTHENTICATION_FAILED,
message: 'Failed to authenticate with credential',
},
};
}
catch (error) {
return this.handleWebAuthnError(error);
}
}
async authenticateWithCredentials(options) {
var _a, _b, _c, _d, _e, _f;
try {
// Get stored credential IDs
const userId = ((_b = (_a = options === null || options === void 0 ? void 0 : options.webAuthnOptions) === null || _a === void 0 ? void 0 : _a.get) === null || _b === void 0 ? void 0 : _b.rpId) ||
((_e = (_d = (_c = options === null || options === void 0 ? void 0 : options.webAuthnOptions) === null || _c === void 0 ? void 0 : _c.create) === null || _d === void 0 ? void 0 : _d.user) === null || _e === void 0 ? void 0 : _e.name);
const storedCredentialIds = getStoredCredentialIds(userId);
// Prepare allowed credentials
const allowCredentials = storedCredentialIds.map((id) => ({
id: Uint8Array.from(atob(id), (c) => c.charCodeAt(0)),
type: 'public-key',
transports: ['internal'],
}));
// Merge user options with defaults
const getOptions = mergeGetOptions((_f = options === null || options === void 0 ? void 0 : options.webAuthnOptions) === null || _f === void 0 ? void 0 : _f.get, {
rpId: window.location.hostname,
userVerification: 'required',
allowCredentials: allowCredentials.length > 0 ? allowCredentials : undefined,
});
// Get the credential
const credential = (await navigator.credentials.get({
publicKey: getOptions,
}));
if (credential &&
credential.response instanceof AuthenticatorAssertionResponse) {
// Generate session token
const sessionId = crypto.randomUUID();
const credentialId = arrayBufferToBase64(credential.rawId);
const token = btoa(JSON.stringify({
credentialId,
timestamp: Date.now(),
sessionId,
type: 'authentication',
}));
// Store session
const expiresAt = Date.now() + (this.config.sessionDuration || 3600) * 1000;
this.sessions.set(sessionId, { token, expiresAt });
// Clean up expired sessions
this.cleanupExpiredSessions();
return {
success: true,
token,
sessionId,
};
}
return {
success: false,
error: {
code: BiometricErrorCode.AUTHENTICATION_FAILED,
message: 'Failed to authenticate with credential',
},
};
}
catch (error) {
return this.handleWebAuthnError(error);
}
}
async register(options) {
var _a, _b, _c, _d, _e;
try {
// Check availability first
const availability = await this.isAvailable();
if (!availability.available) {
return {
success: false,
error: {
code: BiometricErrorCode.NOT_AVAILABLE,
message: availability.errorMessage ||
'Biometric authentication not available',
},
};
}
// If WebAuthn options are provided, use them directly
if ((_a = options === null || options === void 0 ? void 0 : options.webAuthnOptions) === null || _a === void 0 ? void 0 : _a.create) {
return this.registerWithWebAuthnOptions(options);
}
// Merge user options with defaults for fallback
const createOptions = mergeCreateOptions((_b = options === null || options === void 0 ? void 0 : options.webAuthnOptions) === null || _b === void 0 ? void 0 : _b.create, {
rp: {
name: (options === null || options === void 0 ? void 0 : options.title) || 'Biometric Authentication',
},
authenticatorSelection: {
authenticatorAttachment: 'platform',
userVerification: 'required',
},
});
// Create the credential
const credential = (await navigator.credentials.create({
publicKey: createOptions,
}));
if (credential &&
credential.response instanceof AuthenticatorAttestationResponse) {
// Store credential ID for future authentication
const credentialId = arrayBufferToBase64(credential.rawId);
const userId = (_e = (_d = (_c = options === null || options === void 0 ? void 0 : options.webAuthnOptions) === null || _c === void 0 ? void 0 : _c.create) === null || _d === void 0 ? void 0 : _d.user) === null || _e === void 0 ? void 0 : _e.name;
storeCredentialId(credentialId, userId);
// Generate session token
const sessionId = crypto.randomUUID();
const token = btoa(JSON.stringify({
credentialId,
timestamp: Date.now(),
sessionId,
type: 'registration',
}));
// Store session
const expiresAt = Date.now() + (this.config.sessionDuration || 3600) * 1000;
this.sessions.set(sessionId, { token, expiresAt });
// Clean up expired sessions
this.cleanupExpiredSessions();
return {
success: true,
token,
sessionId,
};
}
return {
success: false,
error: {
code: BiometricErrorCode.AUTHENTICATION_FAILED,
message: 'Failed to create credential',
},
};
}
catch (error) {
return this.handleWebAuthnError(error);
}
}
async registerWithWebAuthnOptions(options) {
var _a, _b, _c, _d, _e, _f, _g;
try {
// Use the provided WebAuthn options directly
const createOptions = mergeCreateOptions((_a = options.webAuthnOptions) === null || _a === void 0 ? void 0 : _a.create);
// Create the credential
const credential = (await navigator.credentials.create({
publicKey: createOptions,
}));
if (credential &&
credential.response instanceof AuthenticatorAttestationResponse) {
// Store credential ID for future authentication
const credentialId = arrayBufferToBase64(credential.rawId);
const userId = (_d = (_c = (_b = options === null || options === void 0 ? void 0 : options.webAuthnOptions) === null || _b === void 0 ? void 0 : _b.create) === null || _c === void 0 ? void 0 : _c.user) === null || _d === void 0 ? void 0 : _d.name;
storeCredentialId(credentialId, userId);
// Create enhanced token with credential data for backend verification
const credentialData = {
id: credential.id,
rawId: this.arrayBufferToBase64URL(credential.rawId),
response: {
attestationObject: this.arrayBufferToBase64URL(credential.response.attestationObject),
clientDataJSON: this.arrayBufferToBase64URL(credential.response.clientDataJSON),
transports: ((_f = (_e = credential.response).getTransports) === null || _f === void 0 ? void 0 : _f.call(_e)) || [],
},
type: credential.type,
clientExtensionResults: JSON.stringify(((_g = credential.getClientExtensionResults) === null || _g === void 0 ? void 0 : _g.call(credential)) || {}),
authenticatorAttachment: credential.authenticatorAttachment,
};
// Generate session token
const sessionId = crypto.randomUUID();
const token = btoa(JSON.stringify({
credentialId,
timestamp: Date.now(),
sessionId,
type: 'registration',
credentialData, // Include full credential data
}));
// Store session
const expiresAt = Date.now() + (this.config.sessionDuration || 3600) * 1000;
this.sessions.set(sessionId, { token, expiresAt });
// Clean up expired sessions
this.cleanupExpiredSessions();
return {
success: true,
token,
sessionId,
};
}
return {
success: false,
error: {
code: BiometricErrorCode.AUTHENTICATION_FAILED,
message: 'Failed to create credential',
},
};
}
catch (error) {
return this.handleWebAuthnError(error);
}
}
async deleteCredentials() {
// Clear all sessions
this.sessions.clear();
// Clear stored credential IDs
clearStoredCredentialIds();
// Clear any other stored data
try {
const keys = Object.keys(localStorage).filter((key) => key.startsWith('biometric_auth_'));
keys.forEach((key) => localStorage.removeItem(key));
}
catch (error) {
console.error('Failed to clear stored credentials:', error);
}
}
async configure(config) {
this.config = Object.assign(Object.assign({}, this.config), config);
// Validate configuration
if (config.sessionDuration && config.sessionDuration < 0) {
throw new Error('Session duration must be positive');
}
if (config.encryptionSecret && config.encryptionSecret.length < 32) {
console.warn('Encryption secret should be at least 32 characters for security');
}
}
cleanupExpiredSessions() {
const now = Date.now();
const expiredSessions = [];
this.sessions.forEach((session, id) => {
if (session.expiresAt < now) {
expiredSessions.push(id);
}
});
expiredSessions.forEach((id) => this.sessions.delete(id));
}
handleWebAuthnError(error) {
if (error instanceof Error) {
if (error.name === 'NotAllowedError') {
return {
success: false,
error: {
code: BiometricErrorCode.USER_CANCELLED,
message: 'User cancelled the authentication',
},
};
}
else if (error.name === 'NotSupportedError') {
return {
success: false,
error: {
code: BiometricErrorCode.NOT_AVAILABLE,
message: 'Biometric authentication not supported',
},
};
}
else if (error.name === 'InvalidStateError') {
return {
success: false,
error: {
code: BiometricErrorCode.INVALID_CONTEXT,
message: 'Invalid authentication context',
},
};
}
else if (error.name === 'SecurityError') {
return {
success: false,
error: {
code: BiometricErrorCode.INVALID_CONTEXT,
message: 'Security requirements not met',
},
};
}
}
return {
success: false,
error: {
code: BiometricErrorCode.UNKNOWN,
message: error instanceof Error ? error.message : 'Unknown error occurred',
},
};
}
}
//# sourceMappingURL=web.js.map