Skip to main content

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 &#x27;package:flutter_secure_http/flutter_secure_http.dart&#x27;;

final client = SecureHttpClient();
client.addPinnedCertificate(
  host: &#x27;api.example.com&#x27;,
  certificateFingerprint: &#x27;sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=&#x27;,
);

final response = await client.get(&#x27;https://api.example.com/data&#x27;);
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 &#x27;package:flutter_secure_storage/flutter_secure_storage.dart&#x27;;

final storage = FlutterSecureStorage();

// Write sensitive data
await storage.write(key: &#x27;api_token&#x27;, value: &#x27;abc123xyz&#x27;);

// Read sensitive data
String? token = await storage.read(key: &#x27;api_token&#x27;);
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.
TextFormField(
  validator: (value) {
    if (value == null || value.isEmpty) {
      return 'Field is required';
    }
    if (value.length < 5) {
      return 'Must be at least 5 characters';
    }
    return null;
  },
)
Use TextFormField with a validator function to enforce input rules.
Regex Validation Validates input using regular expressions for complex patterns.
TextFormField(
  validator: (value) {
    final emailRegex = RegExp(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$');
    if (!emailRegex.hasMatch(value ?? '')) {
      return 'Invalid email format';
    }
    return null;
  },
)
Use RegExp to validate email or other structured inputs.
Custom Validation Enables custom logic for input validation beyond basic checks.
TextFormField(
  validator: (value) {
    if (value == null || value.isEmpty) return 'Required';
    if (value.contains('admin')) {
      return 'Admin access not allowed';
    }
    return null;
  },
)
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 &#x27;package:flutter_dotenv/flutter_dotenv.dart&#x27;;

final String apiUrl = DotEnv().env[&#x27;API_URL&#x27;] ?? &#x27;https://default-api.com&#x27;;
final String secretKey = DotEnv().env[&#x27;SECRET_KEY&#x27;] ?? &#x27;default-key&#x27;;
final bool debugMode = DotEnv().env[&#x27;DEBUG_MODE&#x27;] == &#x27;true&#x27;;

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

Example: Enabling Obfuscation in pubspec.yaml

flutter:
  uses-material-design: true
  obfuscation: true
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: true in the pubspec.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
// BAD: Hardcoded in source
String apiKey = "1234567890abcdef";
Environment Variables Using system or app-level environment variables
// GOOD: Using env vars
String apiKey = Platform.environment["API_KEY"];
Secure Storage Using secure storage solutions like Flutter Secure Storage
// GOOD: Using secure storage
import "package:flutter_secure_storage/flutter_secure_storage.dart";

final storage = FlutterSecureStorage();
await storage.write(key: "secret_key", value: "my_secret");

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.** { *; }

Best Practices for ProGuard in Flutter

  • Use -keep rules for custom classes that must not be obfuscated.
  • Ensure @JavascriptInterface methods are preserved if using WebView.
  • Test thoroughly after applying ProGuard rules to avoid runtime errors.

Was this article helpful?