Biometric Auth Lock
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
Recommended: BiometricLockService
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
BiometricLockService (Recommended)
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 aLocalStorageinstance.
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 stateFuture<bool> get isEnabledβ lock is configured and activeFuture<bool> get isSupportedβ device supports biometrics (sensor + enrolled)Future<bool> get isFaceIdAvailableβ FaceID is available on this deviceFuture<bool> get isFingerprintAvailableβ fingerprint is available on this deviceFuture<bool> get isPinConfiguredβ a fallback PIN has been configuredFuture<bool> enable(...)β prompts the user to enable the biometric lockFuture<void> disable()β marks biometric auth as declined by the userFuture<BiometricUnlockResult> unlock(...)β prompts before a critical operationFuture<T?> guard<T>(action, ...)β runsactiononly if biometric check passesFuture<bool> unlockWithPin(String pinCode)β verifies the fallback PINFuture<bool> setPin(String pinCode)β stores or replaces the fallback PINFuture<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 statusFuture<({bool authenticated, bool isConfigured, bool isSupported})> resolveWithBiometric(...)- Authenticate with biometricsFuture<({bool ok, bool isAvailable})> setupBiometric(...)- Setup biometric authenticationFuture<({bool authenticated, bool isConfigured})> resolveWithPin(String pinCode)- Authenticate with PINFuture<bool> setupPin(String pinCode)- Setup PIN codeFuture<void> declineBioMetricAuth()- Mark biometric auth as declinedFuture<void> declinePinCodeAuth()- Mark PIN auth as declined
IBiometricIdentityService
Low-level biometric platform interface.
Methods:
Future<Either<BiometricAuthException, BiometricAuthResult>> authenticate(...)- Perform biometric authenticationFuture<List<IBiometricIdentityType>> getAvailableTypes()- Get available biometric typesFuture<bool> isTypeAvailable(IBiometricIdentityType type)- Check if specific type is availableFuture<BiometricIdentityTypeStatus> status()- Get device capabilities
IBiometricAuthService
Authentication state storage interface.
Methods:
Future<BiometricAuthData> getData()- Get stored auth dataFuture<void> setData(BiometricAuthData data)- Save auth dataFuture<void> clearData()- Clear stored data
BiometricAuthData
Authentication state model.
Properties:
bool isBiometricAuthConfigured- Is biometric auth configuredbool isPinConfigured- Is PIN configured (computed)bool isBiometricAuthSupported- Does device support biometricsbool hasDeclinedBioMetricAuth- User declined biometric setupbool hasDeclinedPinCodeAuth- User declined PIN setupString? pinCodeFallback- Stored PIN codeDateTime? lastAuthenticated- Last authentication timestamp
Exceptions
BiometricAuthException- Base exception classBiometricNotSupportedException- Biometrics not supported on deviceBiometricAuthFailedException- Authentication failedBiometricTypeNotAvailableException- Requested biometric type unavailableBiometricAuthLockoutException- Too many failed attempts
Complete Example
With BiometricLockService (Recommended)
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 SDKjson_annotation: ^4.9.0- JSON serializationlocal_auth: ^2.3.0- Platform biometric authlocal_auth_android: ^1.0.52- Android implementationlocal_auth_darwin: ^1.6.0- iOS/macOS implementationlocal_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