01Process02Projects03About04FAQ05Blog06Hire Me

25 products shipped · $10M+ raised by clients

Back to Blog
Case StudyMay 15, 20258 min read

How We Built Biometric Authentication Into a Fintech Donation App

Face ID on iOS. Fingerprint on Android. One Flutter codebase. Here's exactly how we integrated biometric authentication into OneDonate — a production fintech app — without touching the existing backend auth architecture.

Subhankar Denria

Subhankar Denria

Software Architect · Product Engineer

How We Built Biometric Authentication Into a Fintech Donation App

While building OneDonate — a platform focused on making charitable giving fast and accessible — we noticed something during testing. Users would open the app intending to donate within seconds, but the authentication flow was getting in the way.

  • Open app
  • Enter email
  • Enter password
  • Authenticate
  • Continue to payment

Biometric authentication in OneDonate — Face ID on iOS, fingerprint on Android

For a product where the entire value proposition is frictionless giving, every extra second matters. We wanted to reduce that friction without compromising security. The solution: Face ID on iOS, fingerprint on Android, unified in a single Flutter codebase.

The Goal — And What It Wasn't

The objective wasn't simply 'add biometrics.' The real challenge was integrating biometric authentication into an existing production architecture cleanly — without breaking session handling, duplicating auth logic, or creating platform-specific codepaths.

  • Preserve existing session and token handling
  • Keep backend authentication unchanged
  • Support platform-native UX (Face ID on iPhone, fingerprint on Android)
  • Avoid duplicating authentication logic across platforms
  • Handle failure gracefully — invalid credentials, unenrolled devices, hardware unavailability

Biometrics would extend the authentication flow — not replace it.

The Core Security Model (Most Developers Get This Wrong)

A common misconception: Face ID or fingerprint authentication directly authenticates a user with the server. That is not how production-grade biometric auth works, and building it that way would be a security mistake.

The correct model:

  1. 1Biometrics unlock secure local credentials stored on the device
  2. 2The app retrieves the stored authentication token
  3. 3Standard server-side authentication proceeds with that token

The backend never knows or cares that biometrics were involved. It just receives a valid token. This keeps the server auth model simple and unchanged.

Authentication Flow

User

Opens app

Biometric Prompt

Face ID · Fingerprint

Secure Storage

Keychain · EncryptedPrefs

Server Auth

Laravel API

App Access

Authenticated session

The Implementation

The stack already included Flutter, Riverpod, Laravel APIs, flutter_secure_storage, and Stripe Connect. We added two packages: local_auth for the biometric prompt, and continued using flutter_secure_storage for hardware-backed credential storage — iOS Keychain on iPhone, EncryptedSharedPreferences on Android.

Architecture Layers

Flutter UI

Screens · Widgets · Riverpod state

Biometric Service

local_auth — wraps platform biometric APIs

Secure Storage

iOS Keychain · Android EncryptedSharedPreferences

Laravel Backend

REST API · Sanctum token auth

Stripe Connect

Payment & subscription layer

We wrapped everything in a single BiometricAuthService to keep the rest of the app's auth flow untouched:

dart
class BiometricAuthService {
  final LocalAuthentication _auth = LocalAuthentication();
  final FlutterSecureStorage _storage = const FlutterSecureStorage();

  Future<bool> isAvailable() async {
    return await _auth.canCheckBiometrics ||
           await _auth.isDeviceSupported();
  }

  Future<bool> authenticate() async {
    try {
      return await _auth.authenticate(
        localizedReason: 'Sign in to OneDonate',
        options: const AuthenticationOptions(
          stickyAuth: true,
          biometricOnly: true,
        ),
      );
    } on PlatformException {
      await _clearCredentials();
      return false;
    }
  }

  Future<void> storeCredentials(String token) async {
    await _storage.write(key: 'biometric_token', value: token);
    await _storage.write(key: 'biometric_enabled', value: 'true');
  }

  Future<String?> retrieveToken() async {
    final enabled = await _storage.read(key: 'biometric_enabled');
    if (enabled != 'true') return null;
    return _storage.read(key: 'biometric_token');
  }

  Future<void> _clearCredentials() async {
    await _storage.delete(key: 'biometric_token');
    await _storage.write(key: 'biometric_enabled', value: 'false');
  }
}

Authentication Flow

First-Time Login

After a successful email/password login, we prompt the user to enable biometrics. Timing matters here — prompting immediately after a successful login produced the best adoption rates. The context feels natural because the user just authenticated.

dart
// After successful login, offer biometric setup
if (await biometricService.isAvailable()) {
  final enable = await showEnableBiometricsDialog(context);
  if (enable) {
    await biometricService.storeCredentials(authToken);
  }
}

Returning User Login

On subsequent opens, the app checks for stored biometric credentials and triggers the native prompt. If successful, it retrieves the token and proceeds with server authentication as normal.

dart
// On app open — check for biometric login
final token = await biometricService.retrieveToken();

if (token != null) {
  final authenticated = await biometricService.authenticate();
  if (authenticated) {
    // Standard API call with token — backend unchanged
    await authRepository.loginWithToken(token);
  }
} else {
  // No biometric credentials — show standard login
  navigateToEmailLogin();
}

The Payment Confirmation Layer

The most impactful part of this implementation was extending biometrics beyond login. We reused the same authenticate() call to confirm sensitive actions before they executed:

  • Donation submission
  • Subscription setup
  • High-value payment actions
  • Tap-to-Pay confirmation flows

This created a banking-style confirmation pattern — familiar, trusted, fast — without writing any new auth infrastructure. The biometric prompt IS the confirmation. No extra taps, no modal dialogs.

Architecture Decisions That Mattered

1. Biometrics as a Retrieval Mechanism, Not an Auth System

Treating biometrics purely as a secure key to unlock stored credentials — rather than a server-auth mechanism — kept the backend completely unchanged. The Laravel API never needed to know biometrics existed. That's a good sign: the feature is additive, not invasive.

2. Platform-Native UX Without Platform-Specific Code

local_auth automatically uses Face ID on iPhone and fingerprint on Android, and surfaces the correct system prompt for each. We didn't write a single platform-specific UI string — it adapts based on device capabilities. One codebase, two native experiences.

3. Security-First Fallback Handling

Biometric credentials can become invalid — the user re-enrols their fingerprint, a new face is added to the device, or hardware becomes unavailable. We handle all of these the same way: catch the PlatformException, clear stored credentials, redirect to standard login. No broken states.

  • Biometric access is automatically disabled on any PlatformException
  • Stored credentials are cleaned from secure storage immediately
  • The app falls back to standard email/password login without user confusion

What We Learned

Authentication friction is a silent conversion killer. It doesn't show up obviously in analytics — users just abandon the flow without logging it as a specific pain point. Removing it compounds across every session: better retention, higher payment completion, more trust in the product.

The implementation took less time than expected precisely because we didn't try to change the backend. If you're adding biometrics to an existing app, the biggest decision is architectural: biometrics unlock credentials, they don't replace your auth system. Get that right and the rest is straightforward.

The most interesting part technically wasn't the biometric APIs. It was designing an authentication layer that could remain secure, scale cleanly, and integrate into a production fintech workflow without touching existing systems.

Let's connect

Choose your preferred way

Available for new projects