LogoLogo
HomeArticlesCommunity ProductsPremium ProductsGitHubTalsec Website
  • Introduction
  • articles
    • AI Device Risk Summary Demo | Threat Protection | Risk Scoring | Malware Detection | Android & iOS
    • Podcast: iOS Keychain vs Android Keystore
    • Obfuscation of Mobile Apps
    • OWASP Top 10 For Flutter – M6: Inadequate Privacy Controls in Flutter & Dart
    • Simple Root Detection: Implementation and verification
    • OWASP Top 10 For Flutter - M5: Insecure Communication for Flutter and Dart
    • OWASP Top 10 For Flutter – M4: Insufficient Input/Output Validation in Flutter
    • OWASP Top 10 For Flutter – M3: Insecure Authentication and Authorization in Flutter
    • OWASP Top 10 For Flutter – M2: Inadequate Supply Chain Security in Flutter
    • OWASP Top 10 For Flutter - M1: Mastering Credential Security in Flutter
    • Hook, Hack, Defend: Frida’s Impact on Mobile Security & How to Fight Back
    • Emulators in Gaming: Threats and Detections
    • Exclusive Research: Unlocking Reliable Crash Tracking with PLCrashReporter for iOS SDKs
    • 🚀A Developer’s Guide to Implement End-to-End Encryption in Mobile Apps 🛡️
    • How to Block Screenshots, Screen Recording, and Remote Access Tools in Android and iOS Apps
    • Flutter Security 101: Restricting Installs to Protect Your App from Unofficial Sources
    • How to test a RASP? OWASP MAS: RASP Techniques Not Implemented [MASWE-0103]
    • How to implement Secure Storage in Flutter?
    • User Authentication Risks Coverage in Flutter Mobile Apps | TALSEE
    • Fact about the origin of the Talsec name
    • React Native Secure Boilerplate 2024: Ignite with freeRASP
    • Flutter CTO Report 2024: Flutter App Security Trends
    • Mobile API Anti-abuse Protection with AppiCrypt®: A New Play Integrity and DeviceCheck Alternative
    • Hacking and protection of Mobile Apps and backend APIs | 2024 Talsec Threat Modeling Exercise
    • Detect system VPNs with freeRASP
    • Introducing Talsec’s advanced malware protection!
    • Fraud-Proofing an Android App: Choosing the Best Device ID for Promo Abuse Prevention
    • Enhancing Capacitor App Security with freeRASP: Your Shield Against Threats 🛡️
    • Safeguarding Your Data in React Native: Secure Storage Solutions
    • Secure Storage: What Flutter can do, what Flutter could do
    • 🔒 Flutter Plugin Attack: Mechanics and Prevention
    • Protecting Your API from App Impersonation: Token Hijacking Guide and Mitigation of JWT Theft
    • Build secure apps in React Native
    • How to Hack & Protect Flutter Apps — Simple and Actionable Guide (Pt. 1/3)
    • How to Hack & Protect Flutter Apps — OWASP MAS and RASP. (Pt. 2/3)
    • How to Hack & Protect Flutter Apps — Steal Firebase Auth token and attack the API. (Pt. 3/3)
    • freeRASP meets Cordova
    • Philosophizing security in a mobile-first world
    • 5 Things John Learned Fighting Hackers of His App — A must-read for PM’s and CISO’s
    • Missing Hero of Flutter World
Powered by GitBook
LogoLogo

Company

  • General Terms and Conditions

Stay Connected

  • LinkedIn
  • X
  • YouTube
On this page
  • The Stakes: Beyond Basic Authentication
  • Understanding user flow from login / logout
  • Security Checkpoints
  • Implementing a secure auth flow
  • 1. Setting Up Security Service
  • 2. Implementing Token Management
  • Security Vulnerabilities
  • Implementing Secure Token Storage
  • 3. Supabase Authentication Service
  • 4. User Interface Implementation
  • 5. App Initialization and Integration
  • 6. Securing Network Communications
  • Implementing Secure Network Layer
  • 7. Unit Testing
  • 8. Implementing Multi-Factor Authentication (MFA)
  • Conclusion

