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!

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 75% of these apps have at least one security flaw, highlighting the widespread vulnerability in mobile app ecosystems.

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.
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
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.
Input Validation: Users enter their credentials. Weak validation can allow injection attacks or malformed inputs.
Initiate Login Request: The app sends credentials to the server. Without secure communication channels, credentials may be intercepted.
Receive Authentication Response: A successful response contains tokens; insecure handling can lead to theft or misuse.
Token Management: Tokens need secure storage and monitoring for expiration. Improper storage can lead to credential exposure.
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:
Header: Specifies the token type (JWT) and the signing algorithm (e.g., HS256).
Payload: Contains the claims (data like user info or permissions). Claims can be public, private, or registered (e.g., iss, exp).
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: '[email protected]',
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
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
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:
auth_test.dart: Contains our test scenarios and implementations
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 runflutter pub run build_runner build
, Mockito automatically generatesauth_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: '[email protected]',
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: '[email protected]',
password: 'password123',
)).thenAnswer((_) async => mockResponse);
// Act
final result = await authService.login('[email protected]', 'password123');
// Assert
expect(result, true);
verify(mockSupabaseAuth.login(
email: '[email protected]',
password: 'password123',
)).called(1);
});
test('Sign in with invalid credentials fails', () async {
// Arrange
when(mockSupabaseAuth.login(
email: '[email protected]',
password: 'wrongpass',
)).thenThrow(Exception('Invalid credentials'));
// Act
final result = await authService.login('[email protected]', 'wrongpass');
// Assert
expect(result, false);
});
test('Sign up with valid data succeeds', () async {
// Arrange
final mockUser = User(
id: 'new_user_id',
email: '[email protected]',
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: '[email protected]',
password: 'newpass123',
)).thenAnswer((_) async => mockResponse);
// Act
final result = await authService.signUp('[email protected]', 'newpass123');
// Assert
expect(result, true);
verify(mockSupabaseAuth.signUp(
email: '[email protected]',
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: '[email protected]',
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('[email protected]'))
.thenAnswer((_) async => {});
// Act & Assert
await expectLater(
authService.resetPassword('[email protected]'),
completes,
);
verify(mockSupabaseAuth.resetPassword('[email protected]')).called(1);
});
test('Password reset request fails', () async {
// Arrange
when(mockSupabaseAuth.resetPassword('[email protected]'))
.thenThrow(Exception('Failed to reset password'));
// Act & Assert
expect(
() => authService.resetPassword('[email protected]'),
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
Last updated
Was this helpful?