DEV Community

Cover image for Flutter App Authentication: How to Block Fake Signups in Firebase & Supabase
VTPShopy
VTPShopy

Posted on

Flutter App Authentication: How to Block Fake Signups in Firebase & Supabase

In the mobile development landscape, Flutter has established itself as a premier cross-platform framework for building performant, single-codebase applications across iOS, Android, and the web. When pairing Flutter with modern backend-as-a-service (BaaS) platforms like Firebase or Supabase, mobile developers can construct robust authentication systems, real-time databases, and cloud function pipelines in hours.

However, mobile applications face a unique threat landscape. Because mobile APKs and IPA binaries can be decompiled, reverse-engineered, or targeted by automated REST API scripts, attackers frequently target mobile sign-up flows. Using automated headless scripts or custom clients, bad actors exploit free tiers by registering thousands of accounts using temporary, burner, and disposable email addresses.

Whether your app offers free cloud storage, AI token generations, or a 14-day trial, fake signups inflate your active user metrics while draining backend resources, corrupting analytics, and driving up infrastructure costs.

In this technical guide, we will examine how fake account creation impacts mobile backends, explore why standard mobile form validation fails, and walk through a step-by-step tutorial on implementing real-time email validation in Flutter for both Firebase and Supabase architectures.


Chapter 1: The Mobile Threat Vector (Firebase & Supabase)

Mobile authentication flows differ significantly from traditional web forms. While web apps interact with servers via browser sessions, mobile apps communicate with BaaS platforms through SDKs that expose direct public API keys (such as apiKey in Firebase or the anonKey in Supabase).

Why Mobile SDKs Are Target Vectors

In a standard Flutter application, developers initialize Firebase or Supabase directly in the main.dart entry point:

// Standard Firebase Initialization
await Firebase.initializeApp(
  options: DefaultFirebaseOptions.currentPlatform,
);

// Standard Supabase Initialization
await Supabase.initialize(
  url: 'https://xyzcompany.supabase.co',
  anonKey: 'public-anon-key-12345',
);

Enter fullscreen mode Exit fullscreen mode

While these keys are designed to be client-accessible, attackers can extract them directly from your Flutter app binary using basic reverse-engineering tools. Armed with your project URL and public key, an attacker does not even need to open your Flutter application UI. They can write a simple Python or Node.js script to call your Firebase or Supabase authentication endpoints directly, creating thousands of users every minute using disposable email addresses.

Downstream Consequences for Firebase & Supabase

Allowing temporary emails into your mobile backend triggers severe operational penalties:

  1. Firebase Authentication & Firestore Costs: Firebase charges based on database reads, writes, and stored documents. If your Flutter app uses Cloud Functions to create a user document in Cloud Firestore upon signup, thousands of fake signups generate thousands of unnecessary database writes and index updates.
  2. Supabase Auth & Database Bloat: Supabase runs on PostgreSQL. When a user registers via supabase.auth.signUp(), a row is inserted into the auth.users table. If your app uses database triggers to mirror users into a public.profiles table, fake signups pollute your primary database with dead rows, leading to table bloat and degraded query performance.
  3. Transactional Email Bounce Spikes: Both Firebase and Supabase automatically dispatch verification emails upon registration. When those emails attempt delivery to expired disposable domains, they generate hard bounces. High bounce rates damage your domain reputation and can cause your email provider (e.g., SendGrid, Resend, or AWS SES) to suspend your transactional email capabilities.

Chapter 2: The Inadequacy of Standard Mobile Validation

Flutter developers commonly validate form fields using client-side Regular Expressions (Regex) inside a TextFormField:

// Standard Flutter Regex Check (Inadequate)
String? validateEmail(String? value) {
  if (value == null || value.isEmpty) {
    return 'Please enter an email';
  }
  final regex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
  if (!regex.hasMatch(value)) {
    return 'Enter a valid email address';
  }
  return null; // Regarded as valid!
}

Enter fullscreen mode Exit fullscreen mode

Why Client-Side Checks Fail

  • Regex Blindness: A temporary email address like user99@10minutemail.com matches the RegExp pattern perfectly. Regex verifies format syntax, not domain deliverability or reputation.
  • Bypassing the UI: Automated scripts bypass the Flutter UI entirely by hitting the public Firebase/Supabase REST APIs directly. Client-side Dart validation code never executes during an API script attack.

To secure your mobile application, email validation must be executed via an edge-optimized API layer that evaluates domain threat intelligence in real time before the account is provisioned in Firebase or Supabase.


Chapter 3: Designing an Edge-First Defense with MailCheck