Was this helpful?

  1. articles

User Authentication Risks Coverage in Flutter Mobile Apps | TALSEE

Dive into our full guide as Himesh Panchal walks you through creating a robust and secure authentication flow!

PreviousHow to implement Secure Storage in Flutter?NextFact about the origin of the Talsec name

Last updated 6 months ago

Was this helpful?

Authentication vulnerabilities remain one of the most critical security concerns in mobile application development. When building Flutter applications, developers often overlook crucial security aspects while integrating third-party authentication providers.

The combined total of apps in the Apple App Store and Google Play Store has surpassed 6 million, but a startling have at least one security flaw, highlighting the widespread vulnerability in mobile app ecosystems.

The Stakes: Beyond Basic Authentication

Mobile authentication attacks have evolved beyond simple credential theft. Modern attack vectors target the entire authentication flow, from initial user input to session management. A compromised authentication system doesn't just expose user credentials - it potentially compromises your entire API surface area.

Consider this scenario: An attacker extracts an improperly stored refresh token from a jailbroken device. Even with perfect password security and MFA implementation, this single vulnerability allows indefinite API access through token refresh mechanisms.

While providers like Firebase, Supabase, and Auth0 implement encryption to safeguard user data. providers manage backend security, developers must ensure secure practices in their apps to eliminate vulnerabilities. The post will provide actionable insights on encryption, secure token storage, and robust communication strategies for Flutter apps.

Understanding user flow from login / logout

The authentication flow in a Flutter application represents a complex sequence of security-critical operations. Each step presents unique vulnerabilities that malicious actors can exploit. Let's analyse the complete flow, focusing on security implications at each stage.

Security Checkpoints

  1. Launch the App: The app initializes and prepares the login screen. At this stage, failing to sanitize inputs or enforce app integrity checks could expose the app to tampering.

  2. Input Validation: Users enter their credentials. Weak validation can allow injection attacks or malformed inputs.

  3. Initiate Login Request: The app sends credentials to the server. Without secure communication channels, credentials may be intercepted.

  4. Receive Authentication Response: A successful response contains tokens; insecure handling can lead to theft or misuse.

  5. Token Management: Tokens need secure storage and monitoring for expiration. Improper storage can lead to credential exposure.

  6. API Calls and Logout: Valid tokens allow API access, while the logout process ensures no lingering session data exists.

Implementing a secure auth flow

JWT (JSON Web Tokens) is a compact, URL-safe way to securely transmit information between two parties as a JSON object. It’s commonly used for authentication and information exchange. A JWT is composed of three parts:

  1. Header: Specifies the token type (JWT) and the signing algorithm (e.g., HS256).

  2. Payload: Contains the claims (data like user info or permissions). Claims can be public, private, or registered (e.g., iss, exp).

  3. Signature: Ensures the token’s integrity using the header, payload, and a secret or public/private key.

JWTs are signed, not encrypted, making them verifiable but not inherently confidential. They’re ideal for stateless authentication and are often used in APIs, enabling secure communication without server-side storage.

JSON Web Tokens form the backbone of modern authentication systems. Their structure requires careful handling and validation:

Before we dive into the implementation, let's set up our project with the necessary dependencies. Add these to your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  supabase_flutter: ^1.10.25  # For authentication
  flutter_secure_storage: ^9.0.0  # For secure token storage
  provider: ^6.1.1  # For state management
  freerasp: ^6.0.0  # For security checks

1. Setting Up Security Service

The Security Service acts as your application's first line of defense. It continuously monitors the device environment and enforces security policies. This service helps protect your app against:

  • Device Tampering: Detects rooted (Android) or jailbroken (iOS) devices

  • Development Tools: Identifies debugging attempts and emulator usage

  • Runtime Threats: Monitors for malicious hooks and code modifications

  • Brute Force Attacks: Implements rate limiting to prevent repeated login attempts

The service uses freeRASP for device integrity checks and maintains an in-memory store of failed login attempts. When security violations are detected, it notifies the UI layer to take appropriate action.

The security service monitors device integrity and manages rate limiting. Create lib/services/security_service.dart:

