[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-autenticazione-in-flutter-con-firebase-auth-email-google-e-apple-sign-in":3,"comments-article-autenticazione-in-flutter-con-firebase-auth-email-google-e-apple-sign-in":26},{"id":4,"title":5,"slug":6,"excerpt":7,"body":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"status":16,"published_at":17,"meta_title":18,"meta_description":19,"category":20,"author":24},91,"Autenticazione in Flutter con Firebase Auth: email, Google e Apple Sign-In","autenticazione-in-flutter-con-firebase-auth-email-google-e-apple-sign-in","Guida pratica all'autenticazione in Flutter con Firebase Auth: login con email e password, Google Sign-In con la nuova API 7.x, Sign in with Apple, gestione degli errori, refresh dei token e protezione delle rotte con go_router.","L'autenticazione è uno dei primi requisiti di quasi ogni app reale, ed è anche uno dei punti in cui è più facile sbagliare: flussi diversi per piattaforma, token da rinnovare, errori da tradurre in messaggi comprensibili, rotte da proteggere. In questa guida costruiamo un layer di autenticazione completo con **Firebase Auth**, integrando **email\u002Fpassword**, **Google Sign-In** (con la nuova API 7.x, che ha introdotto breaking change importanti) e **Sign in with Apple**, obbligatorio su iOS se offri altri login social.\n\n## Setup del progetto\n\nDopo aver creato il progetto su Firebase e averlo collegato con `flutterfire configure`, le dipendenze minime sono:\n\n```yaml\ndependencies:\n  firebase_core: ^3.8.0\n  firebase_auth: ^5.3.0\n  google_sign_in: ^7.1.0\n  go_router: ^14.0.0\n```\n\nL'inizializzazione avviene nel `main`:\n\n```dart\nFuture\u003Cvoid> main() async {\n  WidgetsFlutterBinding.ensureInitialized();\n  await Firebase.initializeApp(\n    options: DefaultFirebaseOptions.currentPlatform,\n  );\n  runApp(const MyApp());\n}\n```\n\nRicorda di abilitare i provider desiderati nella console Firebase (Authentication → Sign-in method): senza quel passaggio il client riceverà `operation-not-allowed`.\n\n## Un repository per isolare Firebase\n\nEsporre `FirebaseAuth` direttamente nella UI rende il codice difficile da testare e da migrare. Meglio incapsulare tutto in un repository che espone modelli e errori di dominio.\n\n```dart\nclass AuthRepository {\n  AuthRepository({FirebaseAuth? auth})\n      : _auth = auth ?? FirebaseAuth.instance;\n\n  final FirebaseAuth _auth;\n\n  \u002F\u002F\u002F Emette l'utente corrente a ogni login\u002Flogout.\n  Stream\u003CUser?> get authStateChanges => _auth.authStateChanges();\n\n  \u002F\u002F\u002F Emette anche al refresh del token e ai cambi di profilo.\n  Stream\u003CUser?> get userChanges => _auth.userChanges();\n\n  User? get currentUser => _auth.currentUser;\n\n  Future\u003Cvoid> signOut() => _auth.signOut();\n}\n```\n\nÈ utile conoscere la differenza fra i tre stream disponibili:\n\n- `authStateChanges()`: login, logout e cambio utente. È quello da usare per il routing.\n- `idTokenChanges()`: emette anche quando l'ID token viene rinnovato (ogni ora circa).\n- `userChanges()`: aggiunge gli aggiornamenti al profilo (`displayName`, `photoURL`, verifica email).\n\n## Login con email e password\n\nIl flusso classico, con registrazione, verifica dell'indirizzo e reset della password:\n\n```dart\nextension EmailAuth on AuthRepository {\n  Future\u003CUserCredential> signInWithEmail({\n    required String email,\n    required String password,\n  }) {\n    return _auth.signInWithEmailAndPassword(\n      email: email.trim(),\n      password: password,\n    );\n  }\n\n  Future\u003CUserCredential> registerWithEmail({\n    required String email,\n    required String password,\n  }) async {\n    final credential = await _auth.createUserWithEmailAndPassword(\n      email: email.trim(),\n      password: password,\n    );\n    await credential.user?.sendEmailVerification();\n    return credential;\n  }\n\n  Future\u003Cvoid> sendPasswordReset(String email) =>\n      _auth.sendPasswordResetEmail(email: email.trim());\n}\n```\n\n> Nota: `_auth` è privato, quindi in un progetto reale i metodi vanno dentro la classe. L'extension qui è solo per leggibilità dell'articolo.\n\n### Tradurre gli errori\n\n`FirebaseAuthException` espone un `code` stabile: mapparlo in un enum di dominio evita di mostrare messaggi in inglese all'utente e rende testabile la logica.\n\n```dart\nenum AuthFailure {\n  invalidCredentials,\n  emailAlreadyInUse,\n  weakPassword,\n  tooManyRequests,\n  network,\n  requiresRecentLogin,\n  cancelled,\n  unknown;\n\n  static AuthFailure fromCode(String code) => switch (code) {\n        'invalid-credential' ||\n        'wrong-password' ||\n        'user-not-found' =>\n          AuthFailure.invalidCredentials,\n        'email-already-in-use' => AuthFailure.emailAlreadyInUse,\n        'weak-password' => AuthFailure.weakPassword,\n        'too-many-requests' => AuthFailure.tooManyRequests,\n        'network-request-failed' => AuthFailure.network,\n        'requires-recent-login' => AuthFailure.requiresRecentLogin,\n        _ => AuthFailure.unknown,\n      };\n}\n\nString messageFor(AuthFailure failure) => switch (failure) {\n      AuthFailure.invalidCredentials => 'Email o password non corretti.',\n      AuthFailure.emailAlreadyInUse => 'Esiste già un account con questa email.',\n      AuthFailure.weakPassword => 'La password è troppo debole.',\n      AuthFailure.tooManyRequests => 'Troppi tentativi, riprova più tardi.',\n      AuthFailure.network => 'Connessione assente.',\n      AuthFailure.requiresRecentLogin => 'Rieffettua il login per continuare.',\n      AuthFailure.cancelled => 'Operazione annullata.',\n      AuthFailure.unknown => 'Si è verificato un errore imprevisto.',\n    };\n```\n\nPer motivi di sicurezza Firebase, con *email enumeration protection* attiva, restituisce `invalid-credential` sia per password sbagliata sia per utente inesistente: non provare a distinguere i due casi nella UI.\n\n## Google Sign-In: cosa cambia con la versione 7\n\nLa 7.0 di `google_sign_in` ha riscritto l'API. I punti chiave:\n\n- si usa il singleton `GoogleSignIn.instance` e **serve chiamare `initialize()`** una volta sola;\n- `signIn()` non esiste più: c'è `authenticate()` per il login esplicito e `attemptLightweightAuthentication()` per il ripristino silenzioso della sessione;\n- **autenticazione e autorizzazione sono separate**: `authentication` restituisce solo l'`idToken`; per ottenere un access token e chiamare le API Google si usa `authorizationClient`;\n- su Web `authenticate()` non è supportato: bisogna usare il bottone renderizzato da Google.\n\n```dart\nimport 'package:google_sign_in\u002Fgoogle_sign_in.dart';\n\nclass GoogleAuthService {\n  Future\u003Cvoid> initialize() async {\n    await GoogleSignIn.instance.initialize(\n      \u002F\u002F clientId iOS\u002FmacOS, serverClientId per validare il token lato backend\n      clientId: Platform.isIOS ? iosClientId : null,\n      serverClientId: webClientId,\n    );\n  }\n\n  Future\u003CUserCredential?> signIn() async {\n    try {\n      final account = await GoogleSignIn.instance.authenticate(\n        scopeHint: const ['email', 'profile'],\n      );\n      final idToken = account.authentication.idToken;\n      if (idToken == null) throw StateError('ID token mancante');\n\n      final credential = GoogleAuthProvider.credential(idToken: idToken);\n      return FirebaseAuth.instance.signInWithCredential(credential);\n    } on GoogleSignInException catch (e) {\n      if (e.code == GoogleSignInExceptionCode.canceled) return null;\n      rethrow;\n    }\n  }\n\n  Future\u003Cvoid> signOut() async {\n    await GoogleSignIn.instance.signOut();\n    await FirebaseAuth.instance.signOut();\n  }\n}\n```\n\nSe l'utente deve revocare completamente il consenso (ad esempio in fase di cancellazione dell'account) usa `GoogleSignIn.instance.disconnect()` al posto di `signOut()`.\n\nSu Android ricordati di registrare l'impronta **SHA-1** (e SHA-256 per Play App Signing) nella console Firebase e di rigenerare `google-services.json`: il 90% dei `sign_in_failed` dipende da questo.\n\n## Sign in with Apple\n\nLe linee guida App Store impongono Sign in with Apple se l'app offre login di terze parti. Con `firebase_auth` puoi evitare pacchetti aggiuntivi usando il provider nativo:\n\n```dart\nFuture\u003CUserCredential> signInWithApple() async {\n  final provider = AppleAuthProvider()\n    ..addScope('email')\n    ..addScope('name');\n\n  if (kIsWeb) {\n    return FirebaseAuth.instance.signInWithPopup(provider);\n  }\n  \u002F\u002F Su iOS\u002FmacOS usa il flusso nativo; su Android apre una web view.\n  return FirebaseAuth.instance.signInWithProvider(provider);\n}\n```\n\nRequisiti lato piattaforma:\n\n- attivare la capability **Sign in with Apple** in Xcode (target Runner);\n- configurare il provider Apple nella console Firebase con Services ID, Team ID e chiave privata (necessario per Android e Web);\n- gestire il fatto che **nome ed email arrivano solo al primo accesso**: se non li salvi subito su Firestore o nel profilo utente, li perdi.\n\n```dart\nfinal credential = await signInWithApple();\nfinal fullName = credential.additionalUserInfo?.profile?['name'];\nif (credential.additionalUserInfo?.isNewUser ?? false) {\n  await credential.user?.updateDisplayName(fullName?.toString());\n}\n```\n\nMolti utenti Apple usano l'indirizzo privato `@privaterelay.appleid.com`: se invii email transazionali, configura il relay nel pannello sviluppatore Apple.\n\n## Collegare più provider allo stesso account\n\nSe un utente si registra con email e poi tenta il login con Google usando lo stesso indirizzo, puoi ottenere `account-exists-with-different-credential`. La soluzione è il **linking**:\n\n```dart\nFuture\u003Cvoid> linkGoogle(AuthCredential credential) async {\n  final user = FirebaseAuth.instance.currentUser;\n  if (user == null) return;\n  try {\n    await user.linkWithCredential(credential);\n  } on FirebaseAuthException catch (e) {\n    if (e.code == 'credential-already-in-use') {\n      \u002F\u002F La credenziale appartiene già a un altro account: chiedi conferma\n      \u002F\u002F e fai il merge dei dati applicativi prima di procedere.\n      await FirebaseAuth.instance.signInWithCredential(credential);\n    }\n  }\n}\n```\n\nOperazioni sensibili come `delete()`, `updatePassword()` o `updateEmail()` richiedono un login recente: intercetta `requires-recent-login` e chiama `user.reauthenticateWithCredential(...)`.\n\n## Proteggere le rotte con go_router\n\nIl modo più pulito per gestire l'accesso alle schermate è collegare lo stream di autenticazione al `redirect` del router.\n\n```dart\nclass GoRouterRefreshStream extends ChangeNotifier {\n  GoRouterRefreshStream(Stream\u003Cdynamic> stream) {\n    notifyListeners();\n    _sub = stream.asBroadcastStream().listen((_) => notifyListeners());\n  }\n\n  late final StreamSubscription\u003Cdynamic> _sub;\n\n  @override\n  void dispose() {\n    _sub.cancel();\n    super.dispose();\n  }\n}\n\nfinal router = GoRouter(\n  initialLocation: '\u002F',\n  refreshListenable:\n      GoRouterRefreshStream(FirebaseAuth.instance.authStateChanges()),\n  redirect: (context, state) {\n    final loggedIn = FirebaseAuth.instance.currentUser != null;\n    final goingToLogin = state.matchedLocation == '\u002Flogin';\n\n    if (!loggedIn && !goingToLogin) return '\u002Flogin';\n    if (loggedIn && goingToLogin) return '\u002F';\n    return null;\n  },\n  routes: [\n    GoRoute(path: '\u002F', builder: (_, __) => const HomePage()),\n    GoRoute(path: '\u002Flogin', builder: (_, __) => const LoginPage()),\n  ],\n);\n```\n\nAll'avvio Firebase impiega qualche millisecondo per ripristinare la sessione da disco: mostra una splash finché il primo evento dello stream non è arrivato, altrimenti l'utente vedrà un flash della schermata di login.\n\n## Inviare il token al tuo backend\n\nSe hai un backend proprietario, non inviare mai lo `uid` come prova di identità: manda l'**ID token** e verificalo lato server con l'Admin SDK.\n\n```dart\nclass AuthInterceptor extends Interceptor {\n  @override\n  Future\u003Cvoid> onRequest(\n    RequestOptions options,\n    RequestInterceptorHandler handler,\n  ) async {\n    final token = await FirebaseAuth.instance.currentUser?.getIdToken();\n    if (token != null) {\n      options.headers['Authorization'] = 'Bearer $token';\n    }\n    handler.next(options);\n  }\n}\n```\n\n`getIdToken()` restituisce il token dalla cache e lo rinnova automaticamente se scaduto; usa `getIdToken(true)` solo quando hai appena modificato i **custom claims** e ti serve subito il valore aggiornato.\n\n## Testare il layer di autenticazione\n\nIl vantaggio di avere un `AuthRepository` è poterlo sostituire con un fake nei test di widget:\n\n```dart\nclass FakeAuthRepository implements AuthRepository {\n  final _controller = StreamController\u003CUser?>.broadcast();\n\n  @override\n  Stream\u003CUser?> get authStateChanges => _controller.stream;\n\n  void emitLoggedOut() => _controller.add(null);\n  \u002F\u002F ...\n}\n```\n\nPer i test end-to-end conviene usare l'**Auth Emulator**, che evita di consumare quota e di creare utenti reali:\n\n```dart\nif (kDebugMode) {\n  await FirebaseAuth.instance.useAuthEmulator('localhost', 9099);\n}\n```\n\n## Checklist finale\n\n- Abilita **App Check** per impedire l'uso delle API Firebase da client non autorizzati.\n- Attiva la protezione contro l'enumerazione delle email e i limiti di rate nella console.\n- Non salvare mai la password localmente: la sessione la gestisce già Firebase in modo sicuro.\n- Gestisci esplicitamente l'annullamento del login social (l'utente che chiude il foglio nativo non è un errore da segnalare con uno snackbar rosso).\n- Prevedi la cancellazione dell'account: è un requisito sia di Apple sia di Google Play.\n- Aggiorna il testo dei consensi e la privacy policy indicando quali dati raccogli dai provider.\n\nCon questa struttura hai un sistema di autenticazione multi-provider testabile, disaccoppiato dalla UI e pronto a scalare: aggiungere in futuro un login con numero di telefono o un magic link significherà solo aggiungere un metodo al repository, senza toccare il routing né le schermate esistenti.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F95e58950-e50a-4943-8b76-9d0747a3bd50.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1532295454114-d7bc89024613?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODg4NDAxMDN8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Sara Kurfeß","https:\u002F\u002Funsplash.com\u002F@stereophototyp","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fgold-iphone-6-YddMIRck34I",null,"published","2026-09-08T04:01:43+00:00","Autenticazione Flutter con Firebase Auth, Google e Apple","Guida pratica all'autenticazione in Flutter: Firebase Auth con email, Google Sign-In 7.x, Sign in with Apple, gestione errori e rotte protette con go_router.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1789120579909]