Email Authentication with Firebase

This guide covers how to implement email authentication in your Flutter app using Firebase Authentication, including the email verification flow.

Features

  • Email sign up
  • Email login
  • Email verification
  • Password reset
  • Account linking
  • Error handling

Implementation

1. Email Sign Up

Future<void> signupWithEmailAndPassword(String email, String password) async {
  try {
    // Create user with email and password
    final userCredential = await _firebaseAuth.createUserWithEmailAndPassword(
      email: email,
      password: password,
    );

    // Send verification email
    await userCredential.user?.sendEmailVerification();

    // Update user profile if needed
    await userCredential.user?.updateDisplayName(email.split('@')[0]);
    
  } on FirebaseAuthException catch (e) {
    switch (e.code) {
      case 'weak-password':
        throw Exception('The password provided is too weak.');
      case 'email-already-in-use':
        throw Exception('The account already exists for that email.');
      case 'invalid-email':
        throw Exception('Please provide a valid email');
      default:
        throw Exception('Email Signup Failed');
    }
  }
}

2. Email Login

Future<void> loginWithEmailAndPassword(String email, String password) async {
  try {
    final userCredential = await _firebaseAuth.signInWithEmailAndPassword(
      email: email,
      password: password,
    );

    // Check if email is verified
    if (!userCredential.user!.emailVerified) {
      // Optionally sign out the user if email is not verified
      await _firebaseAuth.signOut();
      throw Exception('Please verify your email before logging in');
    }

  } on FirebaseAuthException catch (e) {
    switch (e.code) {
      case 'invalid-credential':
        throw Exception('Invalid credentials');
      case 'wrong-password':
        throw Exception('Wrong password provided for that user.');
      case 'user-not-found':
        throw Exception('No user found with this email.');
      case 'user-disabled':
        throw Exception('This user has been disabled.');
      default:
        throw Exception('Email Login Failed');
    }
  }
}

3. Email Verification

Future<void> sendEmailVerification() async {
  try {
    final user = _firebaseAuth.currentUser;
    if (user != null && !user.emailVerified) {
      await user.sendEmailVerification();
    }
  } catch (e) {
    throw Exception('Failed to send verification email');
  }
}

Future<bool> checkEmailVerification() async {
  // Reload the user to get the latest email verification status
  await _firebaseAuth.currentUser?.reload();
  return _firebaseAuth.currentUser?.emailVerified ?? false;
}

4. Password Reset

Future<void> sendPasswordResetEmail(String email) async {
  try {
    await _firebaseAuth.sendPasswordResetEmail(email: email);
  } on FirebaseAuthException catch (e) {
    switch (e.code) {
      case 'user-not-found':
        throw Exception('No user found with this email.');
      case 'invalid-email':
        throw Exception('Please provide a valid email');
      default:
        throw Exception('Failed to send password reset email');
    }
  }
}

5. Account Linking

Future<void> linkEmailAccount(String email, String password) async {
  try {
    final credential = EmailAuthProvider.credential(
      email: email,
      password: password,
    );
    await _firebaseAuth.currentUser?.linkWithCredential(credential);
  } on FirebaseAuthException catch (e) {
    switch (e.code) {
      case 'provider-already-linked':
        throw Exception('This email is already linked to another account');
      case 'invalid-credential':
        throw Exception('Invalid email or password');
      case 'credential-already-in-use':
        throw Exception('This email is already in use by another account');
      default:
        throw Exception('Failed to link email account');
    }
  }
}

Best Practices

  1. Email Verification Flow

    • Always verify email addresses for new signups
    • Consider requiring email verification before allowing access to certain features
    • Provide clear feedback to users about verification status
  2. Password Security

    • Enforce strong password requirements
    • Implement password reset functionality
    • Store passwords securely using Firebase’s built-in security
  3. Error Handling

    • Provide user-friendly error messages
    • Log errors for debugging
    • Handle edge cases gracefully
  4. User Experience

    • Show loading states during authentication
    • Provide clear feedback for all actions
    • Implement proper form validation