import 'package:flutter/foundation.dart';
import 'package:freerasp/freerasp.dart';

class SecurityService extends ChangeNotifier {
  bool _isJailbroken = false;
  bool _isEmulator = false;
  bool _isDebuggerAttached = false;
  bool _isHooked = false;
  bool _isAppTampered = false;
  
  final Map<String, int> _failedAttempts = {};
  final int _maxAttempts = 5;
  
  late TalsecConfig _config;
  late ThreatCallback _callback;

  SecurityService() {
    _initConfig();
    _initCallback();
    _initSecurity();
  }

  bool get isDeviceSecure => !_isJailbroken && 
                           !_isEmulator && 
                           !_isDebuggerAttached && 
                           !_isHooked && 
                           !_isAppTampered;

  void _initConfig() {
    _config = TalsecConfig(
      androidConfig: AndroidConfig(
        packageName: 'your.package.name',
        signingCertHashes: ['your_cert_hash'],
        supportedStores: ['com.android.vending'],
      ),
      iosConfig: IOSConfig(
        bundleIds: ['your.bundle.id'],
        teamId: 'YOUR_TEAM_ID',
      ),
      watcherMail: 'security@yourapp.com',
      isProd: !kDebugMode,
    );
  }

  // Rate limiting methods
  bool checkRateLimit(String identifier) {
    return _failedAttempts[identifier] ?? 0 < _maxAttempts;
  }

  void incrementFailedAttempt(String identifier) {
    _failedAttempts[identifier] = (_failedAttempts[identifier] ?? 0) + 1;
  }

  void resetFailedAttempts(String identifier) {
    _failedAttempts.remove(identifier);
  }
}

2. Implementing Token Management

  • Why Secure Token Storage Matters

    Authentication tokens are like digital keys to your application. Without proper lifecycle management, these keys could become a security liability. Let's understand why token management is crucial and how we've implemented it.

    • Understanding the Risks

      If an attacker obtains a token stored in plain text or one that never expires, they could potentially access a user's account indefinitely. Additionally, without proper refresh mechanisms, users might experience frequent, frustrating logouts.

    Here's how tokens are often stored insecurely using SharedPreferences:

class InsecureTokenStorage {
  static const _tokenKey = 'auth_token';
  
  Future<void> storeToken(String token) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_tokenKey, token);
  }

  Future<String?> getToken() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString(_tokenKey);
  }
}

Security Vulnerabilities

  1. Easy Extraction:

    • On rooted/jailbroken devices, attackers can directly read SharedPreferences files

    • Tokens are stored in plain text at /data/data/your.package.name/shared_prefs/your_prefs.xml

  2. Example Attack Scenario:

# On a rooted Android device
adb shell
su
cd /data/data/your.package.name/shared_prefs
cat your_prefs.xml
# Output might look like:
# <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
# <map>
#   <string name="auth_token">eyJhbGciOiJIUzI1NiIs...</string>
# </map>

Implementing Secure Token Storage

Now that we understand the risks, let's implement a secure solution using flutter_secure_storage

The Token Service handles the secure storage and lifecycle management of authentication tokens. It's designed to:

  • Secure Storage: Use platform-specific encryption (EncryptedSharedPreferences for Android, Keychain for iOS)

  • Auto-Refresh: Proactively refresh tokens before expiration to maintain session continuity

  • Clean Lifecycle: Properly handle token storage, updates, and deletion

  • Error Recovery: Implement fallback mechanisms for token refresh failures

This service ensures that sensitive authentication data is never exposed in plain text and maintains secure session state across app launches.

Create lib/services/token_service.dart for secure token storage:

import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'dart:async';

class TokenService {
  final _storage = const FlutterSecureStorage();
  Timer? _refreshTimer;
  Function(String)? onTokenRefreshNeeded;

  Future<void> storeTokens(Map<String, String> tokens) async {
    await _storage.write(
      key: 'access_token',
      value: tokens['access_token'],
      aOptions: _getAndroidOptions(),
      iOptions: _getIOSOptions(),
    );
    
    await _storage.write(
      key: 'refresh_token',
      value: tokens['refresh_token'],
      aOptions: _getAndroidOptions(),
      iOptions: _getIOSOptions(),
    );

    _scheduleTokenRefresh();
  }

