In today's world, security is no longer optional - it's expected. Whether it's a fintech app, a fitness tracker, or an internal company tool, users want fast and secure access without the hassle of remembering passwords.
That's exactly where biometric authentication comes in.
In this guide, we'll walk through how we implement biometric authentication in a Flutter app, the practical approach we follow in production, and the common mistakes developers often make (and how to avoid them).
Why Biometric Authentication?
Before jumping into implementation, let's quickly understand why it matters:
- Faster login experience (no typing passwords)
- More secure than traditional authentication
- Native support across Android & iOS
- Better user trust and retention
What We Use in Flutter
To implement biometric authentication, we rely on:
-
local_authpackage (official Flutter plugin) - Native biometric APIs under the hood (Face ID, Touch ID, Fingerprint)
Step 1: Add Dependency
dependencies:
local_auth: ^3.0.1
Then run:
flutter pub get
Step 2: Platform Setup
✅ Android Setup
Inside android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
Also ensure:
<uses-feature android:name="android.hardware.fingerprint" android:required="false"/>
✅ iOS Setup
Inside ios/Runner/Info.plist:
<key>NSFaceIDUsageDescription</key>
<string>We use Face ID to authenticate you securely</string>
⚠️ Without this, Face ID will NOT work and your app may crash.
Step 3: Implement Biometric Logic
Here's how we structure it in production:
import 'package:flutter/foundation.dart';
import 'package:local_auth/local_auth.dart';
class BiometricService {
final LocalAuthentication _auth = LocalAuthentication();
/// Check if device supports biometrics
Future<bool> isBiometricAvailable() async {
try {
final bool canCheckBiometrics = await _auth.canCheckBiometrics;
final bool isDeviceSupported = await _auth.isDeviceSupported();
return canCheckBiometrics && isDeviceSupported;
} catch (e) {
debugPrint('Biometric availability error: $e');
return false;
}
}
/// Get available biometric types (fingerprint, face, etc.)
Future<List<BiometricType>> getAvailableBiometrics() async {
try {
return await _auth.getAvailableBiometrics();
} catch (e) {
debugPrint('Error fetching biometrics: $e');
return [];
}
}
/// Authenticate user (with optional fallback to device PIN/password)
Future<bool> authenticate({
bool biometricOnly = false,
}) async {
try {
final bool isAvailable = await isBiometricAvailable();
if (!isAvailable) {
debugPrint('Biometric not available on this device');
return false;
}
final bool isAuthenticated = await _auth.authenticate(
localizedReason: 'Please authenticate to continue',
options: AuthenticationOptions(
biometricOnly: biometricOnly,
stickyAuth: true,
useErrorDialogs: true,
),
);
return isAuthenticated;
} catch (e) {
debugPrint('Authentication error: $e');
return false;
}
}
}
Step 4: Use It in UI
final BiometricService biometricService = BiometricService();
Future<void> loginWithBiometrics(BuildContext context) async {
final bool isAuthenticated = await biometricService.authenticate();
if (isAuthenticated) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Authentication successful'),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Authentication failed. Please try again'),
),
);
}
}
Works for Face ID & Fingerprint (No Extra Code Needed)
One thing we really like about this approach is that we don't have to write separate logic for different biometric types.
The same implementation works for:
- Fingerprint (Android)
- Face ID (iOS)
- Touch ID (older iOS devices)
The local_auth package automatically detects and uses the available biometric method on the device.
So whether the user is using Face ID or fingerprint - the flow remains exactly the same from our side.
What We Actually Add in Production Apps
In real apps, we don't stop at just authentication:
- ✅ Store a flag (biometric enabled/disabled) using secure storage
- ✅ Provide fallback (PIN or password)
- ✅ Show a toggle in settings
- ✅ Auto-trigger biometrics on app launch (if enabled)
Mistakes We've Seen While Implementing This
This is where most implementations go wrong 👇
1. Not Handling Unsupported Devices
Many developers assume biometrics will always be available.
👉 Always check:
✅ canCheckBiometrics
✅ isDeviceSupported
2. Ignoring iOS Permission Setup
Missing NSFaceIDUsageDescription is one of the most common issues.
👉 Result: App crash or silent failure.
3. Not Providing a Fallback Option
- Biometrics can fail due to:
- Wet fingers
- Face not recognized
- Sensor issues
👉 Always provide:
- PIN and Password fallback
4. Forcing Biometric Without User Consent
Never enable biometrics by default.
👉 Always:
- Ask user permission
- Let them enable/disable in settings
5. Poor Error Handling
Most developers just return false on failure.
👉 Instead:
- Handle exceptions properly
- Show meaningful messages to users
6. Not Testing Edge Cases
Biometric flows behave differently when:
- App goes to background
- Device is locked/unlocked
- Multiple failed attempts occur
👉 Always test:
- Background/foreground transitions
- Lock screen scenarios
7. Using Biometrics as the Only Security Layer
Biometric authentication should not replace backend authentication.
👉 Always combine with:
- Secure tokens
- Backend validation
Things That Helped Us Get This Right
From our experience, this is what works best:
- Keep biometric logic separate (service class)
- Always provide fallback authentication
- Respect user choice and privacy
- Test on real devices (not just emulators)
- Keep UX smooth and non-intrusive
Final Thoughts
Biometric authentication is powerful - but only when implemented thoughtfully.
It's not just about adding a fingerprint or Face ID button. It's about creating a secure, seamless, and reliable user experience.
When done right, it can significantly improve both security and user satisfaction.
If you're building a Flutter app and want to level up your authentication flow, this is one feature you shouldn't skip.
Top comments (0)