To protect your Flutter application without creating UI lag for mobile users on cellular networks, your validation API must deliver sub-100ms response times.

This is where MailCheck comes in. Engineered by FadSync Development Studio, MailCheck is a developer-first email validation API designed for high-speed application security. By cross-referencing incoming emails against a dynamic registry of over 40 million known disposable and high-risk domains, MailCheck delivers a response in sub-50 milliseconds.

By integrating MailCheck into your Flutter auth flow or backend middleware, you can block temporary email addresses before they touch your Firebase or Supabase instances.


Chapter 4: Implementing Email Validation in Flutter (Client-Side Interception)

Let's build a production-ready Flutter service that validates email addresses using MailCheck before executing registration logic.

Step 1: Create the Validation Service in Dart

Create a file named lib/services/email_validation_service.dart:

import 'dart:convert';
import 'package:http/http.dart' as http;

class ValidationResult {
  final bool isValid;
  final bool isDisposable;
  final String? errorMessage;

  ValidationResult({
    required this.isValid,
    required this.isDisposable,
    this.errorMessage,
  });
}

class EmailValidationService {
  // Use a secure proxy endpoint or environment variable
  static const String _baseUrl = 'https://api.mailcheck.fadsync.com/v1/validate';
  static const String _apiKey = 'YOUR_MAILCHECK_API_KEY';

  static Future<ValidationResult> validateEmail(String email) async {
    try {
      final response = await http.get(
        Uri.parse('$_baseUrl?email=${Uri.encodeComponent(email)}'),
        headers: {
          'Authorization': 'Bearer $_apiKey',
          'Content-Type': 'application/json',
        },
      ).timeout(const Duration(seconds: 2));

      if (response.statusCode == 200) {
        const Map<String, dynamic> data = jsonDecode(response.body);
        final bool isDisposable = data['is_disposable'] ?? false;
        final bool isValid = data['is_valid'] ?? true;

        if (isDisposable) {
          return ValidationResult(
            isValid: false,
            isDisposable: true,
            errorMessage: 'Disposable email addresses are not allowed. Please use a valid email.',
          );
        }

        if (!isValid) {
          return ValidationResult(
            isValid: false,
            isDisposable: false,
            errorMessage: 'This email address appears to be invalid or undeliverable.',
          );
        }

        return ValidationResult(isValid: true, isDisposable: false);
      } else {
        // Fail open on non-200 responses to ensure legitimate users are not blocked
        return ValidationResult(isValid: true, isDisposable: false);
      }
    } catch (e) {
      // Fail open on network errors or timeouts to prioritize user experience
      return ValidationResult(isValid: true, isDisposable: false);
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Chapter 5: Firebase Authentication Integration

Now let's wire the validation service into a Flutter registration screen backed by Firebase Auth.

import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import '../services/email_validation_service.dart';

class FirebaseRegisterScreen extends StatefulWidget {
  const FirebaseRegisterScreen({Key? key}) : super(key: key);

  @override
  State<FirebaseRegisterScreen> createState() => _FirebaseRegisterScreenState();
}

class _FirebaseRegisterScreenState extends State<FirebaseRegisterScreen> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();

  bool _isLoading = false;
  String? _errorMessage;

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

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

    final email = _emailController.text.trim();
    final password = _passwordController.text.trim();

    // 1. Intercept and Validate Email via MailCheck
    final validation = await EmailValidationService.validateEmail(email);

    if (!validation.isValid) {
      setState(() {
        _isLoading = false;
        _errorMessage = validation.errorMessage ?? 'Invalid email address.';
      });
      return; // HALT REGISTRATION
    }

    // 2. Email is Clean: Proceed with Firebase Registration
    try {
      final userCredential = await FirebaseAuth.instance.createUserWithEmailAndPassword(
        email: email,
        password: password,
      );

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

      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Account created! Please verify your email.')),
        );
      }
    } on FirebaseAuthException catch (e) {
      setState(() {
        _errorMessage = e.message ?? 'Authentication failed.';
      });
    } finally {
      if (mounted) {
        setState(() {
          _isLoading = false;
        });
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Create Account (Firebase)')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              if (_errorMessage != null) ...[
                Text(_errorMessage!, style: const TextStyle(color: Colors.red)),
                const SizedBox(height: 12),
              ],
              TextFormField(
                controller: _emailController,
                decoration: const InputDecoration(labelText: 'Email'),
                keyboardType: TextInputType.emailAddress,
                validator: (val) => val == null || !val.contains('@') ? 'Enter a valid email syntax' : null,
              ),
              const SizedBox(height: 12),
              TextFormField(
                controller: _passwordController,
                decoration: const InputDecoration(labelText: 'Password'),
                obscureText: true,
                validator: (val) => val != null && val.length < 6 ? 'Minimum 6 characters' : null,
              ),
              const SizedBox(height: 24),
              ElevatedButton(
                onPressed: _isLoading ? null : _handleRegister,
                child: _isLoading 
                    ? const CircularProgressIndicator(color: Colors.white)
                    : const Text('Register'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

Chapter 6: Supabase Authentication & Auth Hooks Integration

While client-side interception in Flutter prevents well-behaved app users from registering with disposable emails, automated scripts hitting your public API bypass client-side code entirely.

To completely secure Supabase, you should pair client validation with Supabase Auth Hooks.

Step 1: Client-Side Supabase Sign-Up in Flutter

import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../services/email_validation_service.dart';

class SupabaseRegisterScreen extends StatefulWidget {
  const SupabaseRegisterScreen({Key? key}) : super(key: key);

  @override
  State<SupabaseRegisterScreen> createState() => _SupabaseRegisterScreenState();
}

class _SupabaseRegisterScreenState extends State<SupabaseRegisterScreen> {
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  bool _isLoading = false;

  Future<void> _signUpWithSupabase() async {
    final email = _emailController.text.trim();
    final password = _passwordController.text.trim();

    setState(() => _isLoading = true);

    // 1. Client Interception Check
    final validation = await EmailValidationService.validateEmail(email);

    if (!validation.isValid) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(validation.errorMessage ?? 'Invalid email')),
        );
        setState(() => _isLoading = false);
      }
      return;
    }

    // 2. Supabase Signup Execution
    try {
      await Supabase.instance.client.auth.signUp(
        email: email,
        password: password,
      );

      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Registration successful! Check your inbox.')),
        );
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Sign up failed: ${e.toString()}')),
        );
      }
    } finally {
      if (mounted) setState(() => _isLoading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Register (Supabase)')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(controller: _emailController, decoration: const InputDecoration(labelText: 'Email')),
            TextField(controller: _passwordController, obscureText: true, decoration: const InputDecoration(labelText: 'Password')),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: _isLoading ? null : _signUpWithSupabase,
              child: _isLoading ? const CircularProgressIndicator() : const Text('Sign Up'),
            ),
          ],
        ),
      ),
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