  AndroidOptions _getAndroidOptions() => const AndroidOptions(
    encryptedSharedPreferences: true,
  );

  IOSOptions _getIOSOptions() => const IOSOptions(
    accessibility: KeychainAccessibility.first_unlock,
  );

  void _scheduleTokenRefresh() {
    _refreshTimer?.cancel();
    _refreshTimer = Timer(const Duration(minutes: 55), () {
      if (onTokenRefreshNeeded != null) {
        getRefreshToken().then((token) {
          if (token != null) onTokenRefreshNeeded!(token);
        });
      }
    });
  }
}

3. Supabase Authentication Service

This service integrates Supabase authentication with our security layers. It implements:

  • PKCE Flow: Uses Proof Key for Code Exchange for enhanced security

  • Session Management: Maintains and validates authentication state

  • Token Handling: Coordinates with TokenService for secure storage

  • Error Management: Provides structured error handling for auth operations

  • State Recovery: Implements session recovery after app restarts

The service acts as a bridge between Supabase's authentication system and our custom security implementations.

Create lib/services/supabase_auth_service.dart:

import 'package:flutter/foundation.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'token_service.dart';

class SupabaseAuthService extends ChangeNotifier {
  late final SupabaseClient _supabaseClient;
  final TokenService _tokenService = TokenService();
  bool _initialized = false;

  Future<void> initialize() async {
    await Supabase.initialize(
      url: 'YOUR_SUPABASE_URL',
      anonKey: 'YOUR_ANON_KEY',
      authOptions: const FlutterAuthClientOptions(
        authFlowType: AuthFlowType.pkce,
        autoRefreshToken: true,
      ),
    );
    
    _supabaseClient = Supabase.instance.client;
    _initialized = true;

    // Listen to auth state changes
    _supabaseClient.auth.onAuthStateChange.listen(_handleAuthStateChange);
    
    await _recoverSession();
    notifyListeners();
  }

  Future<AuthResponse> login({
    required String email,
    required String password,
  }) async {
    try {
      final response = await _supabaseClient.auth.signInWithPassword(
        email: email,
        password: password,
      );
      notifyListeners();
      return response;
    } catch (e) {
      throw _handleAuthException(e);
    }
  }
}

4. User Interface Implementation

Create lib/screens/login_screen.dart:

class LoginScreen extends StatefulWidget {
  @override
  _LoginScreenState createState() => _LoginScreenState();
}

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

  Future<void> _handleLogin() async {
    final securityService = Provider.of<SecurityService>(context, listen: false);
    final email = _emailController.text.trim();

    // Security checks
    if (!securityService.isDeviceSecure) {
      _showSecurityWarning();
      return;
    }

    // Rate limiting
    if (!securityService.checkRateLimit(email)) {
      _showRateLimitWarning();
      return;
    }

    if (_formKey.currentState!.validate()) {
      setState(() => _isLoading = true);
      try {
        final success = await context.read<AuthService>().login(
          email,
          _passwordController.text,
        );

        if (!success && mounted) {
          securityService.incrementFailedAttempt(email);
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('Login failed')),
          );
        } else {
          securityService.resetFailedAttempts(email);
        }
      } finally {
        if (mounted) setState(() => _isLoading = false);
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Secure Login')),
      body: Consumer<SecurityService>(
        builder: (context, security, _) => Column(
          children: [
            if (!security.isDeviceSecure)
              SecurityWarningBanner(),
            LoginForm(
              formKey: _formKey,
              emailController: _emailController,
              passwordController: _passwordController,
              isLoading: _isLoading,
              onLogin: _handleLogin,
            ),
          ],
        ),
      ),
    );
  }
}

5. App Initialization and Integration

The initialization phase is crucial as it sets up the security foundation for your entire application. This phase orchestrates the proper setup and interaction of all security components.

