Implementasi Biometric Authentication di Flutter dengan Local Auth: Fingerprint dan Face ID

Lhuqita Fazry
Mobile Development Flutter Biometric Authentication Local Auth Keamanan Aplikasi
Implementasi Biometric Authentication di Flutter dengan Local Auth: Fingerprint dan Face ID

Mengapa Autentikasi Biometrik Menjadi Standar Keamanan Aplikasi Mobile

Autentikasi biometrik memanfaatkan karakteristik fisik unik pengguna, seperti sidik jari dan wajah, sebagai pengganti password. Setiap orang memiliki pola sidik jari dan struktur wajah yang berbeda, sehingga identitas ini jauh lebih sulit dipalsukan dibanding kombinasi huruf dan angka. Pengalaman login pun menjadi lebih cepat karena pengguna cukup menempelkan jari atau menatap kamera depan, tanpa perlu mengetik password yang panjang.

Peran fitur ini sangat terasa pada aplikasi yang menyimpan data sensitif. Saldo e-wallet, catatan medis, dan aplikasi perbankan adalah contoh nyata yang membutuhkan lapisan keamanan ekstra. Dengan biometrik, akses ke data tersebut hanya bisa dilakukan oleh pemilik perangkat yang terdaftar. Fitur ini mengurangi risiko pencurian akun ketika perangkat jatuh ke tangan orang lain.

Di ekosistem Flutter, kita bisa mengimplementasikan fitur ini menggunakan plugin local_auth. Plugin ini membungkus platform biometric API yang sudah tersedia di masing-masing sistem operasi. Android memakai BiometricPrompt, sedangkan iOS memakai LocalAuthentication. Kita cukup menulis satu kode Dart dan plugin akan menerjemahkannya ke API native yang sesuai. Artikel ini akan membahas implementasi lengkapnya, mulai dari konfigurasi platform sampai integrasi ke UI.

Ilustrasi konsep autentikasi biometrik pada smartphone

Gambar: Ilustrasi konsep autentikasi biometrik pada smartphone — Sumber: Unsplash

Persiapan Proyek Flutter dan Konfigurasi Platform

Langkah pertama adalah menambahkan dependency local_auth ke pubspec.yaml. Jalankan perintah flutter pub add local_auth atau tambahkan secara manual di bagian dependencies. Setelah itu, jalankan flutter pub get untuk mengunduh package.

yaml
dependencies:
  flutter:
    sdk: flutter
  local_auth: ^2.2.0

Setelah dependency terpasang, kita perlu mengonfigurasi izin di masing-masing platform. Android membutuhkan deklarasi permission USE_BIOMETRIC dan USE_FINGERPRINT di AndroidManifest.xml. Permission USE_BIOMETRIC adalah standar modern untuk API level 28 ke atas, sedangkan USE_FINGERPRINT diperlukan untuk kompatibilitas dengan perangkat lama.

xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.USE_BIOMETRIC"/>
    <uses-permission android:name="android.permission.USE_FINGERPRINT"/>
</manifest>

Di sisi iOS, konfigurasi yang diperlukan adalah key NSFaceIDUsageDescription di Info.plist. Key ini berisi alasan penggunaan Face ID yang akan ditampilkan kepada pengguna saat pertama kali meminta izin. Apple mewajibkan key ini ada, jika tidak, aplikasi akan crash saat memanggil autentikasi Face ID.

xml
<key>NSFaceIDUsageDescription</key>
<string>Gunakan Face ID untuk membuka aplikasi dengan cepat dan aman.</string>

Perlu dicatat bahwa Android membutuhkan emulator dengan API level 28 ke atas atau perangkat fisik yang memiliki sensor biometrik. Emulator dengan API level di bawah 28 tidak akan mendukung fitur ini. Untuk pengujian awal, perangkat fisik dengan fingerprint sensor adalah pilihan paling andal.

Satu hal yang sering terlewat adalah persyaratan minSdkVersion. Plugin local_auth membutuhkan Android SDK minimal 23, yang setara dengan Android 6.0. Jika proyek kita masih memakai minSdkVersion di bawah angka tersebut, build akan gagal dengan pesan error yang jelas. Kita bisa menyesuaikannya di android/app/build.gradle dengan menetapkan minSdkVersion 23 atau memakai flutter.minSdkVersion agar selalu mengikuti nilai default Flutter. Untuk versi plugin, sebaiknya kita memakai caret constraint seperti ^2.2.0 agar Flutter mengambil patch terbaru yang kompatibel, sekaligus menghindari perubahan major yang bisa memecah kode.

Membuat Layanan Autentikasi Biometrik dengan Local Auth

Setelah konfigurasi platform selesai, kita buat class service yang membungkus seluruh logika autentikasi. Class BiometricAuthService ini memisahkan urusan biometrik dari UI, sehingga halaman login tetap bersih dan mudah diuji.

dart
import 'package:local_auth/local_auth.dart';

class BiometricAuthService {
  final LocalAuthentication _localAuth = LocalAuthentication();

  Future<bool> canAuthenticate() async {
    final isSupported = await _localAuth.isDeviceSupported();
    final hasBiometrics = await _localAuth.canCheckBiometrics;
    return isSupported && hasBiometrics;
  }

