Flutter Firebase Authentication: A Complete, Modern Guide
A complete, pragmatic guide to Firebase Authentication in Flutter: setup, email, Google, Apple, phone, linking, security, and testing.
Image used for representation purposes only.
Overview
Firebase Authentication gives Flutter apps a secure, production‑ready identity layer with minimal overhead. It supports email/password, phone, anonymous accounts, and federated identity providers like Google, Apple, Facebook, Microsoft, GitHub, and generic OIDC. This guide walks through setup, core flows, platform nuances, security hardening, and testing strategies you can use in real apps.
Why use Firebase Auth in Flutter?
- Cross‑platform: Android, iOS, Web, macOS, and desktop (limited providers on some platforms) with one Dart API.
- Battle‑tested security: Automatic token refresh, password hashing, anti‑abuse checks for phone sign‑in, and optional multi‑factor auth.
- Tight ecosystem: Works seamlessly with Firestore/RTDB rules, Cloud Functions, and Cloud Storage.
- Developer velocity: Turnkey backend, prebuilt UI (optional), and an Emulator Suite for local development.
Project setup
- Create your Firebase project and enable Authentication
- In the Firebase console, add your Android, iOS, and/or Web apps.
- In Authentication → Sign‑in method, enable the providers you intend to use (Email/Password, Google, Apple, Phone, etc.).
- Add FlutterFire
- Install the CLI and configure platforms:
dart pub global activate flutterfire_cli
firebase login
flutterfire configure # selects your project and generates lib/firebase_options.dart
- Add dependencies
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
firebase_core: any
firebase_auth: any
google_sign_in: any # for Google on mobile
sign_in_with_apple: any # for Apple on iOS/macOS
- Initialize Firebase in your app
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(const MyApp());
}
- Platform notes
- Android: Add google-services.json and the Google Services Gradle plugin. For phone auth, add SHA‑1/SHA‑256 fingerprints in the console.
- iOS: Add GoogleService-Info.plist, enable Keychain Sharing for persistent sessions; for phone auth, set up APNs.
- Web: No script tags needed when using FlutterFire; the configuration in firebase_options.dart is used at runtime.
Observing auth state
Use the stream to build your app shell around signed‑in vs. signed‑out states.
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 HomePage();
}
return const SignInPage();
},
);
}
}
Related streams:
- idTokenChanges(): emits on ID token refresh and sign‑in/out.
- userChanges(): emits on profile updates in addition to the above.
Email and password
Create account
Future<void> register(String email, String password) async {
try {
final cred = await FirebaseAuth.instance
.createUserWithEmailAndPassword(email: email, password: password);
await cred.user!.sendEmailVerification();
} on FirebaseAuthException catch (e) {
// e.code: weak-password, email-already-in-use, invalid-email, operation-not-allowed
rethrow;
}
}
Sign in and sign out
Future<UserCredential> signIn(String email, String password) {
return FirebaseAuth.instance.signInWithEmailAndPassword(
email: email,
password: password,
);
}
Future<void> signOut() => FirebaseAuth.instance.signOut();
Password reset and updates
Future<void> sendReset(String email) =>
FirebaseAuth.instance.sendPasswordResetEmail(email: email);
Future<void> changePassword(String newPassword) async {
final user = FirebaseAuth.instance.currentUser!;
try {
await user.updatePassword(newPassword);
} on FirebaseAuthException catch (e) {
// requires-recent-login if session is old → reauthenticate first
rethrow;
}
}
Google Sign‑In
- Mobile (Android/iOS): use google_sign_in to obtain tokens, then sign in to Firebase.
- Web: call signInWithPopup with GoogleAuthProvider.
// Mobile
import 'package:google_sign_in/google_sign_in.dart';
import 'package:firebase_auth/firebase_auth.dart';
Future<UserCredential> signInWithGoogleMobile() async {
final googleUser = await GoogleSignIn().signIn();
if (googleUser == null) throw Exception('Sign-in aborted');
final googleAuth = await googleUser.authentication;
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
return FirebaseAuth.instance.signInWithCredential(credential);
}
// Web (kIsWeb true)
Future<UserCredential> signInWithGoogleWeb() {
final provider = GoogleAuthProvider();
return FirebaseAuth.instance.signInWithPopup(provider);
}
Sign in with Apple
- iOS/macOS: use sign_in_with_apple to request Apple ID and exchange for a Firebase credential.
- Web: use AppleAuthProvider with signInWithPopup.
// iOS/macOS
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
import 'package:firebase_auth/firebase_auth.dart';
Future<UserCredential> signInWithApple() async {
final apple = await SignInWithApple.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.email,
AppleIDAuthorizationScopes.fullName,
],
);
final oauth = OAuthProvider('apple.com').credential(
idToken: apple.identityToken,
accessToken: apple.authorizationCode,
);
return FirebaseAuth.instance.signInWithCredential(oauth);
}
// Web
Future<UserCredential> signInWithAppleWeb() {
final provider = AppleAuthProvider();
return FirebaseAuth.instance.signInWithPopup(provider);
}
Platform compliance note: On iOS, if your app offers third‑party social login, Apple may require offering Sign in with Apple as well.
Phone authentication
Phone sign‑in uses SMS verification and device checks.
Future<void> signInWithPhone(String phoneNumber) async {
await FirebaseAuth.instance.verifyPhoneNumber(
phoneNumber: phoneNumber,
verificationCompleted: (PhoneAuthCredential credential) async {
// Android: instant verification or auto-retrieval
await FirebaseAuth.instance.signInWithCredential(credential);
},
verificationFailed: (FirebaseAuthException e) {
// invalid-phone-number, too-many-requests, quota-exceeded, etc.
},
codeSent: (String verificationId, int? resendToken) async {
// Ask user for the SMS code:
final smsCode = await promptForCode();
final credential = PhoneAuthProvider.credential(
verificationId: verificationId,
smsCode: smsCode,
);
await FirebaseAuth.instance.signInWithCredential(credential);
},
codeAutoRetrievalTimeout: (String verificationId) {},
);
}
Requirements:
- Android: add SHA‑1/SHA‑256 keys; device needs Google Play services.
- iOS: configure APNs; test on real devices.
Anonymous auth and upgrading accounts
Anonymous sessions let users try your app before committing.
Future<UserCredential> guest() => FirebaseAuth.instance.signInAnonymously();
Future<void> upgradeFromAnonymous(AuthCredential credential) async {
final user = FirebaseAuth.instance.currentUser!; // anonymous
await user.linkWithCredential(credential); // preserves data tied to uid
}
Linking multiple providers
Let users attach multiple sign‑in methods to the same account.
Future<void> linkGoogle() async {
final cred = await signInWithGoogleMobile(); // or build a credential another way
await FirebaseAuth.instance.currentUser!.linkWithCredential(cred.credential!);
}
Future<void> unlink(String providerId) async {
await FirebaseAuth.instance.currentUser!.unlink(providerId); // e.g., 'google.com'
}
Common provider IDs: password, phone, google.com, apple.com, github.com, microsoft.com, yahoo.com, twitter.com, facebook.com.
Session persistence and tokens
- Mobile/desktop: Sessions persist in secure local storage by default.
- Web: choose persistence level.
// Web only
import 'package:firebase_auth/firebase_auth.dart' show Persistence;
await FirebaseAuth.instance.setPersistence(Persistence.LOCAL); // or SESSION/NONE
ID tokens
final token = await FirebaseAuth.instance.currentUser!.getIdToken();
final fresh = await FirebaseAuth.instance.currentUser!.getIdToken(true); // force refresh
Protecting backend resources
Have your backend verify Firebase ID tokens. Example (Node.js, Cloud Functions, or any server using the Admin SDK):
const admin = require('firebase-admin');
admin.initializeApp();
async function verify(req, res) {
const header = req.headers.authorization || '';
const idToken = header.startsWith('Bearer ') ? header.slice(7) : undefined;
try {
const decoded = await admin.auth().verifyIdToken(idToken);
// decoded.uid, decoded.email, decoded.firebase.sign_in_provider
res.json({ uid: decoded.uid, claims: decoded });
} catch (e) {
res.status(401).send('Unauthorized');
}
}
Firestore/RTDB security rules essentials
Tie access to the authenticated user’s UID.
// Firestore rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{uid} {
allow read, write: if request.auth != null && request.auth.uid == uid;
}
}
}
// Realtime Database rules
{
"rules": {
"users": {
"$uid": {
".read": "auth != null && auth.uid == $uid",
".write": "auth != null && auth.uid == $uid"
}
}
}
}
Error handling patterns
Catch FirebaseAuthException and branch on the error code for UX‑friendly messages.
try {
await FirebaseAuth.instance.signInWithEmailAndPassword(
email: email,
password: password,
);
} on FirebaseAuthException catch (e) {
switch (e.code) {
case 'invalid-email':
case 'user-not-found':
case 'wrong-password':
showToast('Invalid credentials');
break;
case 'user-disabled':
showToast('Account disabled');
break;
case 'too-many-requests':
showToast('Too many attempts. Try again later.');
break;
case 'requires-recent-login':
// prompt reauthentication
break;
default:
showToast('Sign-in failed: ${e.code}');
}
}
Tip: For i18n, map codes to localized strings rather than surfacing raw messages.
Reauthentication for sensitive actions
Certain operations require a recent login (e.g., changing email/password, deleting an account). Prompt users to re‑enter credentials, then continue.
Future<void> reauthAndDelete(String email, String password) async {
final user = FirebaseAuth.instance.currentUser!;
final cred = EmailAuthProvider.credential(email: email, password: password);
await user.reauthenticateWithCredential(cred);
await user.delete();
}
Multi‑factor authentication (MFA)
Firebase Auth supports SMS‑based second factors for eligible providers and projects. The high‑level flow:
- Enroll: After primary sign‑in, enroll a phone number as a second factor.
- Challenge: On subsequent sign‑ins, complete the SMS challenge.
- Recovery: Provide fallback and account recovery paths. Consider gating MFA behind a feature flag and offering opt‑in to start.
Developer productivity: Prebuilt UI and routing
- FirebaseUI for Flutter can rapidly ship polished auth screens with common providers and email flows, while you retain control over theming and navigation.
- Guarded routes: Wrap your router with an AuthGate (see above) or use middleware/redirects so protected pages require a user.
Local development with the Emulator Suite
Avoid hitting production while developing and writing tests.
Future<void> connectToEmulator() async {
// Call before any auth operations (e.g., right after Firebase.initializeApp)
await FirebaseAuth.instance.useAuthEmulator('localhost', 9099);
}
Use Firebase Emulator UI to inspect users and sign‑in flows locally. Combine with Firestore/RTDB emulators for end‑to‑end testing.
Security hardening checklist
- Enforce email verification before granting privileged access.
- Least‑privilege rules in Firestore/RTDB based on request.auth.uid.
- Use App Check to protect backend resources from abuse.
- Rate‑limit sensitive endpoints on your server, and always verify ID tokens.
- Show provider‑specific UI checks (e.g., device integrity prompts) only as needed.
- Keep third‑party provider credentials (e.g., Apple keys) rotated and restricted.
Troubleshooting quick hits
- account-exists-with-different-credential: Prompt the user to sign in with the existing provider, then link the new one.
- invalid-credential / expired-action-code: Refresh the credential (restart OAuth flow) or resend verification/reset emails.
- network-request-failed: Implement offline UI states and retry with exponential backoff.
- phone auth test devices: Use test phone numbers in Authentication → Sign‑in method to avoid SMS charges during QA.
Putting it all together
- Decide which providers you truly need; start with Email/Password + Google/Apple for consumer apps.
- Build a small AuthGate shell and test it with the Emulator Suite.
- Harden with rules, verified backend tokens, and reauth flows.
- Add provider linking and account management once the basics are stable.
With these building blocks, you can ship a robust Flutter app with secure, user‑friendly authentication—without owning a complex identity backend.
Related Posts
Flutter REST API Integration: A Practical, Production-Ready Guide
End-to-end Flutter REST API integration: http vs Dio, auth, error handling, caching, pagination, testing, security, and best practices.
React Server Components Tutorial: Build Faster Apps with Less Client JavaScript
Learn React Server Components with Next.js and React Router: server-first data, client boundaries, caching, mutations, and security in 2026.
Flutter State Management Guide: Patterns, Packages, and Practical Examples
A practical, end-to-end Flutter state management guide with patterns, packages, examples, and performance tips for building scalable, testable apps.