Implementing Security Best Practices
1. Using HTTPS for Network Requests
| Property | Description | Default | Notes |
|---|---|---|---|
| HttpClient | Class for making HTTP requests in Flutter | - | Supports HTTPS by default |
| Base URL | Root endpoint for API calls | - | Must use https:// |
| Certificate Pinning | Verifies server certificate against known values | - | Requires additional libraries like flutter_secure_http |
Example: Making an HTTPS Request
import 'dart:io';
import 'dart:convert';
Future<dynamic> fetchUserData(String userId) async {
final client = HttpClient();
client.connectionTimeout = const Duration(seconds: 10);
final request = await client.getUrl(Uri.parse("https://api.example.com/users/$userId"));
final response = await request.close();
final data = await response.transform(utf8.decoder).join();
return jsonDecode(data);
}
Note: All network requests in Flutter must use
https:// to ensure data encryption and prevent man-in-the-middle attacks.
Warning: Never use
http:// in production apps. Even in development, prefer HTTPS to simulate real-world conditions.
HTTPS Security Best Practices
- Always use
https://URLs for API endpoints - Implement certificate pinning in production apps
- Set connection timeouts to prevent hanging requests
- Validate server certificates before sending sensitive data
2. Implementing Certificate Pinning
Certificate pinning is a security technique that ensures your app only accepts connections from specific certificates or public keys, mitigating man-in-the-middle (MITM) attacks.
| Property | Description | Example |
|---|---|---|
| Certificate | The expected certificate fingerprint or public key. | sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= |
| Host | The domain or host for which the certificate is pinned. | api.example.com |
| Pinning Method | How the certificate is validated (e.g., SHA-256 fingerprint). | sha256 |
Example: Using flutter_secure_http for Certificate Pinning
import 'package:flutter_secure_http/flutter_secure_http.dart';
final client = SecureHttpClient();
client.addPinnedCertificate(
host: 'api.example.com',
certificateFingerprint: 'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
);
final response = await client.get('https://api.example.com/data');
Note: Certificate pinning should be used in conjunction with other security measures like secure network configuration and regular certificate updates.
Warning: Incorrectly configured certificate pinning can break your app’s network connectivity. Always test in a staging environment before deploying.
3. Storing Sensitive Data Securely
| Storage Method | Description | Security Level | Notes |
|---|---|---|---|
| SharedPreferences | Key-value storage for small amounts of data | Limited | Not encrypted by default; use with caution for sensitive data |
| EncryptedSharedPreferences | Secure key-value storage using Android Keystore or iOS Keychain | Good | Available in AndroidX Security library; recommended for sensitive data |
| SQLite Database | Local database for structured data | Limited | Requires manual encryption for sensitive fields; not secure by default |
| Secure Storage Plugin | Flutter plugin for secure local storage (e.g., flutter_secure_storage) | Good | Uses native secure storage APIs (Android Keystore, iOS Keychain) |
Example: Using flutter_secure_storage
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
final storage = FlutterSecureStorage();
// Write sensitive data
await storage.write(key: 'api_token', value: 'abc123xyz');
// Read sensitive data
String? token = await storage.read(key: 'api_token');
Note: Never store sensitive data in plain text or shared preferences without encryption.
Warning: Avoid storing credentials or tokens in memory longer than necessary.
| Feature | Support | Details |
|---|---|---|
| Encryption | Good | Uses platform-specific secure storage (e.g., Android Keystore, iOS Keychain) |
| Data Persistence | Good | Data persists across app sessions and device reboots |
| Cross-Platform | Modern | Works on both Android and iOS with consistent APIs |
| Performance | Good | Fast read/write operations with minimal overhead |
4. Validating User Input
| Validation Type | Description | Implementation | Notes |
|---|---|---|---|
| Form Validation | Ensures that user input conforms to expected formats and constraints. | |
Use TextFormField with a validator function to enforce input rules. |
| Regex Validation | Validates input using regular expressions for complex patterns. | |
Use RegExp to validate email or other structured inputs. |
| Custom Validation | Enables custom logic for input validation beyond basic checks. | |
Custom logic can include blacklists, business rules, or context-specific checks. |
Example: Input Validation in a Login Form
class LoginForm extends StatefulWidget {
@override
_LoginFormState createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _emailController,
decoration: InputDecoration(labelText: 'Email'),
validator: (value) {
if (value == null || value.isEmpty) return 'Email is required';
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
return 'Invalid email';
}
return null;
},
),
TextFormField(
controller: _passwordController,
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) return 'Password is required';
if (value.length < 8) return 'Password must be at least 8 characters';
return null;
},
),
],
),
);
}
}
Note: Client-side validation is essential for UX but should never be the only defense. Server-side validation must always be performed to prevent malicious input.
5. Implementing Authentication Flows
| Flow Type | Description | Key Components | Notes |
|---|---|---|---|
| OAuth 2.0 | Authorization framework for access delegation | oauth2 package, flutter_web_auth |
Supports authorization code flow with PKCE |
| OpenID Connect | Identity layer on top of OAuth 2.0 | openid_connect, flutter_appauth |
Provides ID tokens for user identity verification |
| Firebase Auth | Backend-as-a-Service authentication | firebase_auth, google_sign_in |
Supports email/password, phone, and social login |
| Custom JWT | Token-based authentication with custom claims | jwt, dio |
Requires secure token storage (e.g., flutter_secure_storage) |
Example: Firebase Auth Flow
// Initialize Firebase
await Firebase.initializeApp();
// Sign in with email and password
final user = await FirebaseAuth.instance
.signInWithEmailAndPassword(email: 'user@example.com', password: 'password123');
// Get user info
final uid = user.user!.uid;
Example: OAuth 2.0 with Flutter Web Auth
// Configure OAuth provider (e.g., Google)
final result = await FlutterWebAuth.authenticate(
url: 'https://accounts.google.com/o/oauth2/v2/auth?client_id=...&redirect_uri=flutter_web_auth_42://',
callbackUrlScheme: 'flutter_web_auth_42',
);
// Parse the returned token
final token = Uri.parse(result).queryParameters['access_token'];
Note: Always validate tokens server-side and never store sensitive credentials in plain text.
Warning: Avoid hardcoding client secrets or tokens in the app. Use secure storage and backend validation.
6. Using Environment Variables
| Variable | Type | Description | Usage |
|---|---|---|---|
| API_URL | string |
Base URL for API endpoints | Used in HTTP clients to make requests |
| SECRET_KEY | string |
Encryption key for local storage | Used to encrypt sensitive data before storing |
| DEBUG_MODE | boolean |
Flag to enable debug logging | Controls verbosity of logs in development |
Example: Reading Environment Variables in Flutter
import 'package:flutter_dotenv/flutter_dotenv.dart';
final String apiUrl = DotEnv().env['API_URL'] ?? 'https://default-api.com';
final String secretKey = DotEnv().env['SECRET_KEY'] ?? 'default-key';
final bool debugMode = DotEnv().env['DEBUG_MODE'] == 'true';
void main() {
runApp(MyApp(
apiUrl: apiUrl,
secretKey: secretKey,
debugMode: debugMode,
));
}
Note: Ensure that environment files are not committed to version control.
Warning: Never hardcode sensitive values like API keys or secrets in your source code.
7. Obfuscating Dart Code
Obfuscation in Flutter helps protect your Dart code from reverse engineering by renaming classes, methods, and variables to less meaningful names. This process makes it harder for attackers to understand the logic of your application.
| Feature | Description | Notes |
|---|---|---|
| obfuscation | true or false |
Enables or disables obfuscation during release builds |
| split-debug-info | path/to/debug/info |
Specifies directory to store debug information for obfuscated builds |
Note: Obfuscation is only applied during release builds. It does not affect debug or profile builds.
Warning: While obfuscation adds a layer of protection, it does not guarantee complete security. It is recommended to combine obfuscation with other security practices such as server-side validation and secure API communication.
Key Points About Obfuscation
- Obfuscation renames identifiers to obscure their meaning.
- It is enabled via
obfuscation: truein thepubspec.yaml. - Debug information can be stored separately using
split-debug-info. - Obfuscation is only effective in release builds.
8. Avoiding Hardcoded Secrets
| Approach | Description | Implementation |
|---|---|---|
| Hardcoded API Keys | Storing sensitive keys directly in source code | |
| Environment Variables | Using system or app-level environment variables | |
| Secure Storage | Using secure storage solutions like Flutter Secure Storage | |
Example: Secure Configuration Loading
import "package:flutter_secure_storage/flutter_secure_storage.dart";
class ConfigManager {
static const String API_KEY = "api_key";
final FlutterSecureStorage _storage = const FlutterSecureStorage();
Future<String> getApiKey() async {
final key = await _storage.read(key: API_KEY);
if (key == null) {
throw Exception("API key not found");
}
return key;
}
Future<void> setApiKey(String key) async {
await _storage.write(key: API_KEY, value: key);
}
}
Note: Never commit secrets to version control. Use tools like
.gitignore or secret management services.
Warning: Hardcoded secrets in production apps can be extracted by reverse engineering or inspection tools.
9. Implementing Biometric Authentication
| Property | Description | Values | Notes |
|---|---|---|---|
| localAuthentication | Plugin for biometric authentication | flutter_localizations |
Used for local authentication in Flutter apps |
| biometricType | Supported biometric types | fingerprint, face, iris |
Available on Android and iOS devices |
| authPrompt | Authentication prompt message | string |
Customizable user-facing message |
Example: Requesting Biometric Authentication
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:local_auth/local_auth.dart';
final LocalAuthentication auth = LocalAuthentication();
bool canCheckBiometrics = await auth.canCheckBiometrics;
List<BiometricType> availableBiometrics = await auth.getAvailableBiometrics();
bool authenticated = await auth.authenticate(
localizedReason: 'Please authenticate to proceed',
options: const AuthenticationOptions(
biometricOnly: true,
stickyAuth: true,
),
);
Note: Ensure that the app has proper permissions in AndroidManifest.xml and Info.plist for biometric access.
Warning: Biometric authentication should be used in conjunction with other security measures to ensure robust app security.
10. Using ProGuard Rules
Note: ProGuard is primarily used in Android builds. For Flutter apps targeting Android, you can configure ProGuard rules to obfuscate and optimize your code.
| Rule | Description | Usage |
|---|---|---|
| -keep class com.yourcompany.yourapp.** { *; } | Prevents obfuscation of your application's classes | Use to keep your main app classes intact |
| -keepclassmembers class * { @android.webkit.JavascriptInterface <methods>; } | Prevents removal of methods annotated with @JavascriptInterface | Essential for WebView integration |
| -keepnames class * { public <init>(android.content.Context, android.util.AttributeSet); } | Preserves class names for constructors | Useful when reflection is involved |
| -keep public class * extends android.app.Activity | Keeps all Activity classes from being obfuscated | Ensures proper lifecycle handling |
Example: ProGuard Configuration
# Keep your main application classes
-keep class com.yourcompany.yourapp.** { *; }
# Prevent removal of JavaScript interface methods
-keepclassmembers class * { @android.webkit.JavascriptInterface <methods>; }
# Keep all Activity classes
-keep public class * extends android.app.Activity
# Prevent obfuscation of specific libraries
-keep class com.google.gson.** { *; }