DEV Community

Cover image for Flutter: Best Practices for API Key Security 
Harsh Bangari Rawat
Harsh Bangari Rawat

Posted on

Flutter: Best Practices for API Key Security 

Securing API keys in Flutter is crucial to prevent unauthorized access to your app's functionalities. Here are some common methods with increasing security:

1. Least Secure (Not Recommended): Hardcoding API Keys

This involves storing the API key directly in a Dart file. While simple, it exposes the key to anyone with access to your code.

// This is a VULNERABLE approach! Never use it in production.

import 'package:http/http.dart' as http;

final String apiUrl = 'https://api.sample.com/data';
final String apiKey = 'YOUR_ACTUAL_API_KEY'; 

Future<http.Response> fetchData() async {
  final url = Uri.parse('$apiUrl?key=$apiKey');
  final response = await http.get(url);
  return response;
}
Enter fullscreen mode Exit fullscreen mode

Important: Never hardcode API keys in your Flutter code, as it exposes them to anyone with access to your codebase. Here's why it's not recommended:

Version Control Exposure: If you commit the code containing the API key to a version control system (e.g., Git), anyone with access to the repository can see it.
App Vulnerability: Hackers can potentially decompile your app and extract the API key, compromising your app's security.

2. Using --dart-define Flag

Pass the API key at compile time using the --dart-define flag. This keeps the key out of your codebase but requires remembering the flag during development and deployment.

The --dart-define flag in Flutter allows you to define compile-time constants that you can access in your code. This can be useful for storing environment-specific configurations, such as API keys, without hardcoding them directly in your source code. Here's an example of how to use it:

  • Define the Flag:
flutter run --dart-define=API_KEY=YOUR_API_KEY
Enter fullscreen mode Exit fullscreen mode
  • Access the Constant in Your Code:
import 'package:flutter/foundation.dart';

void main() {

  final apiKey = String.fromEnvironment('API_KEY');
  // Use the apiKey variable for your API calls

}
Enter fullscreen mode Exit fullscreen mode

Here we use String.fromEnvironment('API_KEY') to retrieve the value of the constant defined with the --dart-define flag.

Limitations:

  • The --dart-define flag defines constants only during compilation. You cannot dynamically change them at runtime.
  • It's less secure than other methods as someone can potentially see the flag arguments in your terminal history.

3. Using a .env File with ENVied Package

This method is more secure. Here's how it works:

  • Install Dependencies:
flutter pub add envied
flutter pub add --dev envied_generator
flutter pub add --dev build_runner
Enter fullscreen mode Exit fullscreen mode
  • Create the .env File:

Create a file named .env at the root of your Flutter project. This file will store your environment variables in the format KEY=VALUE. Here's an example:

API_KEY=your_actual_api_key (Replace with your actual API key)
BASE_URL=https://api.sample.com
Enter fullscreen mode Exit fullscreen mode

Important: Never commit the .env file to version control (e.g., Git). Add it to your .gitignore file to prevent accidental exposure.

  • Create the env.dart File:

Create a new Dart file named env.dart in your project's lib directory. This file will define a class to access the environment variables.

import 'package:envied/envied.dart';
part 'env.g.dart';

@Envied(path: '.env') // Specify the path to your .env file
abstract class Env {
  @EnviedField(varName: 'API_KEY')
  static final String apiKey = _Env.apiKey; // Use obfuscate: true for extra security (optional)

  @EnviedField(varName: 'BASE_URL')
  static final String baseUrl = _Env.baseUrl;
}
Enter fullscreen mode Exit fullscreen mode
  • Generate Code (Run Once):

Run the following command in your terminal to generate the necessary code for accessing environment variables:

flutter pub run build_runner build
Enter fullscreen mode Exit fullscreen mode

This will create a new file named env.g.dart next to your env.dart file.

  • Access Environment Variables:

Now you can access your environment variables defined in the .env file using the Env class. Import env.dart in your code where you need to use the API key or base URL:

