Firebase Integration

Firebase Authentication — Email & Password

15 min Lesson 3 of 13

Firebase Authentication — Email & Password

Firebase Authentication provides a complete backend solution for user identity. In this lesson you will implement email-and-password registration and sign-in using the firebase_auth Flutter package, handle the structured error codes exposed by FirebaseAuthException, and expose the currently-signed-in user to the rest of the app through the persistent authStateChanges stream.

Why Use Firebase Auth?

Building authentication from scratch requires secure password hashing, token management, session refresh, and account-recovery flows. Firebase Authentication handles all of that server-side so you can focus on the user experience. Key benefits include:

  • Persistent sessions — the SDK stores the ID token locally and refreshes it automatically.
  • A real-time Stream<User?> that emits whenever the auth state changes (sign-in, sign-out, token refresh).
  • Typed error codes via FirebaseAuthException.code — no need to parse raw HTTP responses.
  • Drop-in support for additional providers (Google, Apple, phone) later without changing your app architecture.
Prerequisite: Complete the Firebase project setup (Lesson 1) and add firebase_auth: ^4.x.x to pubspec.yaml. Run flutter pub get before proceeding.

Registering a New User

Call FirebaseAuth.instance.createUserWithEmailAndPassword() to create a new account. The method is asynchronous and returns a UserCredential that contains the new User object. Always wrap the call in a try/catch block to handle FirebaseAuthException.

Registration Example

import 'package:firebase_auth/firebase_auth.dart';

Future<void> registerUser(String email, String password) async {
  try {
    final UserCredential credential =
        await FirebaseAuth.instance.createUserWithEmailAndPassword(
      email: email.trim(),
      password: password,
    );
    final User? user = credential.user;
    print('Registered: ${user?.email}');
  } on FirebaseAuthException catch (e) {
    switch (e.code) {
      case 'email-already-in-use':
        throw Exception('That email address is already registered.');
      case 'invalid-email':
        throw Exception('The email address is badly formatted.');
      case 'weak-password':
        throw Exception('Password must be at least 6 characters.');
      default:
        throw Exception('Registration failed: ${e.message}');
    }
  }
}
Tip: Always call email.trim() before passing the address to Firebase. A trailing space causes an invalid-email error that is confusing for users who cannot see the whitespace.

Signing In an Existing User

Use signInWithEmailAndPassword() for returning users. The error-handling pattern is identical to registration, but the relevant error codes differ.

Sign-In Example

Future<void> signInUser(String email, String password) async {
  try {
    final UserCredential credential =
        await FirebaseAuth.instance.signInWithEmailAndPassword(
      email: email.trim(),
      password: password,
    );
    print('Signed in as: ${credential.user?.email}');
  } on FirebaseAuthException catch (e) {
    switch (e.code) {
      case 'user-not-found':
        throw Exception('No account found for that email.');
      case 'wrong-password':
        throw Exception('Incorrect password. Please try again.');
      case 'user-disabled':
        throw Exception('This account has been disabled.');
      case 'too-many-requests':
        throw Exception('Too many attempts. Try again later.');
      default:
        throw Exception('Sign-in failed: ${e.message}');
    }
  }
}

Future<void> signOutUser() async {
  await FirebaseAuth.instance.signOut();
}

Listening to Auth-State Changes

FirebaseAuth.instance.authStateChanges() returns a Stream<User?> that emits a User object when someone signs in, and null when they sign out. This stream is the recommended way to drive your navigation tree — it keeps the entire app in sync without manual flag management.

Wrap your MaterialApp (or its router) with a StreamBuilder to react to auth changes at the root level:

Root-Level Auth Stream

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: StreamBuilder<User?>(
        stream: FirebaseAuth.instance.authStateChanges(),
        builder: (BuildContext context, AsyncSnapshot<User?> snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }
          if (snapshot.hasData && snapshot.data != null) {
            return const HomeScreen();   // User is signed in
          }
          return const LoginScreen();    // User is signed out
        },
      ),
    );
  }
}
Warning: Never read FirebaseAuth.instance.currentUser synchronously at app start to decide which screen to show. There is a brief moment on cold launch where the SDK has not yet restored the persisted session, so currentUser returns null even for a previously signed-in user. Always use the stream.

Common FirebaseAuthException Error Codes

Below is a reference of the most important codes you will encounter:

  • email-already-in-use — Registration: the email is taken by another account.
  • invalid-email — Registration or sign-in: malformed email address.
  • weak-password — Registration: password is shorter than 6 characters.
  • user-not-found — Sign-in: no account exists for this email.
  • wrong-password — Sign-in: correct email but wrong password.
  • user-disabled — Sign-in: the account has been suspended in the Firebase console.
  • too-many-requests — Sign-in: the account is temporarily locked after repeated failures.
  • operation-not-allowed — Email/password sign-in is not enabled in the Firebase console.
Security note: In a production app, avoid telling users whether the email exists or the password was wrong — combine both into a generic "Invalid email or password" message to prevent user-enumeration attacks.

Summary

In this lesson you learned how to implement the full email-and-password authentication cycle in Flutter using Firebase Authentication:

  • Register new users with createUserWithEmailAndPassword() and handle typed FirebaseAuthException codes.
  • Sign in existing users with signInWithEmailAndPassword() and sign them out with signOut().
  • Drive your app's navigation reactively using the authStateChanges() stream inside a StreamBuilder.
  • Apply security best practices such as trimming email input and using generic error messages in production.