Update your lib/main.dart:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  final supabaseAuth = SupabaseAuthService();
  await supabaseAuth.initialize();
  
  final authService = AuthService(supabaseAuth: supabaseAuth);
  final securityService = SecurityService();
  
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider<SupabaseAuthService>(
          create: (_) => supabaseAuth,
        ),
        ChangeNotifierProvider<AuthService>(
          create: (_) => authService..init(),
        ),
        ChangeNotifierProvider<SecurityService>(
          create: (_) => securityService,
        ),
      ],
      child: const MyApp(),
    ),
  );
}

6. Securing Network Communications

While our project uses Supabase's built-in networking, lot of Flutter applications use Dio for HTTP communications. Let's explore how to implement secure networking with Dio.

Imagine sending a postcard versus a sealed letter. HTTP is like a postcard - anyone handling it can read its contents.

Implementing Secure Network Layer

class SecureApiService {
  late Dio _dio;
  final TokenService _tokenService;

  SecureApiService(this._tokenService) {
    _dio = Dio(BaseOptions(
      baseUrl: 'https://api.yourserver.com',
      validateStatus: (status) => status! < 500,
      connectTimeout: const Duration(seconds: 5),
      receiveTimeout: const Duration(seconds: 3),
    ));
    
    _configureSecurityFeatures();
  }

  void _configureSecurityFeatures() {
    // 1. HTTPS Enforcement & Certificate Pinning
    (_dio.httpClientAdapter as DefaultHttpClientAdapter)
        .onHttpClientCreate = (client) {
      SecurityContext context = SecurityContext(withTrustedRoots: true);
      context.setTrustedCertificatesBytes(yourCertBytes);
      
      // Force HTTPS only
      client.badCertificateCallback = 
          (X509Certificate cert, String host, int port) => false;
      
      return client;
    };

    // 2. Request/Response Security
    _dio.interceptors.add(InterceptorsWrapper(
      onRequest: (options, handler) async {
        // Add security headers
        options.headers['X-Security-Header'] = 'value';
        options.headers['X-Request-ID'] = _generateRequestId();
        
        // Encrypt sensitive data if needed
        if (options.data is Map && options.data['sensitive'] != null) {
          options.data['sensitive'] = await _encryptData(options.data['sensitive']);
        }
        
        return handler.next(options);
      },
      onResponse: (response, handler) async {
        // Validate response integrity
        if (!_validateResponseIntegrity(response)) {
          return handler.reject(
            DioError(
              requestOptions: response.requestOptions,
              error: 'Response integrity check failed',
            ),
          );
        }
        return handler.next(response);
      },
    ));

    // 3. Token Management
    _dio.interceptors.add(InterceptorsWrapper(
      onRequest: (options, handler) async {
        final token = await _tokenService.getAccessToken();
        if (token != null) {
          options.headers['Authorization'] = 'Bearer $token';
        }
        return handler.next(options);
      },
      onError: (error, handler) async {
        if (error.response?.statusCode == 401) {
          if (await _refreshToken()) {
            return handler.resolve(await _retryRequest(error.requestOptions));
          }
        }
        return handler.next(error);
      },
    ));
  }

  // Helper methods for security features
  String _generateRequestId() => DateTime.now().millisecondsSinceEpoch.toString();
  
  Future<String> _encryptData(String data) async {
    // Implement your encryption logic
    return data;
  }
  
  bool _validateResponseIntegrity(Response response) {
    // Implement response validation logic
    return true;
  }

  Future<bool> _refreshToken() async {
    try {
      // Implement token refresh logic
      return true;
    } catch (e) {
      return false;
    }
  }

  Future<Response<dynamic>> _retryRequest(RequestOptions requestOptions) async {
    final token = await _tokenService.getAccessToken();
    requestOptions.headers['Authorization'] = 'Bearer $token';
    return _dio.fetch(requestOptions);
  }

  // Handle security errors
  void _handleSecurityError(DioError error) {
    if (error.type == DioErrorType.badCertificate) {
      _logSecurityEvent('Invalid Certificate');
    }
    // Handle other security errors
  }