import 'package:samples/env.dart'; // Replace with your project path

void main() async {
  final url = Uri.parse('$Env.baseUrl/data');
  final response = await http.get(url);
  // ... process the response using your API key if needed
}
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Security: .env files are excluded from version control, keeping your API keys safe.
  • Flexibility: You can easily change environment variables (e.g., dev vs. production) without modifying your codebase.
  • Maintainability: Keeps sensitive information separate from your application code.

Remember, this is a recommended approach for securing API keys. Consider using obfuscate: true in the @EnviedField annotation for an extra layer of protection (though it's not a foolproof method).

import 'package:envied/envied.dart';
part 'env.g.dart';

@Envied(path: '.env')
abstract class Env {
  @EnviedField(varName: 'API_KEY', obfuscate: true)
  static final String apiKey = _Env.apiKey;

  @EnviedField(varName: 'BASE_URL')
  static final String baseUrl = _Env.baseUrl;
}
Enter fullscreen mode Exit fullscreen mode

4. Using Firebase Functions

Implement a backend layer with Firebase Functions to manage API calls. Store keys securely in Firebase project environment variables and access them within the function

Here's how to leverage Firebase Functions to securely manage API keys in your Flutter application:

  • Firebase Project Setup:

Ensure you have a Firebase project set up and connected to your Flutter app.

  • Enable Firebase Secrets Manager:

In the Firebase console, navigate to "Project Settings" -> "Your apps" and select your Flutter app.
Under "Secrets Manager," enable the service.

  • Set Up Your Firebase Function:

In your Firebase project directory, create a new function using the Firebase CLI:

firebase functions:create function fetchUserData
Enter fullscreen mode Exit fullscreen mode
  • Write the Function Code (index.js):
const functions = require('firebase-functions');
const admin = require('firebase-admin');

admin.initializeApp();

exports.fetchUserData = functions.https.onCall((data, context) => {
  // Retrieve API key from Firebase Secrets Manager
  const apiKey = admin.secretManager().secretVersion('api-key-secret/versions/latest').accessSync();

  // Use the API key to fetch data (replace with your actual API call)
  const url = `https://api.sample.com/data?key=${apiKey}`;
  const response = await fetch(url);
  const userData = await response.json();

  return userData;
});
Enter fullscreen mode Exit fullscreen mode

Explanation:

We import necessary libraries: firebase-functions and firebase-admin.
We initialize the Firebase Admin SDK.
The fetchUserData function is triggered on call from your Flutter app.
It retrieves the API key from a Firebase Secret Manager named api-key-secret.
The key is accessed securely using accessSync() within a protected environment.
Replace the example API call (fetch) with your actual API interaction using the retrieved key.
The function returns the fetched user data.

  • Create the Firebase Secret:

Go to "Secrets Manager" in the Firebase console.
Create a new secret named your-api-key-secret.
Add a version with your actual API key as the value.

  • Call the Function from Flutter:
import 'package:cloud_functions/cloud_functions.dart';

Future<dynamic> getUserData() async {
  final functions = FirebaseFunctions.instance;
  final callable = functions.httpsCallable('fetchUserData');
  final result = await callable();
  return result.data;
}
Enter fullscreen mode Exit fullscreen mode

Conclusion:

This guide explored various approaches to securing API keys in your Flutter application. We learned that:

  • Hardcoding API keys is highly discouraged due to exposure in your codebase.
  • The --dart-define flag offers some protection but has limitations.
  • Using a .env file with the ENVied package provides a more secure solution by storing keys outside your code.
  • Firebase Functions are the most secure option, keeping keys entirely out of the app and leveraging Firebase's secret management features. Remember, choose the method that best suits your security needs and project complexity. Prioritize keeping API keys confidential and avoid version control exposure.

Security is an Ongoing Process

Securing your Flutter app goes beyond API keys. Consider additional measures like user authentication, data encryption, and secure communication protocols. Stay updated on best practices as security threats evolve.

Thank you for reading!🧑🏻‍💻

Top comments (0)