Implementing Biometric Auth in Flutter: A Step-by-Step Guide with local_auth

By Chioma Eze · 6 August 20267,568 views
Implementing Biometric Auth in Flutter: A Step-by-Step Guide with local_auth

Introduction: The Security Paradox of Biometric Authentication

When we talk about biometric authentication in Flutter, we are rarely talking about the biometric sensor itself. As mobile engineers at fintech startups like mine here in Abuja, we are actually managing a bridge between a high-level Dart API and the platform-specific Secure Enclave (iOS) or Trusted Execution Environment (TEE) (Android). The local_auth package is the industry standard for this bridge, but relying on its default configuration is a common pitfall.

If you treat biometric authentication as a simple Boolean check—if (authenticated) { showDashboard() }—you are building a house of cards. True biometric integration in a production fintech application requires a deep understanding of how the operating system handles authentication state, key persistence, and the threat of hardware-backed compromise. In this article, we won't just implement the authenticate() method; we will analyze the interaction between the flutter framework and the underlying hardware security modules to ensure your users' data remains protected even if the device’s OS is compromised.

The Threat Model: What Are We Actually Securing?

Before writing a single line of Dart, we must define our threat model. Biometric authentication is a local gatekeeper, not a network security protocol. When a user scans their fingerprint, the local OS verifies the credential and returns a success token. If your app simply uses this token to bypass a local UI, a malicious actor with root access can easily bypass the local_auth check by modifying the application's binary or hooking the framework methods.

To build production-grade security, you must implement Biometric Binding. This involves generating a cryptographic key inside the hardware-backed keystore that is marked as "user-authentication required." The biometric sensor is then required to unlock that key. By ensuring that your app cannot decrypt sensitive tokens (like OAuth refresh tokens or private keys) without a successful biometric event, you tie the authentication to the hardware rather than just the application state.

Implementing the local_auth Integration

To get started, we add the local_auth package to our pubspec.yaml. However, the real work happens in the platform configuration. On Android, you must explicitly declare the USE_BIOMETRIC permission and update your MainActivity to extend FlutterFragmentActivity. Failure to use FlutterFragmentActivity is a frequent source of runtime exceptions when using local_auth because the standard FlutterActivity lacks the necessary lifecycle hooks for biometric prompts.

Step-by-Step Implementation

  1. Configuration: Ensure your Android AndroidManifest.xml includes the necessary permission.
  2. Check Availability: Always probe the device hardware before attempting an auth flow.
  3. Authenticate: Trigger the prompt with platform-specific options.
import 'package:local_auth/local_auth.dart';
import 'package:flutter/services.dart';

final LocalAuthentication auth = LocalAuthentication();

Future<bool> authenticateUser() async {
  try {
    final bool canAuthenticateWithBiometrics = await auth.canCheckBiometrics;
    final bool canAuthenticate = canAuthenticateWithBiometrics || await auth.isDeviceSupported();

    if (!canAuthenticate) return false;

    return await auth.authenticate(
      localizedReason: 'Please authenticate to access your account',
      options: const AuthenticationOptions(
        biometricOnly: true,
        stickyAuth: true,
      ),
    );
  } on PlatformException catch (e) {
    // Handle specific hardware errors (e.g., locked out)
    return false;
  }
}

Platform-Specific Gotchas: Keychain vs. Keystore

As a contributor to flutter_secure_storage, I often see developers confuse biometric authentication with biometric protection of storage. local_auth is transient. When the authentication succeeds, you get a boolean back. That's it. It does not provide a session token that persists across app restarts.

On iOS, the Keychain allows you to set an accessibility constant—kSecAccessControlUserPresence. When you store data in the Keychain with this constant, the OS automatically triggers the biometric prompt when the app attempts to read the data. This is significantly more secure than the local_auth check because the secret is inaccessible unless the biometric requirement is met.

On Android, this maps to the KeyGenParameterSpec where you set setUserAuthenticationRequired(true). When you implement this, the key is only decrypted inside the TEE after a successful biometric prompt. This is the gold standard for banking apps. If you are building a fintech app, do not rely on local_auth alone to guard data; use it to guard the keys that guard the data.

Package Internals: How local_auth Interfaces with the OS

The local_auth package essentially wraps the BiometricPrompt API on Android and LocalAuthentication framework on iOS. When you call authenticate(), the package creates a transient platform channel request. If you look at the Android source code within the package, you will notice that it instantiates a BiometricPrompt.PromptInfo object.

One critical detail often ignored is the biometricOnly flag. If this is set to false, the device might fall back to the PIN or pattern lock. While this provides a better UX for users who have damaged fingerprints, it weakens your security posture if your threat model assumes physical biometric presence. In the Nigerian market, where we deal with diverse device hardware, I recommend always auditing whether your business logic allows PIN fallback.

Troubleshooting and Production Best Practices

  1. Handle Lockouts: Biometric hardware enters a lockout state after too many failures. Your UI must handle the AuthError.lockedOut and AuthError.permanentlyLockedOut codes. You cannot "unlock" this from within the app; the user must use their device PIN/password to re-enable biometric access.
  2. Device Inconsistency: Not all Android devices implement the BiometricPrompt API correctly. Some manufacturers have bespoke implementations that lead to subtle bugs in the local_auth callback. Always test on real hardware, especially budget-friendly handsets that might use fingerprint sensors with higher false acceptance rates (FAR).
  3. Backgrounding: Android's stickyAuth option is essential. If a user receives a notification or switches apps during the authentication process, stickyAuth ensures the prompt remains valid or handles the interruption gracefully according to the OS lifecycle.
  4. Security Audits: If your app processes sensitive transactions, use local_auth alongside a server-side challenge-response mechanism. Even if the user authenticates locally, the server should verify the integrity of the request.

Pro Tips for the Enterprise

  • Pro Tip 1: Always use the biometricOnly option if your security policy requires actual biometric capture. Do not confuse "device unlock" (PIN/Pattern) with "biometric unlock".
  • Pro Tip 2: Avoid caching the authentication state in a global variable like isAuthorized = true. This is a memory-only state that can be flipped via reflection or debugging tools. Instead, have your sensitive service classes request a "fresh" cryptographic key from the Secure Enclave every time an operation is performed.

Conclusion: Building for Trust

Implementing biometric authentication is a significant responsibility for a mobile engineer. By moving beyond a simple boolean flag and integrating hardware-backed key storage, we move our apps from "protected by UI" to "protected by hardware."

Remember that the Flutter layer is just the final mile. Your true security is defined in the Keychain (iOS) and the KeyStore/TEE (Android). Keep your implementation focused on hardware-backed primitives, handle your platform exceptions with the gravity they deserve, and always assume that the local environment can be compromised. In a fintech context, trust isn't just a design principle—it's a technical requirement. Keep building secure, and always verify what happens at the platform level when you push that biometric button.

Comments

No comments yet. Be the first!

Sign in to leave a comment.