The Flutter Kit logoThe Flutter Kit
Tutorial

Firebase Authentication in Flutter: Complete Tutorial with Email, Google & Apple Sign-In

Complete tutorial for implementing Firebase Auth in Flutter with email, Google, and Apple sign-in flows.

Ahmed GaganAhmed Gagan
15 min read

Authentication is the first thing you build and the last thing you want to debug in production. This tutorial walks you through implementing Firebase Authentication in Flutter from scratch -- email/password, Google Sign-In, Sign in with Apple, anonymous auth, and proper auth state management. Every code example works on both iOS and Android, and I will show you the architecture pattern I use in every production app.

Project Setup

Before writing any auth code, you need Firebase configured in your Flutter project. The fastest way is the FlutterFire CLI:

# Install the FlutterFire CLI
dart pub global activate flutterfire_cli

# Configure Firebase (generates firebase_options.dart)
flutterfire configure

# Add required packages
flutter pub add firebase_core firebase_auth
flutter pub add google_sign_in
flutter pub add sign_in_with_apple
flutter pub add crypto

Initialize Firebase in your main.dart:

// main.dart
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());
}

In the Firebase Console, enable the auth providers you want: Email/Password, Google, and Apple. Each provider has platform-specific setup that I will cover below.

Creating the Auth Repository

The key architectural decision is wrapping Firebase Auth in a repository class. This keeps Firebase imports out of your UI layer and makes the auth logic testable:

// auth_repository.dart
import 'package:firebase_auth/firebase_auth.dart';

abstract class AuthRepository {
  Stream<User?> get authStateChanges;
  User? get currentUser;
  Future<UserCredential> signInWithEmail(String email, String password);
  Future<UserCredential> signUpWithEmail(String email, String password);
  Future<UserCredential> signInWithGoogle();
  Future<UserCredential> signInWithApple();
  Future<UserCredential> signInAnonymously();
  Future<void> signOut();
  Future<void> deleteAccount();
  Future<void> resetPassword(String email);
}

class FirebaseAuthRepository implements AuthRepository {
  final FirebaseAuth _auth;

  FirebaseAuthRepository({FirebaseAuth? auth})
      : _auth = auth ?? FirebaseAuth.instance;

  @override
  Stream<User?> get authStateChanges => _auth.authStateChanges();

  @override
  User? get currentUser => _auth.currentUser;

  @override
  Future<UserCredential> signInWithEmail(
    String email,
    String password,
  ) async {
    try {
      return await _auth.signInWithEmailAndPassword(
        email: email,
        password: password,
      );
    } on FirebaseAuthException catch (e) {
      throw _mapAuthException(e);
    }
  }

  @override
  Future<UserCredential> signUpWithEmail(
    String email,
    String password,
  ) async {
    try {
      return await _auth.createUserWithEmailAndPassword(
        email: email,
        password: password,
      );
    } on FirebaseAuthException catch (e) {
      throw _mapAuthException(e);
    }
  }

  @override
  Future<void> signOut() async {
    await _auth.signOut();
  }

  @override
  Future<void> deleteAccount() async {
    final user = _auth.currentUser;
    if (user == null) throw AuthException('No user signed in');
    await user.delete();
  }

  @override
  Future<void> resetPassword(String email) async {
    await _auth.sendPasswordResetEmail(email: email);
  }

  AuthException _mapAuthException(FirebaseAuthException e) {
    switch (e.code) {
      case 'user-not-found':
        return AuthException('No account found with this email.');
      case 'wrong-password':
        return AuthException('Incorrect password.');
      case 'email-already-in-use':
        return AuthException('An account already exists with this email.');
      case 'weak-password':
        return AuthException('Password must be at least 6 characters.');
      case 'invalid-email':
        return AuthException('Please enter a valid email address.');
      case 'too-many-requests':
        return AuthException('Too many attempts. Please try again later.');
      default:
        return AuthException(e.message ?? 'Authentication failed.');
    }
  }
}