  Future<List<BiometricType>> getAvailableBiometrics() async {
    return await _localAuth.getAvailableBiometrics();
  }

  Future<bool> authenticate() async {
    try {
      return await _localAuth.authenticate(
        localizedReason: 'Verifikasi identitas untuk membuka aplikasi',
        options: const AuthenticationOptions(
          biometricOnly: true,
          useErrorDialogs: true,
          stickyAuth: true,
        ),
      );
    } on Exception catch (e) {
      print('Authentication error: $e');
      return false;
    }
  }
}

Method canAuthenticate() menggabungkan dua pengecekan. isDeviceSupported() memastikan perangkat memiliki hardware biometrik, sedangkan canCheckBiometrics memastikan ada biometrik yang terdaftar. Kedua kondisi ini harus terpenuhi sebelum kita menjalankan autentikasi.

Method getAvailableBiometrics() mengembalikan daftar jenis biometrik yang didukung, seperti BiometricType.fingerprint atau BiometricType.face. Informasi ini berguna untuk menampilkan label tombol yang sesuai, misalnya "Login dengan Face ID" di iPhone dan "Login dengan Fingerprint" di Android.

Method authenticate() adalah inti dari layanan ini. Parameter localizedReason berisi alasan yang ditampilkan ke pengguna di dialog sistem. Opsi biometricOnly memaksa sistem hanya memakai biometrik tanpa fallback ke PIN atau pattern. useErrorDialogs membuat plugin menampilkan dialog error bawaan, dan stickyAuth menjaga sesi autentikasi tetap aktif selama aplikasi berada di foreground. Method ini mengembalikan true jika autentikasi berhasil dan false jika gagal atau dibatalkan.

Perlu kita pahami perbedaan antara biometricOnly dan allowBiometricOnly. biometricOnly menonaktifkan seluruh fallback, sehingga pengguna tidak bisa memakai PIN, pattern, atau password perangkat sebagai alternatif. Sementara allowBiometricOnly hanya menonaktifkan fallback ke password perangkat, tetapi tetap mengizinkan PIN dan pattern. Pilihan ini bergantung pada kebijakan keamanan aplikasi. Untuk aplikasi yang menyimpan data sangat sensitif, biometricOnly adalah pilihan yang lebih ketat.

Opsi stickyAuth juga punya perilaku khusus saat aplikasi berpindah ke background. Tanpa stickyAuth, autentikasi akan dibatalkan otomatis ketika aplikasi kehilangan fokus, misalnya saat pengguna menerima panggilan telepon. Dengan stickyAuth, sesi autentikasi tetap berjalan selama aplikasi masih berada di foreground. Kita perlu memilih dengan bijak, karena stickyAuth yang terlalu longgar bisa membuka celah keamanan jika perangkat berpindah tangan di tengah proses.

Sensor fingerprint pada smartphone untuk autentikasi biometrik

Gambar: Sensor fingerprint pada smartphone yang digunakan untuk autentikasi biometrik — Sumber: Unsplash

Mengintegrasikan Autentikasi Biometrik ke UI dan Mengelola State

Sekarang kita bangun halaman login dengan tombol "Login dengan Biometrik" sebagai alternatif password. Tombol ini memanggil BiometricAuthService dan menangani hasilnya sesuai kondisi.

dart
class LoginPage extends StatefulWidget {
  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final BiometricAuthService _authService = BiometricAuthService();
  bool _isLoading = false;

  Future<void> _handleBiometricLogin() async {
    setState(() => _isLoading = true);

    final canAuthenticate = await _authService.canAuthenticate();
    if (!canAuthenticate) {
      _showMessage('Biometrik tidak tersedia. Gunakan password.');
      setState(() => _isLoading = false);
      return;
    }

    final success = await _authService.authenticate();
    setState(() => _isLoading = false);

    if (success) {
      Navigator.pushReplacementNamed(context, '/home');
    } else {
      _showMessage('Autentikasi gagal. Coba lagi atau gunakan password.');
    }
  }

  void _showMessage(String message) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(message)),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: ElevatedButton.icon(
          onPressed: _isLoading ? null : _handleBiometricLogin,
          icon: const Icon(Icons.fingerprint),
          label: const Text('Login dengan Biometrik'),
        ),
      ),
    );
  }
}

Alur yang kita bangun dimulai dari pengecekan ketersediaan biometrik. Jika perangkat tidak mendukung, kita langsung menampilkan pesan dan mengarahkan pengguna ke form password. Pengecekan ini adalah fallback yang wajib ada, karena tidak semua perangkat memiliki sensor biometrik.

Jika biometrik tersedia, kita jalankan authenticate(). Hasil true berarti identitas terverifikasi, lalu kita navigasi ke halaman utama menggunakan Navigator.pushReplacementNamed. Hasil false bisa berarti pengguna membatalkan dialog, gagal mencocokkan sidik jari, atau sistem mengunci autentikasi karena terlalu banyak percobaan.

