Building Login with Face ID / Fingerprint in Flutter
Biometric authentication, such as Face ID and fingerprint scanning, offers a secure and convenient way for users to log into applications. Integrating these features into your Flutter app enhances security and improves the user experience by eliminating the need to remember complex passwords. This article will guide you through the process of implementing biometric authentication in a Flutter application using the local_auth package.
Prerequisites
- A basic understanding of Flutter development.
- Flutter SDK installed.
- An IDE (VS Code or Android Studio).
- A physical device or an emulator/simulator with biometric capabilities configured.
Step 1: Project Setup
First, you need to add the local_auth package to your pubspec.yaml file. This package provides a cross-platform way to interact with biometric sensors.
dependencies:
flutter:
sdk: flutter
local_auth: ^2.1.8 # Use the latest stable version
After adding the dependency, run flutter pub get in your terminal to fetch the package.
Step 2: Platform-Specific Configuration
Biometric authentication requires specific permissions and configurations on both iOS and Android platforms.
iOS Configuration
For iOS, you need to add a usage description to your Info.plist file, explaining why your app requires biometric authentication. This description will be shown to the user when the app requests access to Face ID or Touch ID.
Open ios/Runner/Info.plist and add the following key-value pair:
<key>NSFaceIDUsageDescription</key>
<string>Why is my app using Face ID? For example: To quickly authenticate you for secure access to your account.</string>
Android Configuration
For Android, you need to declare the USE_BIOMETRIC permission in your AndroidManifest.xml file. This permission is necessary for the app to access biometric hardware.
Open android/app/src/main/AndroidManifest.xml and add the following permission tag inside the <manifest> tag, but outside the <application> tag:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.your_app_name">
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<!-- Optionally, for older Android versions, you might need USE_FINGERPRINT -->
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
<application
android:label="your_app_name"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<!-- ... other application configurations ... -->
</application>
</manifest>
Step 3: Implementing Biometric Authentication
Now, let's write the Flutter code to interact with the biometric sensors.
Import the Package
First, import the local_auth package in your Dart file:
import 'package:local_auth/local_auth.dart';
import 'package:flutter/services.dart'; // For PlatformException
Check Biometric Availability
Before attempting to authenticate, it's good practice to check if biometrics are available on the device and if the user has enrolled any biometrics.
final LocalAuthentication auth = LocalAuthentication();
bool _canCheckBiometrics = false;
List<BiometricType> _availableBiometrics = [];
Future<void> _checkBiometrics() async {
late bool canCheckBiometrics;
try {
canCheckBiometrics = await auth.canCheckBiometrics;
} on PlatformException catch (e) {
canCheckBiometrics = false;
print(e);
}
if (!mounted) {
return;
}
setState(() {
_canCheckBiometrics = canCheckBiometrics;
});
}
Future<void> _getAvailableBiometrics() async {
late List<BiometricType> availableBiometrics;
try {
availableBiometrics = await auth.getAvailableBiometrics();
} on PlatformException catch (e) {
availableBiometrics = <BiometricType>[];
print(e);
}
if (!mounted) {
return;
}
setState(() {
_availableBiometrics = availableBiometrics;
});
}
Authenticate the User
The core function for authentication is authenticate(). It takes a localizedReason which is the message displayed to the user during the biometric prompt.
Future<void> _authenticate() async {
bool authenticated = false;
try {
authenticated = await auth.authenticate(
localizedReason: 'Scan your fingerprint or face to authenticate',
options: const AuthenticationOptions(
stickyAuth: true, // Keep the authentication dialog visible until dismissed or authenticated
useErrorDialogs: true, // Show platform specific error messages
),
);
} on PlatformException catch (e) {
print(e);
// Handle specific errors like not enrolled, locked out, etc.
return;
}
if (!mounted) {
return;
}
// Update UI based on 'authenticated' status
setState(() {
// For example, navigate to home screen or show success message
if (authenticated) {
print("Authentication successful!");
} else {
print("Authentication failed!");
}
});
}
Full Example: Login Widget
Here's a simple StatefulWidget demonstrating how to integrate these methods into a login screen.
import 'package:flutter/material.dart';
import 'package:local_auth/local_auth.dart';
import 'package:flutter/services.dart';
class BiometricLoginPage extends StatefulWidget {
const BiometricLoginPage({Key? key}) : super(key: key);
@override
State<BiometricLoginPage> createState() => _BiometricLoginPageState();
}
class _BiometricLoginPageState extends State<BiometricLoginPage> {
final LocalAuthentication auth = LocalAuthentication();
bool _canCheckBiometrics = false;
List<BiometricType> _availableBiometrics = [];
String _authorized = 'Not Authorized';
@override
void initState() {
super.initState();
_checkBiometrics();
_getAvailableBiometrics();
}
Future<void> _checkBiometrics() async {
late bool canCheckBiometrics;
try {
canCheckBiometrics = await auth.canCheckBiometrics;
} on PlatformException catch (e) {
canCheckBiometrics = false;
print(e);
}
if (!mounted) {
return;
}
setState(() {
_canCheckBiometrics = canCheckBiometrics;
});
}
Future<void> _getAvailableBiometrics() async {
late List<BiometricType> availableBiometrics;
try {
availableBiometrics = await auth.getAvailableBiometrics();
} on PlatformException catch (e) {
availableBiometrics = <BiometricType>[];
print(e);
}
if (!mounted) {
return;
}
setState(() {
_availableBiometrics = availableBiometrics;
});
}
Future<void> _authenticate() async {
bool authenticated = false;
try {
setState(() {
_authorized = 'Authenticating...';
});
authenticated = await auth.authenticate(
localizedReason: 'Please authenticate to access your account',
options: const AuthenticationOptions(
stickyAuth: true,
useErrorDialogs: true,
),
);
} on PlatformException catch (e) {
print(e);
setState(() {
_authorized = 'Error: ${e.message}';
});
return;
}
if (!mounted) {
return;
}
setState(() {
_authorized = authenticated ? 'Authorized' : 'Not Authorized';
if (authenticated) {
// Navigate to the home screen or perform other actions
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Authentication Successful!')),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Authentication Failed!')),
);
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Biometric Login'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Can check biometrics: $_canCheckBiometrics'),
Text('Available biometrics: ${_availableBiometrics.join(', ')}'),
Text('Current authorization status: $_authorized'),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _canCheckBiometrics && _availableBiometrics.isNotEmpty
? _authenticate
: null, // Disable button if biometrics not available
child: const Text('Authenticate with Biometrics'),
),
const SizedBox(height: 10),
TextButton(
onPressed: () {
// Handle traditional login (username/password)
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Proceeding to traditional login')),
);
},
child: const Text('Use Password Login'),
),
],
),
),
);
}
}
Step 4: Handling Authentication States and Errors
It's crucial to provide clear feedback to the user about the authentication status:
- Success: Navigate the user to the main application screen.
- Failure: Inform the user that authentication failed and perhaps offer an alternative login method (e.g., password).
- Not Available/Enrolled: If
_canCheckBiometricsisfalseor_availableBiometricsis empty, disable the biometric login button and suggest traditional login methods. - Platform Exceptions: The
authenticatemethod can throwPlatformExceptionfor various reasons (e.g., biometrics not enrolled, device security features not set up, user cancelled, too many failed attempts). Catching these exceptions allows you to provide more specific error messages to the user.
You can also offer an option for users to switch to traditional password-based login if they prefer not to use biometrics or if biometric authentication fails multiple times.
Conclusion
Integrating Face ID or fingerprint authentication significantly enhances the user experience and security of your Flutter application. By following these steps, you can successfully implement biometric login, making your app more convenient and secure for your users. Remember to thoroughly test your implementation on various devices and platforms to ensure a robust and reliable solution.