class AuthException implements Exception {
  final String message;
  AuthException(this.message);

  @override
  String toString() => message;
}

Email and Password Authentication

Email/password is the baseline auth method. Here is a sign-in screen that handles both sign-in and sign-up with proper validation and error handling:

// email_auth_screen.dart
import 'package:flutter/material.dart';

class EmailAuthScreen extends StatefulWidget {
  const EmailAuthScreen({super.key});

  @override
  State<EmailAuthScreen> createState() => _EmailAuthScreenState();
}

class _EmailAuthScreenState extends State<EmailAuthScreen> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  bool _isSignUp = false;
  bool _isLoading = false;
  String? _errorMessage;

  Future<void> _submit() async {
    if (!_formKey.currentState!.validate()) return;

    setState(() {
      _isLoading = true;
      _errorMessage = null;
    });

    try {
      final authRepo = FirebaseAuthRepository();
      if (_isSignUp) {
        await authRepo.signUpWithEmail(
          _emailController.text.trim(),
          _passwordController.text,
        );
      } else {
        await authRepo.signInWithEmail(
          _emailController.text.trim(),
          _passwordController.text,
        );
      }
      // Navigation handled by auth state listener
    } on AuthException catch (e) {
      setState(() => _errorMessage = e.message);
    } finally {
      if (mounted) setState(() => _isLoading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(_isSignUp ? 'Create Account' : 'Sign In'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              TextFormField(
                controller: _emailController,
                keyboardType: TextInputType.emailAddress,
                decoration: const InputDecoration(
                  labelText: 'Email',
                  prefixIcon: Icon(Icons.email_outlined),
                ),
                validator: (v) => v != null && v.contains('@')
                    ? null
                    : 'Enter a valid email',
              ),
              const SizedBox(height: 16),
              TextFormField(
                controller: _passwordController,
                obscureText: true,
                decoration: const InputDecoration(
                  labelText: 'Password',
                  prefixIcon: Icon(Icons.lock_outlined),
                ),
                validator: (v) => v != null && v.length >= 6
                    ? null
                    : 'Password must be at least 6 characters',
              ),
              const SizedBox(height: 24),
              if (_errorMessage != null)
                Padding(
                  padding: const EdgeInsets.only(bottom: 16),
                  child: Text(
                    _errorMessage!,
                    style: TextStyle(
                      color: Theme.of(context).colorScheme.error,
                    ),
                  ),
                ),
              FilledButton(
                onPressed: _isLoading ? null : _submit,
                child: _isLoading
                    ? const SizedBox(
                        height: 20,
                        width: 20,
                        child: CircularProgressIndicator(
                          strokeWidth: 2,
                        ),
                      )
                    : Text(_isSignUp ? 'Create Account' : 'Sign In'),
              ),
              TextButton(
                onPressed: () =>
                    setState(() => _isSignUp = !_isSignUp),
                child: Text(_isSignUp
                    ? 'Already have an account? Sign In'
                    : 'No account? Create one'),
              ),
            ],
          ),
        ),
      ),
    );
  }

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }
}

Google Sign-In

Google Sign-In works on both iOS and Android with the google_sign_in package. Add the implementation to your auth repository:

// In FirebaseAuthRepository
import 'package:google_sign_in/google_sign_in.dart';

@override
Future<UserCredential> signInWithGoogle() async {
  // Trigger the Google Sign-In flow
  final googleUser = await GoogleSignIn().signIn();
  if (googleUser == null) {
    throw AuthException('Google sign-in was cancelled.');
  }

  // Obtain the auth details
  final googleAuth = await googleUser.authentication;

  // Create a Firebase credential
  final credential = GoogleAuthProvider.credential(
    accessToken: googleAuth.accessToken,
    idToken: googleAuth.idToken,
  );

  // Sign in to Firebase
  return await _auth.signInWithCredential(credential);
}

Platform Setup for Google Sign-In

