← Articles

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.

Flutter + Firebase Authentication: complete guide — ANN Tech