Flutter + Firebase Authentication: complete guide
By Ann Tech · 11 March 2025
Firebase Authentication handles the complexity of secure identity management — token refresh, session persistence, OAuth flows — so you don't have to. Here is a complete guide to integrating it in Flutter with proper state management.
Setup
dependencies:
firebase_auth: ^5.0.0
firebase_core: ^3.0.0
google_sign_in: ^6.2.0 # For Google OAuth
Auth state stream
The most important concept: Firebase manages session persistence. You never need to store tokens — just listen to authStateChanges():
// The stream emits null when signed out, User when signed in
FirebaseAuth.instance.authStateChanges().listen((User? user) {
if (user == null) {
// Navigate to login screen
} else {
// Navigate to home screen
}
});
Auth state with Riverpod
@riverpod
Stream<User?> authState(AuthStateRef ref) {
return FirebaseAuth.instance.authStateChanges();
}
@riverpod
bool isSignedIn(IsSignedInRef ref) {
final user = ref.watch(authStateProvider);
return user.value != null;
}
// In your router redirect:
GoRouter(
redirect: (context, state) {
final isSignedIn = ref.read(isSignedInProvider);
final isAuthRoute = state.uri.path.startsWith('/auth');
if (!isSignedIn && !isAuthRoute) return '/auth/login';
if (isSignedIn && isAuthRoute) return '/';
return null;
},
)
Email/password auth
class AuthRepository {
final _auth = FirebaseAuth.instance;
Future<UserCredential> signIn(String email, String password) async {
try {
return await _auth.signInWithEmailAndPassword(
email: email.trim(),
password: password,
);
} on FirebaseAuthException catch (e) {
throw _mapError(e);
}
}
Future<UserCredential> signUp(String email, String password, String name) async {
try {
final credential = await _auth.createUserWithEmailAndPassword(
email: email.trim(),
password: password,
);
// Update display name
await credential.user!.updateDisplayName(name);
// Send verification email
await credential.user!.sendEmailVerification();
return credential;
} on FirebaseAuthException catch (e) {
throw _mapError(e);
}
}
Future<void> signOut() async {
await _auth.signOut();
}
Future<void> sendPasswordResetEmail(String email) async {
await _auth.sendPasswordResetEmail(email: email.trim());
}
String _mapError(FirebaseAuthException e) {
return switch (e.code) {
'user-not-found' => 'No account found for this email.',
'wrong-password' => 'Incorrect password.',
'email-already-in-use' => 'An account with this email already exists.',
'weak-password' => 'Password must be at least 6 characters.',
'invalid-email' => 'Please enter a valid email address.',
'too-many-requests' => 'Too many attempts. Try again later.',
_ => 'Authentication failed. Please try again.',
};
}
}
Google Sign-In
Future<UserCredential?> signInWithGoogle() async {
final googleUser = await GoogleSignIn().signIn();
if (googleUser == null) return null; // User cancelled
final googleAuth = await googleUser.authentication;
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
return await FirebaseAuth.instance.signInWithCredential(credential);
}
Apple Sign-In (iOS required for apps with social login)
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
Future<UserCredential> signInWithApple() async {
final appleCredential = await SignInWithApple.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.email,
AppleIDAuthorizationScopes.fullName,
],
);
final oauthCredential = OAuthProvider('apple.com').credential(
idToken: appleCredential.identityToken,
accessToken: appleCredential.authorizationCode,
);
return await FirebaseAuth.instance.signInWithCredential(oauthCredential);
}
ID token for backend requests
When calling your own backend, include the Firebase ID token:
Future<String?> getIdToken({bool forceRefresh = false}) async {
return FirebaseAuth.instance.currentUser?.getIdToken(forceRefresh);
}
// In Dio interceptor:
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) async {
final token = await getIdToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
},
onError: (error, handler) async {
if (error.response?.statusCode == 401) {
// Token expired — refresh and retry
final token = await getIdToken(forceRefresh: true);
if (token != null) {
error.requestOptions.headers['Authorization'] = 'Bearer $token';
final retryResponse = await dio.fetch(error.requestOptions);
return handler.resolve(retryResponse);
}
}
handler.next(error);
},
));
Linking accounts
Allow users to sign in with multiple providers:
Future<void> linkGoogleAccount() async {
final googleUser = await GoogleSignIn().signIn();
if (googleUser == null) return;
final googleAuth = await googleUser.authentication;
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
await FirebaseAuth.instance.currentUser!.linkWithCredential(credential);
}
Firestore security rules for auth
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Users can only read/write their own data
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
// Orders: readable by owner, writable by backend (via Admin SDK)
match /orders/{orderId} {
allow read: if request.auth != null
&& resource.data.userId == request.auth.uid;
allow write: if false; // Backend only
}
}
}
Common pitfalls
Checking currentUser instead of authStateChanges(). currentUser is null until Firebase finishes loading the persisted session (usually < 1 second, but not zero). Use authStateChanges() stream to react to auth state properly.
Not refreshing the ID token. Firebase ID tokens expire after 1 hour. Always use getIdToken() (which refreshes automatically) rather than caching the raw token string.
Forgetting Apple Sign-In. Apple requires that any app offering third-party social login must also offer "Sign in with Apple." Missing this causes App Store rejection.
Sign in to like, dislike, or report.