Perché l'autenticazione biometrica

L'autenticazione biometrica è diventata uno standard di fatto per proteggere l'accesso ad aree sensibili delle applicazioni mobili: dati bancari, note private, gestori di password. Rispetto a un PIN, offre un'esperienza utente più fluida e un buon livello di sicurezza, poiché i dati biometrici non lasciano mai il secure enclave del dispositivo.

In Flutter, il pacchetto ufficiale local_auth permette di sfruttare Face ID, Touch ID e il riconoscimento tramite impronta digitale con poche righe di codice.

Installazione

Aggiungi le dipendenze al file pubspec.yaml:

dependencies:
  local_auth: ^2.3.0
  local_auth_android: ^1.0.46
  local_auth_ios: ^1.1.7

Configurazione nativa

iOS

Aggiungi la chiave NSFaceIDUsageDescription al file ios/Runner/Info.plist, altrimenti l'app verrà rifiutata dall'App Store:

<key>NSFaceIDUsageDescription</key>
<string>Utilizziamo Face ID per proteggere l'accesso ai tuoi dati.</string>

Android

Nel file android/app/src/main/AndroidManifest.xml verifica il permesso:

<uses-permission android:name="android.permission.USE_BIOMETRIC"/>

Inoltre la MainActivity deve estendere FlutterFragmentActivity (e non FlutterActivity), altrimenti la chiamata biometrica va in crash:

import io.flutter.embedding.android.FlutterFragmentActivity

class MainActivity: FlutterFragmentActivity()

Verificare la disponibilità

Prima di autenticare, è buona norma controllare se il dispositivo supporta la biometria e quali metodi sono disponibili:

import 'package:local_auth/local_auth.dart';

final LocalAuthentication auth = LocalAuthentication();

Future<bool> isBiometricAvailable() async {
  final bool canCheck = await auth.canCheckBiometrics;
  final bool isSupported = await auth.isDeviceSupported();
  return canCheck && isSupported;
}

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

Il metodo getAvailableBiometrics() restituisce una lista di tipi come BiometricType.face, BiometricType.fingerprint o BiometricType.strong, utile per mostrare l'icona corretta nell'interfaccia.

Eseguire l'autenticazione

Il cuore del pacchetto è il metodo authenticate:

Future<bool> authenticate() async {
  try {
    return await auth.authenticate(
      localizedReason: 'Conferma la tua identità per continuare',
      options: const AuthenticationOptions(
        biometricOnly: false, // consente il fallback a PIN/pattern
        stickyAuth: true,     // riprende dopo un'interruzione
        useErrorDialogs: true,
      ),
    );
  } on PlatformException catch (e) {
    debugPrint('Errore biometrico: ${e.code} - ${e.message}');
    return false;
  }
}

Alcune opzioni importanti:

  • biometricOnly: se true, esclude il fallback al codice di sblocco del dispositivo.
  • stickyAuth: mantiene attiva l'autenticazione se l'app va in background (ad esempio quando l'utente cambia app per aprire un'altra applicazione).
  • useErrorDialogs: mostra le finestre di dialogo di sistema per situazioni come l'assenza di biometria registrata.

Gestione degli errori

Le PlatformException restituiscono codici specifici che è opportuno gestire. I più comuni sono definiti nella classe auth_error:

import 'package:local_auth/error_codes.dart' as auth_error;

try {
  await auth.authenticate(localizedReason: 'Accedi');
} on PlatformException catch (e) {
  switch (e.code) {
    case auth_error.notAvailable:
      // Nessuna biometria disponibile sul dispositivo
      break;
    case auth_error.notEnrolled:
      // L'utente non ha registrato impronte o volto
      break;
    case auth_error.lockedOut:
    case auth_error.permanentlyLockedOut:
      // Troppi tentativi falliti
      break;
  }
}

Un esempio completo

class AuthGate extends StatefulWidget {
  const AuthGate({super.key});

  @override
  State<AuthGate> createState() => _AuthGateState();
}

class _AuthGateState extends State<AuthGate> {
  final LocalAuthentication _auth = LocalAuthentication();
  bool _autenticato = false;

  Future<void> _sblocca() async {
    final bool disponibile =
        await _auth.canCheckBiometrics && await _auth.isDeviceSupported();
    if (!disponibile) return;

    try {
      final ok = await _auth.authenticate(
        localizedReason: 'Sblocca per accedere ai tuoi dati',
        options: const AuthenticationOptions(stickyAuth: true),
      );
      if (mounted) setState(() => _autenticato = ok);
    } on PlatformException catch (e) {
      debugPrint('Errore: ${e.code}');
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_autenticato) {
      return const HomeScreen();
    }
    return Scaffold(
      body: Center(
        child: ElevatedButton.icon(
          onPressed: _sblocca,
          icon: const Icon(Icons.fingerprint),
          label: const Text('Sblocca app'),
        ),
      ),
    );
  }
}

Best practice

  • Non affidare la sicurezza solo alla biometria: usala come gate di accesso, ma conserva i dati sensibili con soluzioni come flutter_secure_storage.
  • Offri sempre un'alternativa: mantieni un metodo di fallback (PIN dell'app o codice del dispositivo) per gli utenti senza biometria registrata.
  • Ri-autentica dopo il background: per app critiche, richiedi nuovamente l'autenticazione quando l'app torna in foreground usando il WidgetsBindingObserver.
  • Rispetta la privacy: comunica chiaramente all'utente perché richiedi l'accesso biometrico tramite il parametro localizedReason.

Conclusioni

Con local_auth integrare Face ID, Touch ID e impronte digitali in Flutter richiede poche righe di codice, ma la parte più delicata riguarda la configurazione nativa e la gestione robusta degli errori. Combinando l'autenticazione biometrica con uno storage sicuro otterrai un livello di protezione adeguato per le informazioni più sensibili dei tuoi utenti.