Biometric Auth Lock

πŸ“– Documentation en franΓ§ais

A reusable biometric authentication and PIN lock service for Flutter applications with support for fingerprint, Face ID, and PIN fallback.

Features

  • Biometric Authentication: Support for fingerprint and Face ID
  • PIN Fallback: Optional PIN code authentication when biometrics aren't available
  • Multi-Platform: Works on Android and iOS
  • Persistent Storage: Save authentication state across app restarts
  • Flexible API: High-level and low-level APIs for different use cases
  • BiometricLockService: App-level facade for one-line integration with dependency injection
  • Type-Safe: Interface-based design with functional error handling (Either)
  • Comprehensive Exception Handling: Specific exceptions for different failure scenarios

Installation

Add to your pubspec.yaml:

dependencies:
  biometric_auth_lock:
    path: packages/biometric_auth_lock

Platform Setup

Android

1. Permissions β€” Add the following to your AndroidManifest.xml:

<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>

2. MainActivity β€” Your MainActivity must extend FlutterFragmentActivity (instead of FlutterActivity). The local_auth plugin requires FlutterFragmentActivity because the Android biometric prompt APIs rely on fragment management.

If you don't make this change you'll see:

PlatformException(no_fragment_activity, local_auth plugin requires activity
to be a FragmentActivity., null, null)

Kotlin (android/app/src/main/kotlin/.../MainActivity.kt):

package com.example.your_app_name // Keep your original package name

import io.flutter.embedding.android.FlutterFragmentActivity

class MainActivity: FlutterFragmentActivity() {
    // You can leave this body empty
}

Java (android/app/src/main/java/.../MainActivity.java):

package com.example.your_app_name; // Keep your original package name

import io.flutter.embedding.android.FlutterFragmentActivity;

public class MainActivity extends FlutterFragmentActivity {
    // You can leave this body empty
}

iOS

Add the following to your Info.plist:

<key>NSFaceIDUsageDescription</key>
<string>Why is my app authenticating using face id?</string>

Usage

BiometricLockService is the simplest way to integrate biometric auth. It wraps the entire package behind a single facade designed for dependency injection:

import 'package:biometric_auth_lock/biometric_auth_lock.dart';

// Register once in your service locator
final lockService = await BiometricLockService.withDefaults();

// --- Check device capabilities ---
final supported = await lockService.isSupported;   // Sensor present & enrolled
final faceIdOk  = await lockService.isFaceIdAvailable;
final fingerOk  = await lockService.isFingerprintAvailable;
final pinOk     = await lockService.isPinConfigured;

// --- Enable the lock (prompts user once) ---
final enabled = await lockService.enable();

// --- Gate a critical operation ---
final result = await lockService.unlock(
  reason: 'Authenticatez-vous pour effectuer le paiement',
);
if (result.unlocked) {
  // proceed with sensitive operation
}

// --- Or use guard to wrap any action ---
final paymentResult = await lockService.guard(
  () => performPayment(),
  reason: 'Authentifiez-vous pour confirmer le paiement',
);

// --- PIN fallback ---
await lockService.setPin('1234');
final pinOk = await lockService.unlockWithPin('1234');

// --- Disable the lock ---
await lockService.disable();

BiometricUnlockResult

The unlock() method returns a BiometricUnlockResult enum:

Value Meaning
success User authenticated successfully
failed Authentication was cancelled or failed
notConfigured Biometric lock hasn't been enabled yet
notSupported Device doesn't support biometrics

Use the .unlocked extension getter as a quick success check.

High-Level API (LocalAuthService)

For advanced control, LocalAuthService provides the underlying implementation with automatic setup and fallback handling.

import 'package:biometric_auth_lock/biometric_auth_lock.dart';
import 'package:local_storage_impl/local_storage_impl.dart';

// Initialize services
final localStorage = await SharedPreferencesLocalStorage.getInstance();
final biometricAuthStorage = LocalStorageBiometricAuthService(localStorage);
final biometricIdentity = LocalAuthBiometricIdentityService();
final localAuthService = LocalAuthService(biometricAuthStorage, biometricIdentity);