iOS

  1. In the Firebase Console, download GoogleService-Info.plist and add it to your iOS project
  2. Add the reversed client ID URL scheme to your Info.plist
<!-- ios/Runner/Info.plist -->
<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>com.googleusercontent.apps.YOUR_REVERSED_CLIENT_ID</string>
    </array>
  </dict>
</array>

Android

  1. In the Firebase Console, add your SHA-1 fingerprint to the Android app configuration
  2. Download google-services.json and place it in android/app/
  3. No additional code changes needed -- the plugin handles it

Sign in with Apple

Sign in with Apple is required by Apple if your app offers any other social sign-in method. It also works on Android, which is a nice bonus with Flutter. Add to your auth repository:

// In FirebaseAuthRepository
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
import 'dart:convert';
import 'package:crypto/crypto.dart';

@override
Future<UserCredential> signInWithApple() async {
  // Generate a nonce for security
  final rawNonce = generateNonce();
  final nonce = sha256ofString(rawNonce);

  // Request Apple credential
  final appleCredential =
      await SignInWithApple.getAppleIDCredential(
    scopes: [
      AppleIDAuthorizationScopes.email,
      AppleIDAuthorizationScopes.fullName,
    ],
    nonce: nonce,
  );

  // Create Firebase credential
  final oauthCredential = OAuthProvider('apple.com').credential(
    idToken: appleCredential.identityToken,
    rawNonce: rawNonce,
  );

  // Sign in to Firebase
  final userCredential =
      await _auth.signInWithCredential(oauthCredential);

  // Apple only returns the name on first sign-in
  // Store it if available
  if (appleCredential.givenName != null) {
    await userCredential.user?.updateDisplayName(
      '${appleCredential.givenName} ${appleCredential.familyName}',
    );
  }

  return userCredential;
}

String generateNonce([int length = 32]) {
  const charset =
      '0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._';
  final random = Random.secure();
  return List.generate(
    length,
    (_) => charset[random.nextInt(charset.length)],
  ).join();
}

String sha256ofString(String input) {
  final bytes = utf8.encode(input);
  final digest = sha256.convert(bytes);
  return digest.toString();
}

Platform Setup for Sign in with Apple

iOS

  1. In Xcode, enable the "Sign in with Apple" capability under Signing & Capabilities
  2. In the Apple Developer Portal, enable Sign in with Apple for your App ID
  3. In Firebase Console, enable Apple as a sign-in provider

Android

Sign in with Apple works on Android through a web-based flow. In the Firebase Console, configure the Apple provider with your Services ID and redirect URL. The sign_in_with_apple package handles the web redirect automatically.

Anonymous Authentication

Anonymous auth lets users try your app without creating an account. They get a temporary Firebase UID and can link their account to a permanent auth method later. This is great for reducing friction during onboarding:

// In FirebaseAuthRepository
@override
Future<UserCredential> signInAnonymously() async {
  return await _auth.signInAnonymously();
}

// Later, when they want to create a real account:
Future<UserCredential> linkWithEmail(
  String email,
  String password,
) async {
  final credential = EmailAuthProvider.credential(
    email: email,
    password: password,
  );
  return await _auth.currentUser!.linkWithCredential(credential);
}

Auth State Management

The most important piece is reacting to auth state changes throughout your app. Firebase provides a stream that emits whenever the auth state changes (sign in, sign out, token refresh). Here is how to integrate it with GoRouter for declarative navigation:

// app_router.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:go_router/go_router.dart';

final appRouter = GoRouter(
  refreshListenable:
      GoRouterRefreshStream(FirebaseAuth.instance.authStateChanges()),
  redirect: (context, state) {
    final isLoggedIn = FirebaseAuth.instance.currentUser != null;
    final isOnAuthPage =
        state.matchedLocation.startsWith('/auth');
    final hasCompletedOnboarding =
        /* check SharedPreferences */ true;

    if (!isLoggedIn && !isOnAuthPage) {
      return '/auth';
    }
    if (isLoggedIn && isOnAuthPage) {
      return hasCompletedOnboarding ? '/home' : '/onboarding';
    }
    return null; // No redirect needed
  },
  routes: [
    GoRoute(path: '/auth', builder: (_, __) => const AuthScreen()),
    GoRoute(path: '/onboarding', builder: (_, __) => const OnboardingScreen()),
    GoRoute(path: '/home', builder: (_, __) => const HomeScreen()),
  ],
);