Step 2: Supabase Auth Hook (Edge Functions)

To block API-level attacks that bypass your Flutter application code, configure a Supabase before-user-created Hook using a Supabase Edge Function (supabase/functions/validate-email/index.ts):

import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';

serve(async (req) => {
  try {
    const { record } = await req.json();
    const email = record.email;

    if (!email) {
      return new Response(JSON.stringify({ error: 'Email missing' }), { status: 400 });
    }

    const mailcheckKey = Deno.env.get('MAILCHECK_API_KEY');

    // Query MailCheck API synchronously inside the Edge Hook
    const res = await fetch(
      `https://api.mailcheck.fadsync.com/v1/validate?email=${encodeURIComponent(email)}`,
      {
        headers: {
          'Authorization': `Bearer ${mailcheckKey}`,
          'Content-Type': 'application/json',
        },
      }
    );

    if (res.ok) {
      const data = await res.json();
      if (data.is_disposable) {
        // Returning a 400 aborts user creation in Supabase Auth
        return new Response(
          JSON.stringify({ error: 'Disposable email addresses are strictly prohibited.' }),
          { status: 400, headers: { 'Content-Type': 'application/json' } }
        );
      }
    }

    // Allow user creation to proceed
    return new Response(JSON.stringify({ record }), {
      headers: { 'Content-Type': 'application/json' },
    });

  } catch (error) {
    // Fail open to preserve availability if validation endpoint times out
    return new Response(JSON.stringify({ error: error.message }), { status: 500 });
  }
});

Enter fullscreen mode Exit fullscreen mode

Conclusion

Securing Flutter applications that use Firebase or Supabase backends requires looking beyond client-side Regex validation. Because mobile SDK keys are exposed in client binaries, automated scripts can easily bypass Flutter UI validation and hit backend APIs directly.

By implementing real-time email verification with MailCheck at both the Flutter client level and the serverless hook layer, you ensure that temporary emails are blocked instantly. With sub-50ms latency across 40 million+ threat vectors, MailCheck keeps your mobile user database clean, your Firestore and PostgreSQL compute costs optimized, and your transactional email deliverability protected.

Top comments (0)