// Check status
final status = await localAuthService.status();
print('Biometric configured: ${status.isBiometricAuthConfigured}');
print('PIN configured: ${status.isPinConfigured}');
print('Device supported: ${status.isBiometricAuthSupported}');

// Authenticate with biometrics
final result = await localAuthService.resolveWithBiometric(
  'Please authenticate to continue',
  signInTitle: 'Sign In',
  cancelButtonMessage: 'Cancel',
  fallbackToPin: true,
);

if (result.authenticated) {
  print('Authentication successful!');
} else if (!result.isSupported) {
  print('Biometrics not supported on this device');
} else if (!result.isConfigured) {
  print('Biometrics not configured');
}

Setting Up Authentication

// Setup biometric authentication
final setupResult = await localAuthService.setupBiometric(
  'Setup biometric authentication',
  signInTitle: 'Setup',
  cancelButtonMessage: 'Skip',
);

if (setupResult.ok) {
  print('Biometric authentication configured!');
} else if (!setupResult.isAvailable) {
  print('Biometrics not available, setting up PIN...');
  await localAuthService.setupPin('1234');
}

PIN Code Authentication

// Setup PIN
await localAuthService.setupPin('1234');

// Authenticate with PIN
final pinResult = await localAuthService.resolveWithPin('1234');

if (pinResult.authenticated) {
  print('PIN authentication successful!');
} else if (!pinResult.isConfigured) {
  print('PIN not configured');
} else {
  print('Incorrect PIN');
}

Declining Authentication Methods

// User declines biometric auth
await localAuthService.declineBioMetricAuth();

// User declines PIN auth
await localAuthService.declinePinCodeAuth();

Low-Level API (Advanced)

For more control, use the individual services directly.

Biometric Identity Service

final biometricService = LocalAuthBiometricIdentityService();

// Check device status
final status = await biometricService.status();
if (status.canAuthenticateWithBiometrics && status.isDeviceSupported) {
  // Authenticate
  final result = await biometricService.authenticate(
    'Authenticate to access app',
    signInTitle: 'Sign In',
    cancelButtonMessage: 'Cancel',
  );

  result.fold(
    ifLeft: (exception) {
      if (exception is BiometricNotSupportedException) {
        print('Biometrics not supported');
      } else if (exception is BiometricAuthLockoutException) {
        print('Too many failed attempts');
      } else if (exception is BiometricAuthFailedException) {
        print('Authentication failed');
      }
    },
    ifRight: (authResult) {
      print('Authenticated at: ${authResult.timestamp}');
    },
  );
}

// Check available biometric types
final types = await biometricService.getAvailableTypes();
for (final type in types) {
  print('Available: $type');
}

Biometric Auth Storage Service

final localStorage = await SharedPreferencesLocalStorage.getInstance();
final authStorage = LocalStorageBiometricAuthService(localStorage);

// Save authentication state
await authStorage.setData(
  BiometricAuthData(
    isBiometricAuthConfigured: true,
    hasDeclinedBioMetricAuth: false,
    hasDeclinedPinCodeAuth: false,
    isBiometricAuthSupported: true,
    pinCodeFallback: '1234',
    lastAuthenticated: DateTime.now(),
  ),
);

// Retrieve authentication state
final data = await authStorage.getData();
print('Biometric configured: ${data.isBiometricAuthConfigured}');
print('PIN configured: ${data.isPinConfigured}');

// Clear data
await authStorage.clearData();

API Reference

App-level facade that centralizes FaceID / fingerprint lock management and gating of critical operations (payments, transfers, PIN change, etc.). Designed for dependency injection.

Factory:

  • BiometricLockService.withDefaults() β€” wires all default implementations backed by persistent shared-preferences storage. Ideal for quick integration.
  • BiometricLockService.withStorage(LocalStorage localStorage) β€” same as above but with a custom storage backend. Use this from a service locator when your app already manages a LocalStorage instance.

Storage backends:

local_storage_impl ships with three LocalStorage implementations. Choose based on your security requirements:

