Your Flutter app just got featured on TechCrunch. Congratulations! But wait — you're also trending on r/privacy for all the wrong reasons. A security researcher just demonstrated how they extracted user passwords from your app's local storage in under 5 minutes. Your Flutter for startups's reputation? Gone. User trust? Shattered. And those enterprise clients you've been courting? They're already asking uncomfortable questions about your security practices.
Look, I've been there. Not the TechCrunch part — I wish — but definitely the "oh crap, did we actually secure this thing?" moment when you realize your app handles sensitive data and you've been storing API keys in plain text. The mobile security landscape has evolved dramatically, and the old "security through obscurity" approach doesn't cut it anymore. With GDPR fines reaching hundreds of millions and users becoming increasingly privacy-conscious, Flutter security isn't just a nice-to-have — it's make-or-break for your app's success.
Here's the thing: most Flutter developers I know are brilliant at building beautiful UIs and crafting smooth animations, but when it comes to security? We're often winging it. We copy-paste code from Stack Overflow, hope for the best, and pray that nobody tries to reverse-engineer our apps. But attackers aren't hoping — they're actively looking for vulnerabilities, and mobile apps are juicy targets.
📚 What You Will Learn
This comprehensive guide covers everything from basic encryption to advanced certificate pinning. You'll learn how to implement secure storage, protect against the OWASP Mobile Top 10 vulnerabilities, handle authentication securely, and build a security-first mindset into your Flutter development workflow. By the end, you'll have a complete security toolkit and the confidence to build apps that protect user data like Fort Knox.
🔧 Prerequisites
You should have solid Flutter experience with state management comparison and be comfortable with Dart async programming. Basic understanding of HTTP/REST APIs is essential, and familiarity with cryptographic concepts helps but isn't required. We'll be using packages like flutter_secure_storage, crypto, and dio, so package management experience is a plus.
Flutter Security Fundamentals: Understanding the Threat Landscape
Before we dive into code, let's talk about what we're actually protecting against. The mobile threat landscape isn't just script kiddies anymore — we're dealing with sophisticated attacks, state-sponsored hackers, and automated vulnerability scanners that can analyze your app faster than you can say "release to production."
The OWASP Mobile Top 10: Your Security Checklist
The OWASP Mobile Top 10 is basically the security bible for mobile developers. I keep this list pinned above my monitor because it's that important. Here are the big ones that'll bite Flutter developers:
| Vulnerability | Flutter Risk | Real-World Impact |
|---|---|---|
| Insecure Data Storage | High | Passwords, tokens stored in plain text |
| Insecure Communication | Medium | API calls intercepted, MitM attacks |
| Insecure Authentication | High | Session hijacking, unauthorized access |
| Code Quality Issues | Medium | Buffer overflows, injection attacks |
| Reverse Engineering | High | API keys extracted, business logic exposed |
Flutter's Security Model: What's Built-In vs. What You Need to Add
Flutter gives us some security features out of the box, but honestly? It's not enough for production apps. The framework handles basic sandboxing and provides secure HTTP by default, but everything else — encryption, secure storage, certificate pinning — that's on us.
The good news is that Flutter's architecture actually makes implementing security easier than native development. We can write security code once and it works across iOS and Android. The bad news? Most developers don't realize what's missing until it's too late.
🔐 Security Reality Check
I've audited dozens of Flutter apps, and here's what I find 90% of the time: API keys hardcoded in source, sensitive data in SharedPreferences, no certificate pinning, and authentication tokens stored in plain text. Don't be that developer.
Building a Security-First Mindset
Security isn't something you bolt on at the end — it's a mindset you build into every line of code. When I'm writing Flutter apps now, I ask myself three questions for every feature: What sensitive data am I handling? How could this be attacked? What's the worst-case scenario if this gets compromised?
This shift in thinking has saved me countless hours of retrofitting security into existing apps. Trust me, it's way easier to build secure from the start than to secure something that's already built.
Secure Data Storage: Beyond SharedPreferences
Let's be honest — SharedPreferences is basically a text file with a fancy name. Storing anything sensitive there is like leaving your house key under the doormat. Sure, it's convenient, but anyone who knows where to look can find it. And on rooted/jailbroken devices? Game over.
Flutter Secure Storage: Your New Best Friend
The flutter_secure_storage top Flutter packages is what you should be using for anything remotely sensitive. It uses the Android Keystore and iOS Keychain under the hood, which means your data is encrypted with hardware-backed keys when available.
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class SecureStorageService {
static const _storage = FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
keyCipherAlgorithm: KeyCipherAlgorithm.RSA_ECB_OAEPwithSHA_256andMGF1Padding,
storageCipherAlgorithm: StorageCipherAlgorithm.AES_GCM_NoPadding,
),
iOptions: IOSOptions(
accessibility: KeychainAccessibility.first_unlock_this_device,
),
);
// Store sensitive data
static Future storeToken(String token) async {
await _storage.write(key: 'auth_token', value: token);
}
// Retrieve sensitive data
static Future getToken() async {
return await _storage.read(key: 'auth_token');
}
// Store user credentials (never do this in production without proper hashing!)
static Future storeCredentials(String username, String password) async {
// Hash the password first!
final hashedPassword = _hashPassword(password);
await _storage.write(key: 'username', value: username);
await _storage.write(key: 'password_hash', value: hashedPassword);
}
static String _hashPassword(String password) {
// Use proper password hashing like bcrypt or Argon2
// This is just for demonstration
return password; // DON'T DO THIS!
}
// Clear all stored data
static Future clearAll() async {
await _storage.deleteAll();
}
}
Encryption for Complex Data Structures
Sometimes you need to store more than just strings — user profiles, cached API responses, or complex objects. Here's where things get interesting. You'll want to serialize your data to JSON, encrypt it, then store it securely.
import 'dart:convert';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:encrypt/encrypt.dart';
class EncryptedStorageService {
static const _storage = FlutterSecureStorage();
static late final Encrypter _encrypter;
static late final IV _iv;
static Future initialize() async {
// Generate or retrieve encryption key
String? keyString = await _storage.read(key: 'encryption_key');
if (keyString == null) {
final key = Key.fromSecureRandom(32);
keyString = key.base64;
await _storage.write(key: 'encryption_key', value: keyString);
}
final key = Key.fromBase64(keyString);
_encrypter = Encrypter(AES(key));
_iv = IV.fromSecureRandom(16);
}
static Future storeObject(String key, T object) async {
try {
// Serialize object to JSON
final jsonString = jsonEncode(object);
// Encrypt the JSON string
final encrypted = _encrypter.encrypt(jsonString, iv: _iv);
// Store encrypted data with IV
final dataToStore = {
'data': encrypted.base64,
'iv': _iv.base64,
};
await _storage.write(key: key, value: jsonEncode(dataToStore));
} catch (e) {
throw StorageException('Failed to store object: $e');
}
}
static Future retrieveObject(String key, T Function(Map) fromJson) async {
try {
final storedData = await _storage.read(key: key);
if (storedData == null) return null;
final dataMap = jsonDecode(storedData) as Map;
final encryptedData = Encrypted.fromBase64(dataMap['data']);
final iv = IV.fromBase64(dataMap['iv']);
// Decrypt the data
final decryptedJson = _encrypter.decrypt(encryptedData, iv: iv);
final objectMap = jsonDecode(decryptedJson) as Map;
return fromJson(objectMap);
} catch (e) {
throw StorageException('Failed to retrieve object: $e');
}
}
}
class StorageException implements Exception {
final String message;
StorageException(this.message);
@override
String toString() => 'StorageException: $message';
}
When to Use Different Storage Methods
Not everything needs military-grade encryption. Here's my rule of thumb: if it would be embarrassing or damaging if it leaked, encrypt it. If it would destroy your business or get someone hurt, use secure storage with hardware backing.
For app preferences and non-sensitive cache data, SharedPreferences is fine. For authentication tokens, user credentials, or personal data, use flutter_secure_storage. For highly sensitive data like financial information or health records, consider additional encryption layers.
⚡ Performance Tip
Secure storage operations are slower than regular storage. Cache frequently accessed data in memory and only hit secure storage when necessary. I typically load secure data at app startup and keep it in a secure singleton service.
Encryption Implementation: Protecting Data in Transit and at Rest
Encryption is like a good bodyguard — you hope you never need it, but when you do, you're really glad it's there. In Flutter, we've got several layers where encryption matters: data at rest (stored on device), data in transit (network calls), and data in memory (while your app is running).
AES Encryption for Local Data
AES (Advanced Encryption Standard) is the gold standard for symmetric encryption. It's fast, secure, and supported everywhere. Here's how I implement it in Flutter apps that need to encrypt sensitive data before storing it locally:
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:encrypt/encrypt.dart';
class FlutterEncryptionService {
static const int _keyLength = 32; // 256 bits
static const int _ivLength = 16; // 128 bits
/// Generate a secure random key for encryption
static String generateSecureKey() {
final random = Random.secure();
final keyBytes = Uint8List(_keyLength);
for (int i = 0; i < _keyLength; i++) {
keyBytes[i] = random.nextInt(256);
}
return base64Encode(keyBytes);
}
/// Encrypt data using AES-256-GCM
static EncryptionResult encryptData(String plaintext, String keyBase64) {
try {
final key = Key.fromBase64(keyBase64);
final iv = IV.fromSecureRandom(_ivLength);
final encrypter = Encrypter(AES(key, mode: AESMode.gcm));
final encrypted = encrypter.encrypt(plaintext, iv: iv);
return EncryptionResult(
encryptedData: encrypted.base64,
iv: iv.base64,
success: true,
);
} catch (e) {
return EncryptionResult(
error: 'Encryption failed: $e',
success: false,
);
}
}
/// Decrypt data using AES-256-GCM
static DecryptionResult decryptData(String encryptedBase64, String ivBase64, String keyBase64) {
try {
final key = Key.fromBase64(keyBase64);
final iv = IV.fromBase64(ivBase64);
final encrypter = Encrypter(AES(key, mode: AESMode.gcm));
final encrypted = Encrypted.fromBase64(encryptedBase64);
final decrypted = encrypter.decrypt(encrypted, iv: iv);
return DecryptionResult(
decryptedData: decrypted,
success: true,
);
} catch (e) {
return DecryptionResult(
error: 'Decryption failed: $e',
success: false,
);
}
}
/// Hash passwords using PBKDF2
static String hashPassword(String password, String salt) {
final bytes = utf8.encode(password + salt);
final digest = sha256.convert(bytes);
return digest.toString();
}
/// Generate a random salt for password hashing
static String generateSalt() {
final random = Random.secure();
final saltBytes = Uint8List(16);
for (int i = 0; i < 16; i++) {
saltBytes[i] = random.nextInt(256);
}
return base64Encode(saltBytes);
}
}
class EncryptionResult {
final String? encryptedData;
final String? iv;
final String? error;
final bool success;
EncryptionResult({
this.encryptedData,
this.iv,
this.error,
required this.success,
});
}
class DecryptionResult {
final String? decryptedData;
final String? error;
final bool success;
DecryptionResult({
this.decryptedData,
this.error,
required this.success,
});
}
RSA Encryption for Key Exchange
Sometimes you need asymmetric encryption — like when you're exchanging keys with a server or implementing end-to-end encryption. RSA is perfect for this, though it's slower than AES and has size limitations.
import 'package:encrypt/encrypt.dart';
class RSAEncryptionService {
static late final RSAKeyParser _parser;
static late final Encrypter _encrypter;
/// Initialize RSA encryption with key pair
static void initialize(String publicKeyPem, String privateKeyPem) {
_parser = RSAKeyParser();
final publicKey = _parser.parse(publicKeyPem) as RSAPublicKey;
final privateKey = _parser.parse(privateKeyPem) as RSAPrivateKey;
_encrypter = Encrypter(RSA(
publicKey: publicKey,
privateKey: privateKey,
));
}
/// Encrypt data with public key (for sending to server)
static String encryptWithPublicKey(String plaintext) {
try {
final encrypted = _encrypter.encrypt(plaintext);
return encrypted.base64;
} catch (e) {
throw EncryptionException('RSA encryption failed: $e');
}
}
/// Decrypt data with private key (for receiving from server)
static String decryptWithPrivateKey(String encryptedBase64) {
try {
final encrypted = Encrypted.fromBase64(encryptedBase64);
return _encrypter.decrypt(encrypted);
} catch (e) {
throw EncryptionException('RSA decryption failed: $e');
}
}
/// Generate new RSA key pair (typically done on server)
static RSAKeyPair generateKeyPair() {
final keyPair = RSAKeyPair.generate();
return RSAKeyPair(
publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey,
);
}
}
class EncryptionException implements Exception {
final String message;
EncryptionException(this.message);
@override
String toString() => 'EncryptionException: $message';
}
Hybrid Encryption: Best of Both Worlds
In production apps, I often use hybrid encryption — RSA for key exchange and AES for actual data encryption. This gives you the security of asymmetric encryption with the Flutter performance optimization of symmetric encryption. It's what HTTPS does under the hood, and it's what you should consider for sensitive data flows.
The pattern is simple: generate a random AES key, encrypt your data with AES, encrypt the AES key with RSA, then send both the encrypted data and encrypted key. The recipient decrypts the AES key with their RSA private key, then uses that to decrypt the actual data.
🔒 Key Management Reality
Here's what nobody tells you about encryption: key management is harder than the encryption itself. Where do you store keys? How do you rotate them? What happens if they're compromised? Plan this from day one, not when you're already in production.
Certificate Pinning: Preventing Man-in-the-Middle Attacks
Certificate pinning is like having a bouncer at your API's door who actually checks IDs instead of just waving everyone through. Without it, anyone with a valid certificate authority can create fake certificates for your domain and intercept your app's traffic. Corporate firewalls do this all the time, and so do attackers.
Understanding the Certificate Pinning Problem
Here's the scary part: by default, your Flutter app trusts hundreds of certificate authorities. If any one of them gets compromised (and they do — remember DigiNotar?), attackers can create valid certificates for your API endpoints. Your app will happily connect to their fake server, thinking it's talking to your real API.
Certificate pinning solves this by hardcoding which certificates or public keys your app should trust. If the server presents anything else, your app refuses to connect. It's like having a secret handshake with your server.
Implementing Certificate Pinning with Dio
I use Dio for HTTP requests because it makes certificate pinning straightforward. Here's how I implement it in production apps:
import 'package:dio/dio.dart';
import 'package:dio_certificate_pinning/dio_certificate_pinning.dart';
import 'package:flutter/services.dart';
class SecureApiClient {
static late final Dio _dio;
static const String _baseUrl = 'https://api.yourapp.com';
// SHA-256 fingerprints of your server's certificates
static const List _certificateFingerprints = [
'AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99',
// Backup certificate fingerprint
'FF:EE:DD:CC:BB:AA:99:88:77:66:55:44:33:22:11:00:FF:EE:DD:CC:BB:AA:99:88:77:66:55:44:33:22:11:00',
];
static Future initialize() async {
_dio = Dio(BaseOptions(
baseUrl: _baseUrl,
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 30),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
));
// Add certificate pinning interceptor
_dio.interceptors.add(
CertificatePinningInterceptor(
allowedSHAFingerprints: _certificateFingerprints,
),
);
// Add logging in debug mode
if (kDebugMode) {
_dio.interceptors.add(LogInterceptor(
requestBody: true,
responseBody: true,
requestHeader: true,
responseHeader: true,
));
}
// Add authentication interceptor
_dio.interceptors.add(AuthInterceptor());
}
/// Make secure GET request
static Future> get(
String path, {
Map? queryParameters,
Options? options,
}) async {
try {
return await _dio.get(
path,
queryParameters: queryParameters,
options: options,
);
} on DioException catch (e) {
throw _handleDioError(e);
}
}
/// Make secure POST request
static Future> post(
String path, {
dynamic data,
Map? queryParameters,
Options? options,
}) async {
try {
return await _dio.post(
path,
data: data,
queryParameters: queryParameters,
options: options,
);
} on DioException catch (e) {
throw _handleDioError(e);
}
}
static ApiException _handleDioError(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return ApiException('Connection timeout. Please check your internet connection.');
case DioExceptionType.badCertificate:
return ApiException('Certificate validation failed. This could indicate a security threat.');
case DioExceptionType.connectionError:
return ApiException('Unable to connect to server. Please try again.');
case DioExceptionType.badResponse:
final statusCode = e.response?.statusCode;
if (statusCode == 401) {
return ApiException('Authentication failed. Please log in again.');
} else if (statusCode == 403) {
return ApiException('Access denied.');
} else if (statusCode! >= 500) {
return ApiException('Server error. Please try again later.');
}
return ApiException('Request failed with status $statusCode');
default:
return ApiException('An unexpected error occurred: ${e.message}');
}
}
}
class AuthInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
// Add authentication token to requests
final token = await SecureStorageService.getToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
// Handle token refresh on 401
if (err.response?.statusCode == 401) {
final refreshed = await _refreshToken();
if (refreshed) {
// Retry the original request
final response = await SecureApiClient._dio.request(
err.requestOptions.path,
data: err.requestOptions.data,
queryParameters: err.requestOptions.queryParameters,
options: Options(
method: err.requestOptions.method,
headers: err.requestOptions.headers,
),
);
handler.resolve(response);
return;
}
}
handler.next(err);
}
Future _refreshToken() async {
// Implement token refresh logic
return false;
}
}
class ApiException implements Exception {
final String message;
ApiException(this.message);
@override
String toString() => 'ApiException: $message';
}
Getting Certificate Fingerprints
You need your server's certificate fingerprints to implement pinning. Here's how I get them:
# Get certificate fingerprint using OpenSSL
echo | openssl s_client -servername api.yourapp.com -connect api.yourapp.com:443 2>/dev/null | openssl x509 -fingerprint -sha256 -noout
# Alternative: Get certificate info
openssl s_client -servername api.yourapp.com -connect api.yourapp.com:443 -showcerts
# For Let's Encrypt certificates, also get the intermediate certificate
echo | openssl s_client -servername api.yourapp.com -connect api.yourapp.com:443 -showcerts 2>/dev/null | openssl x509 -fingerprint -sha256 -noout
Handling Certificate Rotation
Here's where certificate pinning gets tricky: what happens when your certificates expire or you need to rotate them? If you only pin to one certificate and it changes, your app stops working. That's why I always pin to multiple certificates and implement a fallback strategy.
The best approach is to pin to the intermediate certificate or the root CA certificate rather than the leaf certificate. Leaf certificates change frequently, but intermediate certificates are more stable. Even better, pin to the public key rather than the entire certificate — public keys change less often than certificates.
🚨 Certificate Pinning Gotcha
I learned this the hard way: always test certificate pinning thoroughly before release. I once shipped an app with incorrect certificate fingerprints and had to push an emergency update. Not fun. Also, consider implementing certificate pinning bypass for debug builds to make development easier.
Authentication Security: JWT, OAuth, and Biometric Integration
Authentication is where most apps get pwned. I've seen everything from passwords sent in plain text to JWT tokens stored in localStorage (the web equivalent of SharedPreferences). The authentication flow is your app's front door — if it's not secure, nothing else matters.
Secure JWT Token Handling
JWT tokens are everywhere, but most developers handle them wrong. Here's the thing: JWTs aren't encrypted by default — they're just encoded. Anyone can decode them and see the payload. That's why you should never put sensitive data in JWT claims and always validate them server-side.
import 'dart:convert';
import 'package:jwt_decoder/jwt_decoder.dart';
class AuthenticationService {
static const String _accessTokenKey = 'access_token';
static const String _refreshTokenKey = 'refresh_token';
static const String _userDataKey = 'user_data';
/// Login with email and password
static Future login(String email, String password) async {
try {
// Hash password before sending (use proper hashing in production)
final hashedPassword = _hashPassword(password);
final response = await SecureApiClient.post('/auth/login', data: {
'email': email,
'password': hashedPassword,
});
if (response.statusCode == 200) {
final data = response.data as Map;
final accessToken = data['access_token'] as String;
final refreshToken = data['refresh_token'] as String;
final userData = data['user'] as Map;
// Validate JWT token
if (!_isValidJWT(accessToken)) {
return AuthResult(success: false, error: 'Invalid token received');
}
// Store tokens securely
await SecureStorageService.storeToken(accessToken);
await _storeRefreshToken(refreshToken);
await _storeUserData(userData);
return AuthResult(
success: true,
accessToken: accessToken,
user: User.fromJson(userData),
);
}
return AuthResult(success: false, error: 'Login failed');
} catch (e) {
return AuthResult(success: false, error: 'Authentication error: $e');
}
}
/// Refresh access token using refresh token
static Future refreshAccessToken() async {
try {
final refreshToken = await _getRefreshToken();
if (refreshToken == null || _isTokenExpired(refreshToken)) {
return false;
}
final response = await SecureApiClient.post('/auth/refresh', data: {
'refresh_token': refreshToken,
});
if (response.statusCode == 200) {
final data = response.data as Map;
final newAccessToken = data['access_token'] as String;
await SecureStorageService.storeToken(newAccessToken);
return true;
}
return false;
} catch (e) {
return false;
}
}
/// Check if current user is authenticated
static Future isAuthenticated() async {
final token = await SecureStorageService.getToken();
if (token == null) return false;
if (_isTokenExpired(token)) {
// Try to refresh token
final refreshed = await refreshAccessToken();
return refreshed;
}
return true;
}
/// Get current user data
static Future getCurrentUser() async {
try {
final userData = await SecureStorageService.retrieveObject(
_userDataKey,
(json) => User.fromJson(json),
);
return userData;
} catch (e) {
return null;
}
}
/// Logout and clear all stored data
static Future logout() async {
try {
// Call logout endpoint to invalidate tokens server-side
await SecureApiClient.post('/auth/logout');
} catch (e) {
// Continue with local logout even if server call fails
}
// Clear all stored authentication data
await SecureStorageService.clearAll();
}
// Private helper methods
static bool _isValidJWT(String token) {
try {
JwtDecoder.decode(token);
return true;
} catch (e) {
return false;
}
}
static bool _isTokenExpired(String token) {
try {
return JwtDecoder.isExpired(token);
} catch (e) {
return true;
}
}
static Future _storeRefreshToken(String token) async {
await SecureStorageService._storage.write(key: _refreshTokenKey, value: token);
}
static Future _getRefreshToken() async {
return await SecureStorageService._storage.read(key: _refreshTokenKey);
}
static Future _storeUserData(Map userData) async {
await SecureStorageService.storeObject(_userDataKey, userData);
}
static String _hashPassword(String password) {
// Use proper password hashing like bcrypt or Argon2
// This is just for demonstration
final bytes = utf8.encode(password);
final digest = sha256.convert(bytes);
return digest.toString();
}
}
class AuthResult {
final bool success;
final String? accessToken;
final User? user;
final String? error;
AuthResult({
required this.success,
this.accessToken,
this.user,
this.error,
});
}
class User {
final String id;
final String email;
final String name;
User({required this.id, required this.email, required this.name});
factory User.fromJson(Map json) {
return User(
id: json['id'] as String,
email: json['email'] as String,
name: json['name'] as String,
);
}
Map toJson() {
return {
'id': id,
'email': email,
'name': name,
};
}
}
Biometric Authentication Integration
Biometric authentication is becoming table stakes for mobile apps. Users expect it, and it's actually more secure than passwords when implemented correctly. Here's how I integrate it with local_auth:
import 'package:local_auth/local_auth.dart';
import 'package:local_auth_android/local_auth_android.dart';
import 'package:local_auth_ios/local_auth_ios.dart';
class BiometricAuthService {
static final LocalAuthentication _localAuth = LocalAuthentication();
static const String _biometricEnabledKey = 'biometric_enabled';
/// Check if biometric authentication is available
static Future checkBiometricCapability() async {
try {
final isAvailable = await _localAuth.isDeviceSupported();
if (!isAvailable) {
return BiometricCapability.notSupported;
}
final canCheckBiometrics = await _localAuth.canCheckBiometrics;
if (!canCheckBiometrics) {
return BiometricCapability.notEnrolled;
}
final availableBiometrics = await _localAuth.getAvailableBiometrics();
if (availableBiometrics.isEmpty) {
return BiometricCapability.notEnrolled;
}
return BiometricCapability.available;
} catch (e) {
return BiometricCapability.error;
}
}
/// Enable biometric authentication for the app
static Future enableBiometricAuth() async {
try {
final capability = await checkBiometricCapability();
if (capability != BiometricCapability.available) {
return false;
}
final authenticated = await _authenticateWithBiometrics(
reason: 'Enable biometric authentication for secure app access',
);
if (authenticated) {
await SecureStorageService._storage.write(
key: _biometricEnabledKey,
value: 'true',
);
return true;
}
return false;
} catch (e) {
return false;
}
}
/// Disable biometric authentication
static Future disableBiometricAuth() async {
await SecureStorageService._storage.delete(key: _biometricEnabledKey);
}
/// Check if biometric auth is enabled for this app
static Future isBiometricEnabled() async {
final enabled = await SecureStorageService._storage.read(key: _biometricEnabledKey);
return enabled == 'true';
}
/// Authenticate using biometrics
static Future authenticateWithBiometrics({
String? reason,
}) async {
try {
final isEnabled = await isBiometricEnabled();
if (!isEnabled) {
return BiometricAuthResult(
success: false,
error: 'Biometric authentication is not enabled',
);
}
final authenticated = await _authenticateWithBiometrics(
reason: reason ?? 'Verify your identity to access the app',
);
return BiometricAuthResult(success: authenticated);
} catch (e) {
return BiometricAuthResult(
success: false,
error: 'Biometric authentication failed: $e',
);
}
}
/// Quick login using biometrics
static Future biometricLogin() async {
try {
final biometricResult = await authenticateWithBiometrics(
reason: 'Use your fingerprint or face to sign in',
);
if (!biometricResult.success) {
return AuthResult(
success: false,
error: biometricResult.error ?? 'Biometric authentication failed',
);
}
// Check if we have stored credentials
final isAuthenticated = await AuthenticationService.isAuthenticated();
if (isAuthenticated) {
final user = await AuthenticationService.getCurrentUser();
return AuthResult(
success: true,
user: user,
);
}
return AuthResult(
success: false,
error: 'No stored credentials found',
);
} catch (e) {
return AuthResult(
success: false,
error: 'Biometric login failed: $e',
);
}
}
// Private helper method
static Future _authenticateWithBiometrics({required String reason}) async {
return await _localAuth.authenticate(
localizedReason: reason,
authMessages: const [
AndroidAuthMessages(
signInTitle: 'Biometric Authentication',
cancelButton: 'Cancel',
biometricHint: 'Touch the fingerprint sensor',
biometricNotRecognized: 'Fingerprint not recognized. Try again.',
biometricSuccess: 'Fingerprint recognized',
deviceCredentialsRequiredTitle: 'Device credentials required',
deviceCredentialsSetupDescription: 'Please set up device credentials',
goToSettingsButton: 'Go to Settings',
goToSettingsDescription: 'Set up fingerprint on your device',
),
IOSAuthMessages(
cancelButton: 'Cancel',
goToSettingsButton: 'Go to Settings',
goToSettingsDescription: 'Set up Face ID or Touch ID',
lockOut: 'Biometric authentication is disabled. Please lock and unlock your screen to enable it.',
),
],
options: const AuthenticationOptions(
biometricOnly: true,
stickyAuth: true,
),
);
}
}
enum BiometricCapability {
available,
notSupported,
notEnrolled,
error,
}
class BiometricAuthResult {
final bool success;
final String? error;
BiometricAuthResult({required this.success, this.error});
}
OAuth 2.0 and Social Login Security
Social login seems simple until you realize how many ways it can go wrong. The biggest mistake I see is developers storing OAuth tokens insecurely or not validating them properly. Here's my approach to secure OAuth implementation:
Always use PKCE (Proof Key for Code Exchange) for OAuth flows — it prevents code interception attacks. Never store client secrets in your mobile app — they're not actually secret once they're compiled into your app. And always validate OAuth tokens server-side before trusting them.
🔐 Authentication Security Checklist
- ✅ Use secure storage for tokens
- ✅ Implement token refresh logic
- ✅ Validate JWTs properly
- ✅ Enable biometric authentication
- ✅ Use PKCE for OAuth flows
- ✅ Implement logout on server-side
- ✅ Handle authentication state properly
Network Security: HTTPS, API Security, and Request Validation
Network security is where your app meets the real world — and the real world is full of people trying to intercept, modify, or hijack your data. I've seen apps that do everything else right but fail spectacularly because they trust network requests blindly or don't validate responses properly.
HTTPS Everywhere and Certificate Validation
This should be obvious in 2026, but I still see apps making HTTP requests for sensitive data. HTTPS isn't optional anymore — it's the bare minimum. But just using HTTPS isn't enough; you need to validate certificates properly and handle edge cases.
import 'package:dio/dio.dart';
import 'dart:io';
class NetworkSecurityService {
static late final Dio _secureClient;
static void initialize() {
_secureClient = Dio(BaseOptions(
baseUrl: 'https://api.yourapp.com',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 30),
headers: {
'User-Agent': 'YourApp/1.0.0 (Flutter)',
'Accept': 'application/json',
'Content-Type': 'application/json',
},
));
// Add security interceptors
_secureClient.interceptors.add(SecurityInterceptor());
_secureClient.interceptors.add(RequestValidationInterceptor());
_secureClient.interceptors.add(ResponseValidationInterceptor());
// Configure certificate validation
(_secureClient.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () {
final client = HttpClient();
// Strict certificate validation
client.badCertificateCallback = (cert, host, port) {
// Never return true in production!
// This would disable certificate validation
return false;
};
// Set security context
client.connectionTimeout = const Duration(seconds: 10);
client.idleTimeout = const Duration(seconds: 30);
return client;
};
}
/// Make a secure API request with full validation
static Future> secureRequest(
String method,
String path, {
dynamic data,
Map? queryParameters,
Map? headers,
Duration? timeout,
}) async {
try {
final options = Options(
method: method,
headers: headers,
sendTimeout: timeout ?? const Duration(seconds: 30),
receiveTimeout: timeout ?? const Duration(seconds: 30),
);
final response = await _secureClient.request(
path,
data: data,
queryParameters: queryParameters,
options: options,
);
return SecureResponse(
success: true,
data: response.data,
statusCode: response.statusCode ?? 0,
headers: response.headers.map,
);
} on DioException catch (e) {
return SecureResponse(
success: false,
error: _handleNetworkError(e),
statusCode: e.response?.statusCode ?? 0,
);
} catch (e) {
return SecureResponse(
success: false,
error: 'Unexpected error: $e',
statusCode: 0,
);
}
}
static NetworkError _handleNetworkError(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
return NetworkError(
type: NetworkErrorType.timeout,
message: 'Connection timeout',
isRetryable: true,
);
case DioExceptionType.badCertificate:
return NetworkError(
type: NetworkErrorType.security,
message: 'Certificate validation failed',
isRetryable: false,
);
case DioExceptionType.connectionError:
return NetworkError(
type: NetworkErrorType.connection,
message: 'Network connection failed',
isRetryable: true,
);
case DioExceptionType.badResponse:
final statusCode = e.response?.statusCode ?? 0;
if (statusCode >= 500) {
return NetworkError(
type: NetworkErrorType.server,
message: 'Server error ($statusCode)',
isRetryable: true,
);
} else if (statusCode == 401) {
return NetworkError(
type: NetworkErrorType.authentication,
message: 'Authentication failed',
isRetryable: false,
);
} else {
return NetworkError(
type: NetworkErrorType.client,
message: 'Request failed ($statusCode)',
isRetryable: false,
);
}
default:
return NetworkError(
type: NetworkErrorType.unknown,
message: 'Unknown network error',
isRetryable: false,
);
}
}
}
class SecurityInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// Add security headers
options.headers['X-Request-ID'] = _generateRequestId();
options.headers['X-App-Version'] = '1.0.0';
options.headers['X-Platform'] = Platform.isAndroid ? 'android' : 'ios';
// Validate request URL
if (!options.uri.isScheme('https')) {
handler.reject(
DioException(
requestOptions: options,
error: 'HTTPS required for all requests',
type: DioExceptionType.badRequest,
),
);
return;
}
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
// Validate response headers
final contentType = response.headers.value('content-type');
if (contentType != null && !contentType.startsWith('application/json')) {
// Log potential security issue
print('WARNING: Unexpected content type: $contentType');
}
// Check for security headers
final securityHeaders = [
'strict-transport-security',
'x-frame-options',
'x-content-type-options',
];
for (final header in securityHeaders) {
if (!response.headers.map.containsKey(header)) {
print('WARNING: Missing security header: $header');
}
}
handler.next(response);
}
String _generateRequestId() {
return DateTime.now().millisecondsSinceEpoch.toString();
}
}
class RequestValidationInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// Validate request data
if (options.data != null) {
if (!_isValidRequestData(options.data)) {
handler.reject(
DioException(
requestOptions: options,
error: 'Invalid request data format',
type: DioExceptionType.badRequest,
),
);
return;
}
}
// Rate limiting check (implement based on your needs)
if (_isRateLimited(options.path)) {
handler.reject(
DioException(
requestOptions: options,
error: 'Rate limit exceeded',
type: DioExceptionType.badRequest,
),
);
return;
}
handler.next(options);
}
bool _isValidRequestData(dynamic data) {
// Implement request validation logic
// Check for SQL injection patterns, XSS attempts, etc.
if (data is String) {
return !_containsSuspiciousPatterns(data);
}
return true;
}
bool _containsSuspiciousPatterns(String data) {
final suspiciousPatterns = [
RegExp(r'
API Security and Request Signing
For sensitive API operations, basic authentication isn't enough. You need request signing to ensure requests haven't been tampered with and to prevent replay attacks. Here's how I implement HMAC-based request signing:
import 'dart:convert';
import 'package:crypto/crypto.dart';
class RequestSigningService {
static const String _apiKeyHeader = 'X-API-Key';
static const String _signatureHeader = 'X-Signature';
static const String _timestampHeader = 'X-Timestamp';
static const String _nonceHeader = 'X-Nonce';
/// Sign an API request with HMAC-SHA256
static Map signRequest({
required String method,
required String path,
required String apiKey,
required String secretKey,
Map? queryParams,
dynamic body,
}) {
final timestamp = DateTime.now().millisecondsSinceEpoch.toString();
final nonce = _generateNonce();
// Create canonical request string
final canonicalRequest = _createCanonicalRequest(
method: method,
path: path,
queryParams: queryParams,
body: body,
timestamp: timestamp,
nonce: nonce,
);
// Generate HMAC signature
final signature = _generateSignature(canonicalRequest, secretKey);
return {
_apiKeyHeader: apiKey,
_signatureHeader: signature,
_timestampHeader: timestamp,
_nonceHeader: nonce,
};
}
/// Verify request signature (for testing)
static bool verifySignature({
required String method,
required String path,
required String signature,
required String secretKey,
required String timestamp,
required String nonce,
Map? queryParams,
dynamic body,
}) {
final canonicalRequest = _createCanonicalRequest(
method: method,
path: path,
queryParams: queryParams,
body: body,
timestamp: timestamp,
nonce: nonce,
);
final expectedSignature = _generateSignature(canonicalRequest, secretKey);
return signature == expectedSignature;
}
static String _createCanonicalRequest({
required String method,
required String path,
required String timestamp,
required String nonce,
Map? queryParams,
dynamic body,
}) {
final parts = [
method.toUpperCase(),
path,
_canonicalizeQueryParams(queryParams),
_canonicalizeBody(body),
timestamp,
nonce,
];
return parts.join('\n');
}
static String _canonicalizeQueryParams(Map? params) {
if (params == null || params.isEmpty) return '';
final sortedKeys = params.keys.toList()..sort();
final canonicalParams = sortedKeys
.map((key) => '$key=${Uri.encodeComponent(params[key].toString())}')
.join('&');
return canonicalParams;
}
static String _canonicalizeBody(dynamic body) {
if (body == null) return '';
if (body is String) {
return body;
} else if (body is Map || body is List) {
return jsonEncode(body);
}
return body.toString();
}
static String _generateSignature(String canonicalRequest, String secretKey) {
final key = utf8.encode(secretKey);
final bytes = utf8.encode(canonicalRequest);
final hmac = Hmac(sha256, key);
final digest = hmac.convert(bytes);
return base64Encode(digest.bytes);
}
static String _generateNonce() {
final random = Random.secure();
final bytes = List.generate(16, (_) => random.nextInt(256));
return base64Encode(bytes);
}
}
class SignedApiClient {
static late final Dio _dio;
static late final String _apiKey;
static late final String _secretKey;
static void initialize({
required String baseUrl,
required String apiKey,
required String secretKey,
}) {
_apiKey = apiKey;
_secretKey = secretKey;
_dio = Dio(BaseOptions(
baseUrl: baseUrl,
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 30),
));
_dio.interceptors.add(RequestSigningInterceptor());
}
static Future> signedRequest(
String method,
String path, {
dynamic data,
Map? queryParameters,
}) async {
return await _dio
Frequently Asked Questions
What is Flutter app security and why is it important?
Flutter app security refers to the practices and techniques used to protect mobile applications built with the Flutter framework from security threats and data breaches. It encompasses data encryption, secure storage, authentication, network security, and protection against common vulnerabilities. Flutter app security is crucial because mobile apps handle sensitive user data like personal information, payment details, and location data that must be protected from unauthorized access. Without proper security measures, apps are vulnerable to attacks that can compromise user privacy and damage business reputation.
How does Flutter security compare to native Android and iOS security?
Flutter security relies on the underlying platform security features of Android and iOS while adding its own
layer of protection through Dart code compilation and framework-specific security practices. Flutter apps
compile to native ARM code, which provides similar performance and security characteristics as native apps.
However, Flutter developers must implement security measures differently, using packages like
flutter_secure_storage instead of platform-specific APIs. The main advantage is consistent
security implementation across both platforms, while the challenge is staying updated with Flutter-specific
security best practices.
What is the OWASP Mobile Top 10 and how does it apply to Flutter apps?
The OWASP Mobile Top 10 is a list of the most critical security risks for mobile applications, including improper platform usage, insecure data storage, insecure communication, and insufficient cryptography. Flutter apps are susceptible to all these vulnerabilities despite being cross-platform. Developers must address each OWASP risk by implementing secure coding practices, using proper encryption, securing network communications, and validating user inputs. Flutter-specific considerations include securing local databases like SQLite, implementing proper certificate pinning, and using secure storage solutions.
How do you implement Flutter encryption for sensitive data?
Flutter encryption can be implemented using the crypto package for basic hashing and the
encrypt package for advanced encryption algorithms like AES. For local data encryption, use
flutter_secure_storage which automatically encrypts data using platform-specific secure storage
mechanisms. For custom encryption needs, implement AES encryption with proper key management and
initialization vectors. Always use established cryptographic libraries rather than creating custom encryption
solutions, and ensure encryption keys are stored securely using platform keychain services.
What is Flutter secure storage and when should you use it?
Flutter secure storage refers to encrypted storage solutions that protect sensitive data on the device,
primarily implemented through the flutter_secure_storage package. It uses Android Keystore and
iOS Keychain to encrypt data with hardware-backed security when available. You should use secure storage for
authentication tokens, API keys, user credentials, personal identification numbers, and any sensitive user
data that must persist between app sessions. Regular SharedPreferences or local databases should never be used
for sensitive data as they store information in plain text.
Can Flutter apps implement certificate pinning for network security?
Yes, Flutter apps can implement certificate pinning using packages like dio_certificate_pinning
or by configuring custom HTTP clients with pinned certificates. Certificate pinning prevents man-in-the-middle
attacks by validating that the server's SSL certificate matches a predefined certificate or public key. This
is implemented by overriding the default certificate validation in HTTP clients and comparing the server
certificate against stored certificate hashes. Certificate pinning is essential for apps handling sensitive
data and should be combined with other security measures like proper SSL/TLS configuration.
Which Flutter packages are essential for app security?
Essential Flutter security packages include flutter_secure_storage for encrypted local storage,
crypto for hashing
and basic cryptographic functions, and encrypt for advanced
encryption algorithms. For network security, use dio with certificate pinning capabilities or
http with custom security configurations. The local_auth package provides biometric
authentication, while permission_handler manages app permissions securely. Additional packages
like flutter_keychain and secure_application provide platform-specific security
enhancements for comprehensive protection.
How can you protect Flutter apps from reverse engineering?
Flutter apps can be protected from reverse engineering through code obfuscation using the
--obfuscate flag during release builds, which scrambles class and function names. Enable
split-debug-info to separate debugging information from the release binary. Implement runtime application
self-protection (RASP) techniques to detect debugging and tampering attempts. Use string encryption for
sensitive constants and API endpoints, and consider third-party solutions like ProGuard for Android builds.
However, remember that client-side protection has limitations, so critical security logic should always be
implemented on the server side.