Kondisi LockedOut dan PermanentlyLockedOut perlu ditangani dengan hati-hati. LockedOut terjadi setelah beberapa percobaan gagal dan biasanya membutuhkan jeda waktu sebelum bisa mencoba lagi. PermanentlyLockedOut mengharuskan pengguna membuka perangkat dengan password atau PIN sistem. Untuk kedua kondisi ini, tampilkan pesan yang jelas dan arahkan pengguna ke fallback password. Jangan biarkan tombol biometrik tetap aktif, karena percobaan berulang hanya akan memperpanjang masa lockout.

Untuk mendeteksi kondisi ini secara presisi, kita bisa memanfaatkan error code yang dilempar oleh plugin. Saat authenticate() melempar PlatformException dengan code LockedOut atau PermanentlyLockedOut, kita bisa menangkapnya di dalam BiometricAuthService dan meneruskannya ke UI. Dengan cara ini, UI bisa menampilkan state yang berbeda: ikon gembok terkunci, pesan "Terlalu banyak percobaan, coba lagi dalam 30 detik", dan tombol biometrik yang dinonaktifkan sementara.

Kita juga bisa menambahkan countdown sebelum mengaktifkan kembali tombol biometrik. Simpan waktu lockout berakhir di dalam state, lalu gunakan Timer.periodic untuk menghitung mundur setiap detik. Ketika countdown mencapai nol, kita aktifkan kembali tombol dan reset state. Pendekatan ini mencegah pengguna menekan tombol berulang kali dan membuat pengalaman error menjadi lebih terkontrol. Kombinasikan dengan fallback password agar pengguna tetap punya jalur masuk yang valid selama masa lockout.

Best Practices Keamanan dan Pengujian

Aturan pertama yang harus diingat: jangan pernah menyimpan data biometrik di aplikasi. Sensor biometrik menyimpan data ini di secure hardware seperti Secure Enclave di iOS atau Trusted Execution Environment di Android. Aplikasi hanya menerima hasil verifikasi berupa boolean, bukan data mentah sidik jari atau wajah. Kondisi ini berarti data biometrik tidak pernah meninggalkan perangkat dan tidak bisa dicuri dari server.

Biometric authentication sebaiknya diposisikan sebagai lapisan kedua setelah password, bukan pengganti tunggal untuk transaksi bernilai tinggi. Untuk transfer uang dalam jumlah besar, kombinasikan biometrik dengan password atau OTP. Pendekatan ini dikenal sebagai two-factor authentication dan memberikan perlindungan yang jauh lebih kuat.

Pengujian bisa dilakukan di emulator. Android Emulator menyediakan menu "Fingerprint" di Extended Controls untuk mensimulasikan sentuhan jari. iOS Simulator memakai perintah xcrun simctl atau menu Features untuk mensimulasikan Face ID. Kedua tool ini memungkinkan kita menguji alur sukses dan gagal tanpa perangkat fisik.

Ada satu keterbatasan yang perlu dipahami. Di Android, local_auth menganggap seluruh biometrik sebagai satu entitas, tanpa membedakan fingerprint dan face. Jika perangkat mendukung keduanya, getAvailableBiometrics() tetap mengembalikan satu jenis. Jangan membuat logika yang bergantung pada perbedaan ini di Android.

Terakhir, tangani error BiometricUnsupported dan NotEnrolled dengan pesan yang informatif. BiometricUnsupported berarti perangkat tidak memiliki sensor, sedangkan NotEnrolled berarti pengguna belum mendaftarkan sidik jari atau wajah di pengaturan sistem. Untuk NotEnrolled, kita bisa menampilkan tombol yang membuka pengaturan biometrik perangkat agar pengguna bisa mendaftar.

Satu praktik penting lainnya adalah mengikat hasil autentikasi biometrik ke session token yang berumur pendek. Jangan menganggap pengguna tetap terautentikasi selamanya setelah satu kali verifikasi. Saat aplikasi kembali dari background, kita perlu meminta ulang biometrik menggunakan AppLifecycleListener atau WidgetsBindingObserver. Dengan mendengarkan perubahan lifecycle, kita bisa mendeteksi saat aplikasi di-resume dan memicu autentikasi ulang sebelum menampilkan data sensitif.

Session token yang dihasilkan setelah biometrik berhasil sebaiknya memiliki masa berlaku singkat, misalnya beberapa menit. Setelah token kedaluwarsa, aplikasi harus meminta verifikasi ulang. Kombinasi antara re-authentication saat resume dan token berumur pendek ini memastikan bahwa data sensitif tidak pernah terbuka begitu saja ketika perangkat berpindah tangan. Pendekatan ini adalah standar yang dipakai aplikasi perbankan dan e-wallet untuk menjaga keamanan sesi.

Pengguna melakukan autentikasi fingerprint pada smartphone

Gambar: Pengguna melakukan autentikasi fingerprint pada smartphone — Sumber: Unsplash

Perdalam skill mobile development bersama Bootcamp Mobile Development Rumah Coding. Dari dasar Flutter sampai fitur keamanan production-grade seperti biometric authentication, semua dibahas dengan pendekatan praktis dan studi kasus nyata.

Artikel Terkait