Backend When to use
SharedPreferencesLocalStorage Default; plain-text persistence. Used by withDefaults().
SecureLocalStorage Production apps storing PIN codes. Backed by flutter_secure_storage (Android Keystore / iOS Keychain).
MapLocalStorage Unit tests or prototyping (in-memory, no persistence).
import 'package:local_storage_impl/local_storage_impl.dart';

// --- Secure storage (recommended for production) ---
final lockService = BiometricLockService.withStorage(
  await SecureLocalStorage.getInstance(),
);

// --- SharedPreferences (what withDefaults() does internally) ---
final lockService = BiometricLockService.withStorage(
  await SharedPreferencesLocalStorage.getInstance(),
);

// --- In-memory (tests only) ---
final lockService = BiometricLockService.withStorage(MapLocalStorage());

Methods:

  • Future<BiometricAuthData> status() β€” raw persisted state
  • Future<bool> get isEnabled β€” lock is configured and active
  • Future<bool> get isSupported β€” device supports biometrics (sensor + enrolled)
  • Future<bool> get isFaceIdAvailable β€” FaceID is available on this device
  • Future<bool> get isFingerprintAvailable β€” fingerprint is available on this device
  • Future<bool> get isPinConfigured β€” a fallback PIN has been configured
  • Future<bool> enable(...) β€” prompts the user to enable the biometric lock
  • Future<void> disable() β€” marks biometric auth as declined by the user
  • Future<BiometricUnlockResult> unlock(...) β€” prompts before a critical operation
  • Future<T?> guard<T>(action, ...) β€” runs action only if biometric check passes
  • Future<bool> unlockWithPin(String pinCode) β€” verifies the fallback PIN
  • Future<bool> setPin(String pinCode) β€” stores or replaces the fallback PIN
  • Future<void> declinePin() β€” marks PIN fallback as declined

BiometricUnlockResult

Value Meaning
success User successfully authenticated
failed Authentication failed or was cancelled
notConfigured Biometric lock isn't configured yet
notSupported Device doesn't support biometrics

ILocalAuthService

High-level authentication service interface.

Methods:

  • Future<BiometricAuthData> status() - Get current authentication status
  • Future<({bool authenticated, bool isConfigured, bool isSupported})> resolveWithBiometric(...) - Authenticate with biometrics
  • Future<({bool ok, bool isAvailable})> setupBiometric(...) - Setup biometric authentication
  • Future<({bool authenticated, bool isConfigured})> resolveWithPin(String pinCode) - Authenticate with PIN
  • Future<bool> setupPin(String pinCode) - Setup PIN code
  • Future<void> declineBioMetricAuth() - Mark biometric auth as declined
  • Future<void> declinePinCodeAuth() - Mark PIN auth as declined

IBiometricIdentityService

Low-level biometric platform interface.

Methods:

  • Future<Either<BiometricAuthException, BiometricAuthResult>> authenticate(...) - Perform biometric authentication
  • Future<List<IBiometricIdentityType>> getAvailableTypes() - Get available biometric types
  • Future<bool> isTypeAvailable(IBiometricIdentityType type) - Check if specific type is available
  • Future<BiometricIdentityTypeStatus> status() - Get device capabilities

IBiometricAuthService

Authentication state storage interface.

Methods:

  • Future<BiometricAuthData> getData() - Get stored auth data
  • Future<void> setData(BiometricAuthData data) - Save auth data
  • Future<void> clearData() - Clear stored data

BiometricAuthData

Authentication state model.

Properties:

  • bool isBiometricAuthConfigured - Is biometric auth configured
  • bool isPinConfigured - Is PIN configured (computed)
  • bool isBiometricAuthSupported - Does device support biometrics
  • bool hasDeclinedBioMetricAuth - User declined biometric setup
  • bool hasDeclinedPinCodeAuth - User declined PIN setup
  • String? pinCodeFallback - Stored PIN code
  • DateTime? lastAuthenticated - Last authentication timestamp

Exceptions

  • BiometricAuthException - Base exception class
  • BiometricNotSupportedException - Biometrics not supported on device
  • BiometricAuthFailedException - Authentication failed
  • BiometricTypeNotAvailableException - Requested biometric type unavailable
  • BiometricAuthLockoutException - Too many failed attempts

Complete Example

import 'package:biometric_auth_lock/biometric_auth_lock.dart';