// Helper class to convert Stream to Listenable for GoRouter
class GoRouterRefreshStream extends ChangeNotifier {
  GoRouterRefreshStream(Stream<dynamic> stream) {
    stream.listen((_) => notifyListeners());
  }
}

With this setup, navigation is completely reactive. When a user signs in, they are automatically redirected to the home screen (or onboarding if it is their first time). When they sign out, they go back to the auth screen. No manual navigation calls needed.

Account Deletion (Required by Both Stores)

Both Apple and Google require that apps with account creation also provide account deletion. Here is a complete implementation with confirmation dialog:

// delete_account_dialog.dart
Future<void> showDeleteAccountDialog(BuildContext context) async {
  final confirmed = await showDialog<bool>(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('Delete Account'),
      content: const Text(
        'This will permanently delete your account and all '
        'associated data. This action cannot be undone.',
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context, false),
          child: const Text('Cancel'),
        ),
        FilledButton(
          style: FilledButton.styleFrom(
            backgroundColor: Theme.of(context).colorScheme.error,
          ),
          onPressed: () => Navigator.pop(context, true),
          child: const Text('Delete Account'),
        ),
      ],
    ),
  );

  if (confirmed == true && context.mounted) {
    try {
      final authRepo = FirebaseAuthRepository();
      await authRepo.deleteAccount();
      // Auth state listener handles navigation
    } on AuthException catch (e) {
      if (context.mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(e.message)),
        );
      }
    }
  }
}

Common Mistakes to Avoid

  • Not handling re-authentication -- Firebase requires recent authentication for sensitive operations (delete account, change email). If the session is old, you need to re-authenticate first.
  • Forgetting Apple name is only returned once -- Sign in with Apple only returns the user's name on the first sign-in. Store it in Firestore immediately or you lose it forever.
  • Not testing on both platforms -- Google Sign-In requires SHA-1 on Android and URL schemes on iOS. Test both.
  • Ignoring token refresh -- Firebase tokens expire after 1 hour. The SDK handles refresh automatically, but if you are passing tokens to your backend, ensure it validates them properly.
  • Not implementing sign-out properly -- Sign out from Firebase AND from Google/Apple. Otherwise the next sign-in attempt silently reuses the old session.
  • Missing account deletion -- Both Apple and Google will reject your app if you have sign-in but no delete-account functionality.

Production Auth Checklist

  1. Email/password sign-in and sign-up work on both platforms
  2. Google Sign-In works on both iOS and Android
  3. Sign in with Apple works on iOS (and optionally Android)
  4. Sign out clears all auth state
  5. Delete account works and removes user data
  6. Password reset email sends and the link works
  7. Auth state changes trigger proper navigation
  8. Error messages are user-friendly (not Firebase error codes)
  9. Loading states show during auth operations
  10. Network errors are handled gracefully

Skip the Setup -- Use The Flutter Kit

Everything in this tutorial -- the auth repository, email sign-in, Google Sign-In, Sign in with Apple, anonymous auth, auth state management, GoRouter integration, account deletion, and error handling -- is already built, tested, and production-ready in The Flutter Kit. You configure Firebase with one CLI command, and the entire auth system works on both iOS and Android.

Get The Flutter Kit for $69 and have authentication running on both platforms in under 15 minutes. Or browse the feature list to see everything that is included beyond auth.

Share this article

Ready to ship your Flutter app faster?

The Flutter Kit gives you a production-ready Flutter codebase with onboarding, paywalls, auth, AI integrations, and more. Stop building boilerplate. Start building your product.

Get The Flutter Kit