Firebase Authentication — Email & Password
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.
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}');
}
}
}
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
},
),
);
}
}
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.
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 typedFirebaseAuthExceptioncodes. - Sign in existing users with
signInWithEmailAndPassword()and sign them out withsignOut(). - Drive your app's navigation reactively using the
authStateChanges()stream inside aStreamBuilder. - Apply security best practices such as trimming email input and using generic error messages in production.