class AuthManager {
  late final BiometricLockService _lockService;

  Future<void> initialize() async {
    _lockService = await BiometricLockService.withDefaults();
  }

  /// Authenticate before a critical operation (e.g., payment).
  Future<bool> authenticate() async {
    // Guard wraps the action β€” runs only if biometric check passes
    final result = await _lockService.guard(
      () async => true, // your critical operation here
      reason: 'Authentifiez-vous pour continuer',
    );

    return result ?? false;
  }

  /// Setup biometrics or fall back to PIN.
  Future<bool> setupAuthentication() async {
    // Try biometric first
    final enabled = await _lockService.enable();
    if (enabled) return true;

    // Fall back to PIN if biometrics aren't available
    if (!await _lockService.isSupported) {
      final pinCode = await showPinSetupDialog(); // Your UI
      return await _lockService.setPin(pinCode);
    }

    return false;
  }

  Future<String> showPinSetupDialog() async {
    // Your PIN setup UI
    return '1234';
  }
}

With LocalAuthService (Advanced)

import 'package:biometric_auth_lock/biometric_auth_lock.dart';
import 'package:local_storage_impl/local_storage_impl.dart';

class AuthManager {
  late final ILocalAuthService _authService;

  Future<void> initialize() async {
    final localStorage = await SharedPreferencesLocalStorage.getInstance();
    final authStorage = LocalStorageBiometricAuthService(localStorage);
    final biometricService = LocalAuthBiometricIdentityService();
    _authService = LocalAuthService(authStorage, biometricService);
  }

  Future<bool> authenticate() async {
    // Check if already configured
    final status = await _authService.status();

    if (status.isBiometricAuthConfigured) {
      // Use biometrics
      final result = await _authService.resolveWithBiometric(
        'Authenticate to continue',
        signInTitle: 'Sign In',
        cancelButtonMessage: 'Cancel',
        fallbackToPin: true,
      );
      return result.authenticated;
    } else if (status.isPinConfigured) {
      // Fall back to PIN (would show PIN input UI)
      final pinCode = await showPinInputDialog(); // Your UI
      final result = await _authService.resolveWithPin(pinCode);
      return result.authenticated;
    } else {
      // Not configured, setup first
      return await setupAuthentication();
    }
  }

  Future<bool> setupAuthentication() async {
    // Try biometric first
    final bioResult = await _authService.setupBiometric(
      'Setup biometric authentication',
      signInTitle: 'Setup',
      cancelButtonMessage: 'Skip',
    );

    if (bioResult.ok) {
      return true;
    }

    // Fall back to PIN
    if (!bioResult.isAvailable) {
      final pinCode = await showPinSetupDialog(); // Your UI
      return await _authService.setupPin(pinCode);
    }

    return false;
  }

  Future<String> showPinInputDialog() async {
    // Your PIN input UI
    return '1234';
  }

  Future<String> showPinSetupDialog() async {
    // Your PIN setup UI
    return '1234';
  }
}

Testing

The interface-based design makes testing easy:

class MockBiometricAuthService implements IBiometricAuthService {
  BiometricAuthData? _data;

  @override
  Future<BiometricAuthData> getData() async =>
      _data ?? BiometricAuthData(
        isBiometricAuthConfigured: false,
        hasDeclinedBioMetricAuth: false,
        hasDeclinedPinCodeAuth: false,
        isBiometricAuthSupported: true,
      );

  @override
  Future<void> setData(BiometricAuthData data) async {
    _data = data;
  }

  @override
  Future<void> clearData() async {
    _data = null;
  }
}

Dependencies

  • dart3z - Functional programming (Either)
  • flutter - Flutter SDK
  • json_annotation: ^4.9.0 - JSON serialization
  • local_auth: ^2.3.0 - Platform biometric auth
  • local_auth_android: ^1.0.52 - Android implementation
  • local_auth_darwin: ^1.6.0 - iOS/macOS implementation
  • local_storage_impl: ^0.0.5 - Persistent storage

License

MIT License - See LICENSE file for details.

Libraries

biometric_auth_lock
Biometric authentication and PIN lock service for Flutter