  void _logSecurityEvent(String event) {
    // Implement security logging
  }
}

7. Unit Testing

  • Authentication testing is crucial for ensuring your security measures work as intended. Our testing approach combines mock generation with comprehensive test scenarios to verify authentication flows, error handling, and security constraints.

  • Setting Up Authentication Tests

    To implement our tests, we need two key files:

    1. auth_test.dart: Contains our test scenarios and implementations

    2. auth_test.mocks.dart: Auto-generated mocks for simulating authentication services

First, ensure you have the required dependencies in your pubspec.yaml:

dev_dependencies:
  flutter_test:
    sdk: flutter
  mockito: ^5.4.4
  build_runner: ^2.4.8
  • Complete Test Implementation

    In our testing setup, we leverage Mockito's powerful code generation capabilities to create mock services. By adding the @GenerateMocks([SupabaseAuthService]) annotation to our test file, we tell Mockito which classes need to be mocked. When we run flutter pub run build_runner build, Mockito automatically generates auth_test.mocks.dart, which contains a sophisticated mock implementation of our SupabaseAuthService.

// auth_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:login_app/services/auth_service.dart';
import 'package:login_app/services/supabase_auth_service.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

import 'auth_test.mocks.dart';

@GenerateMocks([SupabaseAuthService])
void main() {
  group('Authentication Tests', () {
    late MockSupabaseAuthService mockSupabaseAuth;
    late AuthService authService;

    setUp(() {
      mockSupabaseAuth = MockSupabaseAuthService();
      authService = AuthService(supabaseAuth: mockSupabaseAuth);
      
      // Setup default mock behavior
      when(mockSupabaseAuth.isAuthenticated).thenReturn(false);
      when(mockSupabaseAuth.currentUser).thenReturn(null);
    });

    test('Initialize auth service', () async {
      // Arrange
      when(mockSupabaseAuth.initialize()).thenAnswer((_) async => {});

      // Act
      await authService.init();

      // Assert
      verify(mockSupabaseAuth.initialize()).called(1);
    });

    test('Sign in with valid credentials succeeds', () async {
      // Arrange
      final mockUser = User(
        id: 'mock_user_id',
        email: 'test@example.com',
        aud: 'authenticated',
        role: 'authenticated',
        createdAt: DateTime.now().toIso8601String(),
        appMetadata: {},
        userMetadata: {},
        phone: '',
      );

      final mockResponse = AuthResponse(
        session: Session(
          accessToken: 'mock_access_token',
          refreshToken: 'mock_refresh_token',
          tokenType: '',
          expiresIn: 3600,
          user: mockUser,
        ),
        user: mockUser,
      );

      when(mockSupabaseAuth.login(
        email: 'test@example.com',
        password: 'password123',
      )).thenAnswer((_) async => mockResponse);

      // Act
      final result = await authService.login('test@example.com', 'password123');

      // Assert
      expect(result, true);
      verify(mockSupabaseAuth.login(
        email: 'test@example.com',
        password: 'password123',
      )).called(1);
    });

    test('Sign in with invalid credentials fails', () async {
      // Arrange
      when(mockSupabaseAuth.login(
        email: 'wrong@example.com',
        password: 'wrongpass',
      )).thenThrow(Exception('Invalid credentials'));

      // Act
      final result = await authService.login('wrong@example.com', 'wrongpass');

      // Assert
      expect(result, false);
    });

    test('Sign up with valid data succeeds', () async {
      // Arrange
      final mockUser = User(
        id: 'new_user_id',
        email: 'new@example.com',
        aud: 'authenticated',
        role: 'authenticated',
        createdAt: DateTime.now().toIso8601String(),
        appMetadata: {},
        userMetadata: {},
        phone: '',
      );

      final mockResponse = AuthResponse(
        session: Session(
          accessToken: 'mock_access_token',
          refreshToken: 'mock_refresh_token',
          tokenType: '',
          expiresIn: 3600,
          user: mockUser,
        ),
        user: mockUser,
      );

      when(mockSupabaseAuth.signUp(
        email: 'new@example.com',
        password: 'newpass123',
      )).thenAnswer((_) async => mockResponse);

      // Act
      final result = await authService.signUp('new@example.com', 'newpass123');

      // Assert
      expect(result, true);
      verify(mockSupabaseAuth.signUp(
        email: 'new@example.com',
        password: 'newpass123',
      )).called(1);
    });

    test('Sign out succeeds', () async {
      // Arrange
      when(mockSupabaseAuth.logout())
          .thenAnswer((_) async => {});

      // Act
      await authService.logout();

      // Assert
      verify(mockSupabaseAuth.logout()).called(1);
    });

    test('Check authentication state', () {
      // Initial state
      expect(authService.isAuthenticated, false);

      // Simulate successful login
      when(mockSupabaseAuth.isAuthenticated).thenReturn(true);
      when(mockSupabaseAuth.currentUser).thenReturn(
        User(
          id: 'user_id',
          email: 'test@example.com',
          aud: 'authenticated',
          role: 'authenticated',
          createdAt: DateTime.now().toIso8601String(),
          appMetadata: {},
          userMetadata: {},
          phone: '',
        ),
      );
      
      // Verify authenticated state
      expect(authService.isAuthenticated, true);
    });

    test('Password reset request succeeds', () async {
      // Arrange
      when(mockSupabaseAuth.resetPassword('test@example.com'))
          .thenAnswer((_) async => {});

      // Act & Assert
      await expectLater(
        authService.resetPassword('test@example.com'),
        completes,
      );
      verify(mockSupabaseAuth.resetPassword('test@example.com')).called(1);
    });

    test('Password reset request fails', () async {
      // Arrange
      when(mockSupabaseAuth.resetPassword('test@example.com'))
          .thenThrow(Exception('Failed to reset password'));

      // Act & Assert
      expect(
        () => authService.resetPassword('test@example.com'),
        throwsException,
      );
    });
  });
}