Example Usage in UI

class EmailSignUpForm extends StatefulWidget {
  
  _EmailSignUpFormState createState() => _EmailSignUpFormState();
}

class _EmailSignUpFormState extends State<EmailSignUpForm> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  bool _isLoading = false;

  
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
            controller: _emailController,
            decoration: InputDecoration(labelText: 'Email'),
            validator: (value) {
              if (value == null || value.isEmpty) {
                return 'Please enter your email';
              }
              if (!value.contains('@')) {
                return 'Please enter a valid email';
              }
              return null;
            },
          ),
          TextFormField(
            controller: _passwordController,
            decoration: InputDecoration(labelText: 'Password'),
            obscureText: true,
            validator: (value) {
              if (value == null || value.isEmpty) {
                return 'Please enter your password';
              }
              if (value.length < 6) {
                return 'Password must be at least 6 characters';
              }
              return null;
            },
          ),
          ElevatedButton(
            onPressed: _isLoading ? null : _handleSignUp,
            child: _isLoading
                ? CircularProgressIndicator()
                : Text('Sign Up'),
          ),
        ],
      ),
    );
  }

  Future<void> _handleSignUp() async {
    if (_formKey.currentState!.validate()) {
      setState(() => _isLoading = true);
      try {
        await context.read<IAuthService>().signupWithEmailAndPassword(
          _emailController.text,
          _passwordController.text,
        );
        // Show verification email sent message
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Verification email sent!')),
        );
      } catch (e) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(e.toString())),
        );
      } finally {
        setState(() => _isLoading = false);
      }
    }
  }
}

Security Considerations

  1. Email Verification

    • Always verify email addresses for new accounts
    • Consider implementing rate limiting for verification emails
    • Store verification status in user profile
  2. Password Management

    • Use Firebase’s built-in password hashing
    • Implement password strength requirements
    • Provide secure password reset flow
  3. Session Management

    • Implement proper session handling
    • Consider implementing remember me functionality
    • Handle session timeouts appropriately

Testing

void main() {
  group('Email Authentication Tests', () {
    late FirebaseAuthService authService;
    late FirebaseAuth mockAuth;

    setUp(() {
      mockAuth = MockFirebaseAuth();
      authService = FirebaseAuthService(mockAuth);
    });

    test('signupWithEmailAndPassword success', () async {
      // Setup mock
      when(mockAuth.createUserWithEmailAndPassword(
        email: 'test@example.com',
        password: 'password123',
      )).thenAnswer((_) async => MockUserCredential());

      // Test
      await expectLater(
        authService.signupWithEmailAndPassword(
          'test@example.com',
          'password123',
        ),
        completes,
      );

      // Verify
      verify(mockAuth.createUserWithEmailAndPassword(
        email: 'test@example.com',
        password: 'password123',
      )).called(1);
    });

    test('loginWithEmailAndPassword with unverified email', () async {
      // Setup mock
      final mockUser = MockUser();
      when(mockUser.emailVerified).thenReturn(false);
      when(mockAuth.signInWithEmailAndPassword(
        email: 'test@example.com',
        password: 'password123',
      )).thenAnswer((_) async => MockUserCredential(user: mockUser));

      // Test
      await expectLater(
        authService.loginWithEmailAndPassword(
          'test@example.com',
          'password123',
        ),
        throwsException,
      );
    });
  });
}

Common Issues and Solutions

  1. Email Verification Not Working

    • Check spam folder
    • Verify email domain is not blocked
    • Ensure proper Firebase configuration
  2. Password Reset Issues

    • Verify email exists in system
    • Check email delivery status
    • Ensure proper Firebase configuration
  3. Account Linking Problems

    • Check if email is already in use
    • Verify credentials are correct
    • Ensure proper error handling

Next Steps

For more information about authentication in your Flutter app, check out: