Harsh Mittal
Back to blog
2026-02-15ยท4 min read

๐Ÿ” Biometric Login in Flutter (Riverpod + Bloc)

Fintech securityFlutterAlso on Medium

Biometric Login in Flutter (Riverpod + Bloc)

If you're building a fintech, banking, or wallet app, biometric login must be secure, structured, testable, cleanly architected, and token-safe.

This guide covers a Riverpod-based implementation, a Bloc-based implementation, fintech-grade secure architecture, secure token flow, and the architecture diagram behind it.

Fintech-Grade Secure Architecture

Before writing code, understand the security flow.

Fintech-grade secure architecture

Secure biometric login flow (production pattern):

First-time login โ€” user logs in via email/password, backend returns an access token and a refresh token, tokens are stored using flutter_secure_storage, and the user enables "Biometric Login."

Next app launch โ€” the app checks if a token exists, triggers the biometric prompt, and on success unlocks the stored token and navigates to the dashboard. On failure, it shows a PIN/password fallback.

Clean architecture layers:

Presentation (UI + Riverpod/Bloc)
โ†“
Domain (UseCases)
โ†“
Data (Repositories)
โ†“
Local Auth + Secure Storage
โ†“
Backend API

This separation makes your app testable, scalable, and enterprise-ready.

1. Riverpod Biometric Implementation

Riverpod works beautifully for secure state management.

Folder structure:

lib/
โ”œโ”€โ”€ core/
โ”‚   โ”œโ”€โ”€ biometric_service.dart
โ”‚   โ”œโ”€โ”€ secure_storage_service.dart
โ”œโ”€โ”€ features/auth/
โ”‚   โ”œโ”€โ”€ auth_repository.dart
โ”‚   โ”œโ”€โ”€ biometric_provider.dart
โ”‚   โ”œโ”€โ”€ login_screen.dart

Biometric service:

import 'package:local_auth/local_auth.dart';

class BiometricService {
  final LocalAuthentication _auth = LocalAuthentication();

  Future<bool> authenticate() async {
    try {
      return await _auth.authenticate(
        localizedReason: "Authenticate to access your account",
        options: const AuthenticationOptions(
          biometricOnly: true,
          stickyAuth: true,
        ),
      );
    } catch (_) {
      return false;
    }
  }
}

Secure storage service:

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class SecureStorageService {
  final _storage = const FlutterSecureStorage();

  Future<void> saveToken(String token) async {
    await _storage.write(key: "auth_token", value: token);
  }

  Future<String?> getToken() async {
    return await _storage.read(key: "auth_token");
  }

  Future<void> clear() async {
    await _storage.deleteAll();
  }
}

Riverpod providers:

import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/biometric_service.dart';
import '../../core/secure_storage_service.dart';

final biometricServiceProvider = Provider((ref) => BiometricService());
final secureStorageProvider = Provider((ref) => SecureStorageService());

final biometricAuthProvider =
    StateNotifierProvider<BiometricAuthNotifier, AsyncValue<bool>>((ref) {
  return BiometricAuthNotifier(ref);
});

class BiometricAuthNotifier extends StateNotifier<AsyncValue<bool>> {
  final Ref ref;
  BiometricAuthNotifier(this.ref) : super(const AsyncValue.data(false));

  Future<void> authenticate() async {
    state = const AsyncValue.loading();
    final biometric = ref.read(biometricServiceProvider);
    final storage = ref.read(secureStorageProvider);
    final token = await storage.getToken();

    if (token == null) {
      state = const AsyncValue.error("No stored token", StackTrace.empty);
      return;
    }

    final success = await biometric.authenticate();
    state = AsyncValue.data(success);
  }
}

Riverpod login UI:

class LoginScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final authState = ref.watch(biometricAuthProvider);

    return Scaffold(
      body: Center(
        child: authState.when(
          data: (success) => success
              ? const Text("Welcome Back!")
              : ElevatedButton(
                  onPressed: () =>
                      ref.read(biometricAuthProvider.notifier).authenticate(),
                  child: const Text("Login with Biometrics"),
                ),
          loading: () => const CircularProgressIndicator(),
          error: (e, _) => Text("Error: $e"),
        ),
      ),
    );
  }
}

2. Bloc Biometric Implementation

Bloc is preferred in larger fintech systems with event-driven flows.

flutter_bloc: ^8.1.0

Bloc events:

abstract class BiometricEvent {}

class BiometricLoginRequested extends BiometricEvent {}

Bloc states:

abstract class BiometricState {}

class BiometricInitial extends BiometricState {}

class BiometricLoading extends BiometricState {}

class BiometricSuccess extends BiometricState {}

class BiometricFailure extends BiometricState {
  final String message;
  BiometricFailure(this.message);
}

Bloc implementation:

class BiometricBloc extends Bloc<BiometricEvent, BiometricState> {
  final BiometricService biometricService;
  final SecureStorageService storage;

  BiometricBloc(this.biometricService, this.storage)
      : super(BiometricInitial()) {
    on<BiometricLoginRequested>(_onLoginRequested);
  }

  Future<void> _onLoginRequested(
      BiometricLoginRequested event, Emitter emit) async {
    emit(BiometricLoading());
    final token = await storage.getToken();

    if (token == null) {
      emit(BiometricFailure("No token found"));
      return;
    }

    final success = await biometricService.authenticate();
    if (success) {
      emit(BiometricSuccess());
    } else {
      emit(BiometricFailure("Authentication failed"));
    }
  }
}

Bloc UI:

BlocBuilder<BiometricBloc, BiometricState>(
  builder: (context, state) {
    if (state is BiometricLoading) {
      return const CircularProgressIndicator();
    } else if (state is BiometricSuccess) {
      return const Text("Welcome!");
    } else if (state is BiometricFailure) {
      return Text(state.message);
    }
    return ElevatedButton(
      onPressed: () =>
          context.read<BiometricBloc>().add(BiometricLoginRequested()),
      child: const Text("Login with Biometrics"),
    );
  },
)

Fintech Security Enhancements (Advanced)

  1. Enable biometricOnly in fintech apps โ€” biometricOnly: true.
  2. Use device integrity checks โ€” Android Play Integrity API, iOS DeviceCheck API.
  3. Protect against rooted/jailbroken devices โ€” packages like flutter_jailbreak_detection.
  4. Use short token expiry โ€” access token 15 min, refresh token securely stored.
  5. Logout if biometric fails multiple times.

Riverpod or Bloc โ€” Which Fits Fintech Better?

Both are production-grade choices. Riverpod tends to feel lighter for smaller, focused auth flows; Bloc's explicit event/state model tends to fit larger, event-driven fintech systems with many moving parts better. The right choice depends on your team's existing architecture more than the feature itself.

Interview-Level Talking Points

If asked "How would you implement biometric login in a fintech app?" โ€” use secure storage for tokens, never store the password locally, use biometrics only after first login, provide a fallback mechanism, handle locked-out cases, separate presentation and domain layers, and use DI with a testable architecture.

That answer immediately sounds senior-level.

Final Thoughts

Biometric login is easy to implement. But fintech-grade biometric login requires clean architecture, secure token management, proper state management (Riverpod/Bloc), security edge case handling, and device integrity validation.

Get new posts by email

No spam, no schedule โ€” just an email when a new post goes up.