Flutter Firebase Authentication: Email, Google & Phone
Published: 2026-10-09 · 6 min read
The Flutter Firebase authentication guide: email/password, Google Sign-In, phone OTP, auth state routing, account linking, and the security checklist to run.
Why Flutter Firebase Authentication Is Still the Default
For most Flutter apps, Flutter Firebase authentication is the fastest path from zero to a secure, production-ready login system. Email/password, Google Sign-In, phone OTP, Apple Sign-In, anonymous upgrade — all handled by one SDK, with a free tier generous enough for most launches.
This is the complete 2026 setup guide: project wiring, email/password, Google sign-in, phone auth, auth state management, and the security rules and pitfalls that separate a demo from a shippable product.
Project Setup
Add the dependencies:
dependencies:
firebase_core: ^3.0.0
firebase_auth: ^5.0.0
google_sign_in: ^6.0.0
Initialize Firebase in main():
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'firebase_options.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(const MyApp());
}
Generate firebase_options.dart with the FlutterFire CLI (dart pub global activate flutterfire_cli, then flutterfire configure). Don't hand-write platform configs — the CLI keeps Android SHA fingerprints, iOS bundle IDs, and API keys consistent.
Email & Password Authentication
The bread-and-butter flow. Always trim inputs, always handle the specific FirebaseAuthException codes — generic "something went wrong" messages are how you ship a login screen nobody can debug:
import 'package:firebase_auth/firebase_auth.dart';
final _auth = FirebaseAuth.instance;
Future<UserCredential> signUpWithEmail({
required String email,
required String password,
}) async {
try {
final credential = await _auth.createUserWithEmailAndPassword(
email: email.trim(),
password: password,
);
await credential.user?.sendEmailVerification();
return credential;
} on FirebaseAuthException catch (e) {
switch (e.code) {
case 'email-already-in-use':
throw const AuthFailure('This email is already registered. Try signing in.');
case 'weak-password':
throw const AuthFailure('Password must be at least 6 characters.');
case 'invalid-email':
throw const AuthFailure('That email address looks invalid.');
default:
throw AuthFailure('Sign-up failed: ${e.message}');
}
}
}
Send the verification email immediately after sign-up, and gate sensitive actions behind user.emailVerified. Refresh the user object with await user.reload() before checking — the flag doesn't update live.
Google Sign-In
On mobile, Google Sign-In through Firebase is a few lines:
import 'package:google_sign_in/google_sign_in.dart';
Future<UserCredential> signInWithGoogle() async {
final googleUser = await GoogleSignIn().signIn();
if (googleUser == null) {
throw const AuthFailure('Google sign-in was cancelled.');
}
final googleAuth = await googleUser.authentication;
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
return _auth.signInWithCredential(credential);
}
Android setup catches everyone once: add your debug and release SHA-1 fingerprints in the Firebase console, or Google Sign-In silently fails with ApiException: 10. iOS needs the reversed client ID in Info.plist — the FlutterFire CLI handles it, manual setups forget it.
Phone Authentication (OTP)
Phone auth is essential for markets where email isn't the primary identity. The flow is callback-heavy — wrap it in a clean API:
Future<void> verifyPhoneNumber(String phoneNumber) async {
await _auth.verifyPhoneNumber(
phoneNumber: phoneNumber, // E.164 format: +923001234567
timeout: const Duration(seconds: 60),
verificationCompleted: (PhoneAuthCredential credential) async {
// Android auto-verification: sign in silently
await _auth.signInWithCredential(credential);
},
verificationFailed: (FirebaseAuthException e) {
throw AuthFailure('Phone verification failed: ${e.message}');
},
codeSent: (String verificationId, int? resendToken) {
// Store verificationId, navigate to OTP screen
},
codeAutoRetrievalTimeout: (String verificationId) {},
);
}
Future<UserCredential> signInWithOtp({
required String verificationId,
required String smsCode,
}) {
final credential = PhoneAuthProvider.credential(
verificationId: verificationId,
smsCode: smsCode,
);
return _auth.signInWithCredential(credential);
}
On Android, enable the Play Integrity API in the Firebase console or phone auth gets throttled. On iOS, APNs push is required for silent verification — without it, users always type the code manually (which still works, it's just less smooth). Also note: Firebase phone auth requires a billing-enabled project once you exceed the free quota — set up budget alerts before launch.
Reacting to Auth State (The Right Way)
Never store "isLoggedIn" in a boolean. Listen to the auth state stream and let it drive your router:
class AuthGate extends StatelessWidget {
const AuthGate({super.key});
@override
Widget build(BuildContext context) {
return StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
if (snapshot.hasData) return const HomeScreen();
return const SignInScreen();
},
);
}
}
With go_router, use refreshListenable on the auth stream and redirect in one place. One source of truth, no scattered Navigator calls after login.
Account Linking: Don't Create Duplicate Users
A user signs up with email, then taps "Sign in with Google" using the same address. Default Firebase behavior: error (account-exists-with-different-credential). Handle it by linking:
try {
await signInWithGoogle();
} on FirebaseAuthException catch (e) {
if (e.code == 'account-exists-with-different-credential') {
// Fetch the pending credential, sign in with the existing
// provider first, then linkCredential(pendingCredential).
}
}
Fetch the sign-in methods for the email with fetchSignInMethodsForEmail, sign in with the existing provider, then call linkWithCredential using the pending credential from the exception. It's fiddly but it's the difference between a professional auth system and one that silently forks user accounts.
Security Checklist Before Launch
| Item | Why it matters |
|---|---|
Firestore rules check request.auth != null |
Client SDKs don't enforce anything by themselves |
| Email enumeration protection enabled | Prevents attackers harvesting registered emails |
| App Check enforced on Auth | Blocks credential-stuffing from scripts |
| Password policy configured | Firebase console → Authentication → Settings |
| Authorized domains restricted (web) | Stops your config keys being used on phishing clones |
Enable email enumeration protection in the Firebase console — it changes error messages slightly, so update your UI copy to match ("If an account exists, we've sent a reset link" instead of "user not found").
Common Pitfalls
- Missing SHA fingerprints. The #1 cause of "Google Sign-In works in debug, fails in release." Add release + Play App Signing SHAs.
- Ignoring
user.reload(). Email verification status, disabled accounts, and token refresh all need an explicit reload. - Storing tokens in plain SharedPreferences. Use
flutter_secure_storagefor anything sensitive; Firebase persists its own tokens securely, but your app's extra secrets shouldn't sit in plaintext. - No offline handling on the login screen. Firebase Auth caches sessions, but first-time sign-in needs network. Show a real error, not an infinite spinner.
- Skipping App Check. Without it, anyone with your public config can hammer your auth endpoints. Enforcement mode takes ten minutes to enable.
Sign Out and Session Hygiene
Sign-out is one line, but do the cleanup around it:
Future<void> signOut() async {
await GoogleSignIn().signOut(); // clears the Google session too
await FirebaseAuth.instance.signOut();
// Clear any locally cached user profile / onboarding flags here
}
Forgetting GoogleSignIn().signOut() is a classic bug: the user taps "sign out," taps "Sign in with Google" again, and is silently re-authenticated as the same account with no account picker. Always sign out of the provider alongside Firebase.
Also handle token refresh failures. If a user's refresh token is revoked (password change, admin disable), authStateChanges fires with null and your AuthGate sends them to login — which is correct behavior, not a bug. Test this path: disable a test user in the console and confirm the app handles it gracefully instead of looping.
Wrapping Up
Flutter Firebase authentication covers email, Google, and phone in under an hour of wiring — the remaining work is all in the details: account linking, security rules, App Check, and honest error states. Get those right and your auth system disappears into the background, which is exactly what good auth should do.
Need auth (or the whole app) built properly? Let's talk — I've shipped Firebase-backed Flutter apps to both stores for years.
Frequently Asked Questions
How do I add Google Sign-In to a Flutter app with Firebase?
Call GoogleSignIn().signIn(), then exchange the accessToken and idToken with GoogleAuthProvider.credential and sign in via FirebaseAuth.instance.signInWithCredential. On Android, register both debug and release SHA-1 fingerprints or sign-in fails silently with ApiException: 10; on iOS the reversed client ID belongs in Info.plist.
How do I link accounts when a user signs in with a different provider for the same email?
Firebase throws account-exists-with-different-credential by default. Fetch the existing sign-in methods with fetchSignInMethodsForEmail, sign in with the original provider, then call linkWithCredential using the pending credential from the exception — this prevents silently forking the user into two accounts.
How do I verify a user's email with Firebase Authentication?
Send the verification email right after sign-up with sendEmailVerification(), gate sensitive actions behind user.emailVerified, and call await user.reload() before checking the flag — it does not update live.
What is the right way to handle auth state in a Flutter app?
Never store an isLoggedIn boolean. Listen to FirebaseAuth.instance.authStateChanges() in an AuthGate (StreamBuilder or go_router refreshListenable) and let it drive the router — one source of truth with no scattered Navigator calls after login.