Stop Committing Your API Keys! Here's How to Actually Secure Them in Flutter (Part 2)
FlutterPulsefinal options = error.requestOptions;
options.headers['Authorization'] = 'Bearer $newToken';
final response = await _dio.fetch(options);
return handler.resolve(response);
} catch (e) {
// Refresh failed, clear storage and force login
await _secureStorage.clearAll();
return handler.next(error);
}
}
}
return handler.next(error);
},
),
);
}
Future<String> _refreshToken(String refreshToken) async {
final response = await _dio.post(
'/auth/refresh',
data: {'refreshToken': refreshToken},
);
return response.data['token'];
}
// Example API methods
Future<Map<String, dynamic>> getUser(String userId) async {
final response = await _dio.get('/users/$userId');
return response.data;
}
Future<List<dynamic>> fetchPosts() async {
final response = await _dio.get('/posts');
return response.data;
}
Future<void> createPost(Map<String, dynamic> postData) async {
await _dio.post('/posts', data: postData);
}
}
// Provider for API client
final apiClientProvider = Provider<ApiClient>((ref) {
final apiKey = ref.watch(apiKeyProvider);
final baseUrl = ref.watch(apiBaseUrlProvider);
final secureStorage = ref.watch(secureStorageProvider);
return ApiClient(
baseUrl: baseUrl,
apiKey: apiKey,
secureStorage: secureStorage,
);
});
Testing Your Security Setup
Here's how to verify everything is working:
1. Test Different Environments
// lib/screens/debug_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../config/secrets_provider.dart';
class DebugScreen extends ConsumerWidget {
const DebugScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final apiKey = ref.watch(apiKeyProvider);
final baseUrl = ref.watch(apiBaseUrlProvider);
final isConfigured = ref.watch(secretsConfiguredProvider);
return Scaffold(
appBar: AppBar(title: const Text('Debug Info')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Secrets Configured: $isConfigured'),
const SizedBox(height: 16),
Text('API Base URL: $baseUrl'),
const SizedBox(height: 16),
Text('API Key: ${_maskSecret(apiKey)}'),
const SizedBox(height: 16),
Text('Platform: ${Theme.of(context).platform}'),
],
),
),
);
}
String _maskSecret(String secret) {
if (secret.length <= 8) return '****';
return '${secret.substring(0, 4)}...${secret.substring(secret.length - 4)}';
}
}
2. Test Secure Storage
// Test saving and retrieving from secure storage
Future<void> testSecureStorage(SecureStorageService storage) async {
// Save a test value
await storage.writeSecure('test_key', 'test_value');
// Retrieve it
final value = await storage.readSecure('test_key');
assert(value == 'test_value', 'Secure storage test failed!');
// Clean up
await storage.deleteSecure('test_key');
print('✅ Secure storage test passed!');
}
3. Verify No Secrets in Binary
After building, you can check if your secrets are exposed:
# For Android APK
unzip -p build/app/outputs/flutter-apk/app-release.apk | strings | grep -i "your_api_key"
# If nothing is found, you're good! If it shows up, your secrets are exposed.
Common Pitfalls and How to Avoid Them
Mistake #1: Committing Config Files
Problem: You accidentally commit config/prod.json
Solution:
# If you already committed it
git rm --cached config/prod.json
git commit -m "Remove secrets from repo"
# Then make sure .gitignore is correct
echo "config/*.json" >> .gitignore
But wait! Even after removing, the secrets are still in Git history. You need to purge them:
# Use BFG Repo Cleaner
java -jar bfg.jar --delete-files config/prod.json
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push --force
Then immediately rotate all exposed secrets!
Mistake #2: Logging Secrets
Problem: You're debugging and accidentally log secrets
// ❌ NEVER DO THIS
print('API Key: ${AppSecrets.apiKey}');
debugPrint('Token: $userToken');
Solution: Create a safe logger
class SafeLogger {
static void log(String message) {
// Only log in debug mode
assert(() {
debugPrint(message);
return true;
}());
}
static void logSecret(String secretName) {
assert(() {
debugPrint('$secretName: [REDACTED]');
return true;
}());
}
}Mistake #3: Hardcoding Secrets in Comments
// TODO: Replace with actual API key: sk_live_abc123xyz
// ❌ Still exposed!
Scanners will find these. Never put secrets in comments.
Mistake #4: Using Predictable Secret Names
// ❌ Bad
const String adminPassword = "admin123";
// ✅ Good
// Use a secrets manager and reference it
final adminPassword = ref.watch(secureConfigProvider).adminPassword;
Mistake #5: Not Rotating Secrets
If secrets are exposed, rotate them immediately:
- Generate new API keys
- Update your config files (but NOT in Git!)
- Update secrets in CI/CD
- Rebuild and redeploy
Best Practices Checklist
Before shipping your app, check these boxes:
Configuration: ☑ Config files are in .gitignore ☑ Example config files are provided ☑ No secrets in source code ☑ No secrets in comments ☑ Environment-specific configs exist
Security: ☑ Using --dart-define or --dart-define-from-file ☑ Obfuscation enabled for production builds ☑ ProGuard/R8 enabled on Android ☑ Bitcode enabled on iOS (if possible) ☑ HTTPS-only (no cleartext traffic) ☑ Runtime secrets use flutter_secure_storage ☑ Code is obfuscated (--obfuscate flag)
Android: ☑ MinSDK is at least 18 ☑ Network security config prevents HTTP ☑ ProGuard rules are set up ☑ Release builds are signed
iOS: ☑ Keychain entitlements configured ☑ App Transport Security enforced ☑ Bitcode enabled (if no conflicting dependencies)
Testing: ☑ Tested all environments (dev, staging, prod) ☑ Verified secrets aren't in binary ☑ Secure storage works on real devices ☑ Tested on both Android and iOS
CI/CD: ☑ Secrets stored in CI/CD platform (not in code) ☑ Build scripts inject secrets at build time ☑ Artifacts don't contain plain secrets
Advanced: Using a Secrets Management Service
For enterprise apps or when you need centralized secret management, consider using a service like:
- AWS Secrets Manager
- Google Secret Manager
- HashiCorp Vault
- Azure Key Vault
Here's a quick example with AWS Secrets Manager:
dependencies:
aws_secretsmanager_api: ^1.0.0
import 'package:aws_secretsmanager_api/secretsmanager-2017-10-17.dart';
class CloudSecretsService {
final SecretsManager _secretsManager;
CloudSecretsService()
: _secretsManager = SecretsManager(
region: 'us-east-1',
credentials: AwsClientCredentials(
accessKey: 'YOUR_ACCESS_KEY',
secretKey: 'YOUR_SECRET_KEY',
),
);
Future<String> getSecret(String secretName) async {
final response = await _secretsManager.getSecretValue(
secretId: secretName,
);
return response.secretString ?? '';
}
}
// Use it
final apiKey = await cloudSecrets.getSecret('prod/api/key');
Note: You still need to secure the cloud service credentials!
Wrapping Up: The Complete Security Picture
Here's what we've covered:
Layer 1: Keep secrets out of code using --dart-defineLayer 2: Obfuscate compiled secrets with envied package Layer 3: Use native secure storage for runtime secrets Layer 4: Implement OS-specific security features Layer 5: Proper CI/CD secret management
Remember:
- Never commit secrets to version control
- Always use HTTPS in production
- Regularly rotate API keys and tokens
- Test security on real devices
- Audit your code for accidental leaks
Security is not a one-time thing — it's an ongoing process. But with the tools and techniques I've shown you, you're now way ahead of 90% of Flutter developers out there.
Related Content
- Implementing OAuth2 authentication flow in Flutter with Riverpod
- Building a secure REST API client with Dio and interceptors
- Certificate pinning in Flutter for enhanced API security
- Biometric authentication implementation guide
- Secure data encryption at rest in Flutter apps
- Handling sensitive user data according to GDPR and privacy laws
- Penetration testing your Flutter app before release
- Managing secrets across multiple build flavors
- Implementing secure deep linking in Flutter
- Creating a security-first authentication system with Firebase
Got questions about securing your Flutter app? Drop them in the comments! And if this helped you, don't forget to share it with your fellow developers. Stay safe out there! 🔒
#Flutter #FlutterSecurity #APIKeys #Riverpod #FlutterDev #MobileSecurity #SecureStorage #EnvironmentVariables #DartProgramming #AppSecurity #FlutterTutorial #BestPractices #SecretManagement #iOSDevelopment #AndroidDevelopment #CodeSecurity #DevOps #CICD #FlutterBeginner #SecureCoding