Running the Tests

flutter test test/auth_test.dart

The combination of mock generation and comprehensive test scenarios provides confidence in our authentication system's reliability and security.

8. Implementing Multi-Factor Authentication (MFA)

Multi-Factor Authentication strengthens your application's security by requiring multiple forms of verification. Think of it as adding multiple locks to your front door – each additional layer makes unauthorized access significantly more difficult.

MFA relies on a combination of:

  • Knowledge factors (something you know) • Passwords • PIN codes • Security questions

  • Possession factors (something you have) • Mobile devices • Security tokens • Authentication apps

  • Inherence factors (something you are) • Fingerprints • Face recognition • Voice patterns

  • Popular MFA Methods

    • Time-based One-Time Passwords (TOTP)

      TOTP enhances security through authenticator apps that generate temporary codes using time-synchronized algorithms. These codes automatically expire after 30 seconds, providing a secure yet convenient authentication method. The time-sensitive nature ensures that intercepted codes quickly become useless, making it an effective choice for applications requiring strong security.

  • SMS and Email Verification

    • SMS and email verification offer familiar authentication experiences using existing communication channels. While simple to implement and widely accessible, these methods are considered less secure due to potential vulnerabilities like SIM swapping or email compromise. They remain popular for applications where user convenience takes priority over maximum security measures.

Conclusion

Building secure authentication in Flutter requires a careful balance between security and user experience. Throughout this guide, we've explored implementing a authentication system that protects user data without compromising usability.

While our implementation provides a solid foundation for secure authentication, remember that security is not a one-time implementation. Regular reviews and updates of your security measures are essential to maintain strong protection for your users.

The principles and patterns we've discussed serve as a starting point. Your specific application may require additional security measures depending on your use case, user base, and sensitivity of data.

Keep building secure applications, stay informed about emerging security threats, and always prioritise your users' data protection.

written by Himesh Panchal

75% of these apps
Cover

Himesh Panchal

I’m a passionate web and tech enthusiast who has been working with Flutter since its 1.0. I specialise in optimising mobile app CI/CD workflows and enjoy writing technical articles to share my knowledge with the developer community. When I’m not coding, you’ll likely find me hiking in the mountains and connecting with nature.

,

Twitter (X)
GitHub