Skip to content
Blog

Firebase Auth in Flutter: Auth-State Streams, Password Sign-in, and Federated Providers

Set up firebase_auth the way the docs intend: pick the right state stream, handle real error codes, and wire Google sign-in on native and web.

Published on • October 20, 2026

AI Assistant

Firebase Authentication is usually the first backend service a Flutter app grows, and the Flutter get-started guide is short on purpose — the API surface is small, the mistakes are in the details. Here’s the integration done properly: state streams, password flows, federated sign-in, persistence, and the emulator for local testing.

Install and enable a provider

From your project root:

flutter pub add firebase_auth
flutter run   # rebuild so the plugin registers
import 'package:firebase_auth/firebase_auth.dart';

Before any code works, enable a provider in the Firebase console under Security > Authentication > Sign-in method — Email/Password, Google, and so on. Skip this step and you’ll get confusing operation-not-allowed failures that look like client bugs.

Subscribe to auth state — pick the right stream

Firebase Auth exposes three streams, and they are not interchangeable. Each fires immediately on listener registration, then on sign-in and sign-out:

FirebaseAuth.instance
    .authStateChanges()
    .listen((User? user) => print(user == null ? 'signed out' : 'signed in'));
  • authStateChanges() — only sign-in and sign-out transitions. Use this for routing: show the login route or the home route.
  • idTokenChanges() — adds token events. Fires when the ID token changes or is refreshed, and (with custom claims set via the Admin SDK) when a user re-authenticates after claims change, when a session’s token refreshes after expiry, or when you call currentUser.getIdTokenResult(true). Use this when your API authenticates with ID tokens.
  • userChanges() — adds profile mutations on top of token events: reload(), unlink(), updateEmail(), updatePassword(), updatePhoneNumber(), and updateProfile() all re-emit. Use this when the UI renders user profile fields (display name, photo URL).

One caveat straight from the docs: profile updates, disables, or deletions made server-side (Admin SDK or console) do not fire these streams — call FirebaseAuth.instance.currentUser.reload() to surface them, expecting user-disabled or user-not-found exceptions.

Password sign-up, sign-in, sign-out

The password guide shows the shape you should copy, error codes included:

try {
  final credential = await FirebaseAuth.instance.createUserWithEmailAndPassword(
    email: emailAddress,
    password: password,
  );
} on FirebaseAuthException catch (e) {
  if (e.code == 'weak-password') {
    print('The password provided is too weak.');
  } else if (e.code == 'email-already-in-use') {
    print('The account already exists for that email.');
  }
}

try {
  await FirebaseAuth.instance.signInWithEmailAndPassword(
    email: emailAddress,
    password: password,
  );
} on FirebaseAuthException catch (e) {
  if (e.code == 'user-not-found') {
    print('No user found for that email.');
  } else if (e.code == 'wrong-password') {
    print('Wrong password provided for that user.');
  }
}

await FirebaseAuth.instance.signOut();

Creating an account signs the user in immediately and pushes a new event to your listeners. Be aware that Firebase rate-limits new email/password and anonymous sign-ups per IP in a short window; the console’s Sign-in method tab lets you request temporary quota changes.

Federated sign-in: Google on native and web

The federated identity guide splits Google sign-in by platform. On Android and iOS you need the official google_sign_in plugin to trigger the flow (and your Android app’s SHA-1 key configured):

import 'package:google_sign_in/google_sign_in.dart';

Future<UserCredential> signInWithGoogle() async {
  final GoogleSignInAccount googleUser = await GoogleSignIn.instance.authenticate();
  final GoogleSignInAuthentication googleAuth = googleUser.authentication;
  final credential = GoogleAuthProvider.credential(idToken: googleAuth.idToken);
  return FirebaseAuth.instance.signInWithCredential(credential);
}

On web, Firebase handles the flow itself — build a GoogleAuthProvider, add scopes and custom parameters, then hand it to signInWithPopup (or signInWithRedirect to stay in the same window):

Future<UserCredential> signInWithGoogleWeb() async {
  final googleProvider = GoogleAuthProvider();
  googleProvider.addScope('https://www.googleapis.com/auth/contacts.readonly');
  googleProvider.setCustomParameters({'login_hint': 'user@example.com'});
  return FirebaseAuth.instance.signInWithPopup(googleProvider);
}

Everything else — Facebook via flutter_facebook_auth, Apple, Microsoft, Twitter, GitHub, Play Games — follows the same pattern: obtain platform credentials, build the matching *AuthProvider.credential(...), call signInWithCredential() (native) or signInWithPopup() / signInWithProvider() (web).

Persistence: know the defaults

Out of the box, auth state persists across restarts on Android and iOS and is not configurable there — it lives in platform storage (on iOS/macOS the system keychain, which means state can survive app reinstalls; that’s documented as flutterfire issue #4661). On web, state lives in IndexedDB and you can change it:

final auth = FirebaseAuth.instanceFor(app: Firebase.app(), persistence: Persistence.LOCAL);
await auth.setPersistence(Persistence.NONE); // or per-instance override after init

Putting it behind a thin layer

Nothing forces you to call FirebaseAuth.instance from widgets — but with three streams and per-platform sign-in, a small repository keeps routing decisions in one place:

class AuthRepository {
  Stream<User?> get authState => FirebaseAuth.instance.authStateChanges();

  Future<void> signIn(String email, String password) =>
      FirebaseAuth.instance.signInWithEmailAndPassword(email: email, password: password);

  Future<void> signInWithGoogle() async {
    final googleUser = await GoogleSignIn.instance.authenticate();
    final googleAuth = googleUser.authentication;
    final credential = GoogleAuthProvider.credential(idToken: googleAuth.idToken);
    await FirebaseAuth.instance.signInWithCredential(credential);
  }

  Future<void> signOut() => FirebaseAuth.instance.signOut();
}

Wire it to a single StreamBuilder<User?> at the root of your widget tree and gate routes on user == null. From there, the docs’ next steps are: verify emails after sign-up, read profile data from the User object (see the manage users guide), and pull the signed-in user’s UID from the auth variable in Firestore/Storage Security Rules to scope data access. If you’re upgrading an existing auth system, linking credentials lets one account carry multiple providers.

Test locally with the Auth emulator

Before touching live services, prototype against the Local Emulator Suite:

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  await FirebaseAuth.instance.useAuthEmulator('localhost', 9099);
  runApp(const MyApp());
}

Then run firebase emulators:start from your project directory and use the emulator UI (or its REST API) for interactive and non-interactive testing — ideal for trying sign-in UIs and Security Rules combinations without creating throwaway users.

Sources