Authentication & Security

Firebase Auth Setup & Email/Password Flows

16 min Lesson 1 of 12

Firebase Auth Setup & Email/Password Flows

Firebase Authentication is the industry-standard backend service for handling user identity in Flutter apps. It provides a secure, scalable authentication system without requiring you to build and maintain your own auth server. In this lesson you will configure the Firebase Auth SDK from scratch and implement the four core flows: register, login, logout, and email verification.

1. Project Setup: Adding Firebase to Flutter

Before writing any Dart code, you must connect your Flutter project to a Firebase project. The recommended approach is the FlutterFire CLI:

  • Create a Firebase project at console.firebase.google.com and enable the Email/Password sign-in provider under Authentication > Sign-in method.
  • Install the FlutterFire CLI: dart pub global activate flutterfire_cli
  • Run flutterfire configure in your project root. This downloads platform-specific config files (google-services.json for Android, GoogleService-Info.plist for iOS) and generates lib/firebase_options.dart.
  • Add the required packages to pubspec.yaml: firebase_core and firebase_auth.

pubspec.yaml — Required Dependencies

dependencies:
  flutter:
    sdk: flutter
  firebase_core: ^3.6.0
  firebase_auth: ^5.3.1

2. Initialising Firebase in main.dart

Firebase must be initialised before runApp() is called. Because initialisation is asynchronous, main() must be async and you must await Firebase.initializeApp(). Passing DefaultFirebaseOptions.currentPlatform (generated by the CLI) selects the correct platform configuration automatically.

main.dart — Firebase Initialisation

import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
import 'package:flutter/material.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}
Note: WidgetsFlutterBinding.ensureInitialized() must be called before any native plugin work (including Firebase). Omitting it causes a runtime exception before the app even renders.

3. Register a New User

Use FirebaseAuth.instance.createUserWithEmailAndPassword() to create a new account. The method returns a UserCredential whose .user property is the newly created User object. Always wrap Firebase calls in a try/catch block and handle FirebaseAuthException specifically — it exposes a .code string (e.g. 'email-already-in-use', 'weak-password') that lets you surface meaningful error messages.

auth_service.dart — Register Flow

import 'package:firebase_auth/firebase_auth.dart';

class AuthService {
  final FirebaseAuth _auth = FirebaseAuth.instance;

  /// Creates a new account and sends an email-verification link.
  Future<UserCredential> register({
    required String email,
    required String password,
  }) async {
    try {
      final credential = await _auth.createUserWithEmailAndPassword(
        email: email,
        password: password,
      );
      // Immediately request email verification after registration.
      await credential.user?.sendEmailVerification();
      return credential;
    } on FirebaseAuthException catch (e) {
      throw _mapAuthException(e);
    }
  }

  /// Sign in an existing user.
  Future<UserCredential> login({
    required String email,
    required String password,
  }) async {
    try {
      return await _auth.signInWithEmailAndPassword(
        email: email,
        password: password,
      );
    } on FirebaseAuthException catch (e) {
      throw _mapAuthException(e);
    }
  }

  /// Sign out the current user.
  Future<void> logout() => _auth.signOut();

  /// Reload the user and check email-verification status.
  Future<bool> isEmailVerified() async {
    await _auth.currentUser?.reload();
    return _auth.currentUser?.emailVerified ?? false;
  }

  String _mapAuthException(FirebaseAuthException e) {
    switch (e.code) {
      case 'email-already-in-use':
        return 'An account with that email already exists.';
      case 'weak-password':
        return 'Password must be at least 6 characters.';
      case 'user-not-found':
      case 'wrong-password':
        return 'Invalid email or password.';
      case 'too-many-requests':
        return 'Too many attempts. Please try again later.';
      default:
        return e.message ?? 'Authentication failed.';
    }
  }
}

4. Listening to Auth State Changes

FirebaseAuth.instance.authStateChanges() returns a Stream<User?> that emits a User object when signed in and null when signed out. Wrap your root widget with a StreamBuilder on this stream to reactively route users between authenticated and unauthenticated screens — no manual navigation logic required.

Reactive Auth Routing with StreamBuilder

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: StreamBuilder<User?>(
        stream: FirebaseAuth.instance.authStateChanges(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }
          if (snapshot.hasData) {
            final user = snapshot.data!;
            // Route to email-verification screen if not yet verified.
            return user.emailVerified
                ? const HomeScreen()
                : const VerifyEmailScreen();
          }
          return const LoginScreen();
        },
      ),
    );
  }
}
Tip: authStateChanges() only fires on sign-in and sign-out events. To detect email-verification completion without a backend trigger, poll user.reload() on a timer or when the user returns to the foreground, then check user.emailVerified.

5. Email Verification Flow

After registration, Firebase sends a verification email automatically when you call user.sendEmailVerification(). Show a dedicated VerifyEmailScreen where the user is instructed to check their inbox. Provide a Resend button and a I have verified button that triggers a user.reload() + check:

VerifyEmailScreen — Polling for Verification

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

  @override
  State<VerifyEmailScreen> createState() => _VerifyEmailScreenState();
}

class _VerifyEmailScreenState extends State<VerifyEmailScreen> {
  bool _isSending = false;

  Future<void> _checkVerification() async {
    await FirebaseAuth.instance.currentUser?.reload();
    final verified =
        FirebaseAuth.instance.currentUser?.emailVerified ?? false;
    if (verified && mounted) {
      // authStateChanges() will NOT fire here; navigate manually.
      Navigator.of(context).pushReplacement(
        MaterialPageRoute(builder: (_) => const HomeScreen()),
      );
    } else if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Email not yet verified.')),
      );
    }
  }

  Future<void> _resendEmail() async {
    setState(() => _isSending = true);
    await FirebaseAuth.instance.currentUser?.sendEmailVerification();
    setState(() => _isSending = false);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Verify Your Email')),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const Text('A verification email has been sent. Check your inbox.'),
          ElevatedButton(
            onPressed: _checkVerification,
            child: const Text('I have verified'),
          ),
          TextButton(
            onPressed: _isSending ? null : _resendEmail,
            child: Text(_isSending ? 'Sending...' : 'Resend email'),
          ),
        ],
      ),
    );
  }
}
Warning: authStateChanges() does not emit a new event when emailVerified flips from false to true — that field is part of the user profile, not the auth state. You must call user.reload() explicitly and then check the field, then navigate programmatically.

Summary

In this lesson you:

  • Configured firebase_core and firebase_auth using the FlutterFire CLI.
  • Initialised Firebase in main() before runApp().
  • Implemented register (createUserWithEmailAndPassword), login (signInWithEmailAndPassword), and logout (signOut) with proper FirebaseAuthException handling.
  • Used authStateChanges() with a StreamBuilder for reactive routing.
  • Sent and polled email verification, navigating manually when the flag flips.
Key Takeaway: The AuthService pattern keeps all Firebase calls outside your widgets. Widgets call service methods and react to the authStateChanges() stream — this separation makes your auth logic testable and your UI clean.