Close Menu
    DevStackTipsDevStackTips
    • Home
    • News & Updates
      1. Tech & Work
      2. View All

      Error’d: You Talkin’ to Me?

      September 20, 2025

      The Psychology Of Trust In AI: A Guide To Measuring And Designing For User Confidence

      September 20, 2025

      This week in AI updates: OpenAI Codex updates, Claude integration in Xcode 26, and more (September 19, 2025)

      September 20, 2025

      Report: The major factors driving employee disengagement in 2025

      September 20, 2025

      DistroWatch Weekly, Issue 1140

      September 21, 2025

      Distribution Release: DietPi 9.17

      September 21, 2025

      Development Release: Zorin OS 18 Beta

      September 19, 2025

      Distribution Release: IPFire 2.29 Core 197

      September 19, 2025
    • Development
      1. Algorithms & Data Structures
      2. Artificial Intelligence
      3. Back-End Development
      4. Databases
      5. Front-End Development
      6. Libraries & Frameworks
      7. Machine Learning
      8. Security
      9. Software Engineering
      10. Tools & IDEs
      11. Web Design
      12. Web Development
      13. Web Security
      14. Programming Languages
        • PHP
        • JavaScript
      Featured

      @ts-ignore is almost always the worst option

      September 22, 2025
      Recent

      @ts-ignore is almost always the worst option

      September 22, 2025

      MutativeJS v1.3.0 is out with massive performance gains

      September 22, 2025

      Student Performance Prediction System using Python Machine Learning (ML)

      September 21, 2025
    • Operating Systems
      1. Windows
      2. Linux
      3. macOS
      Featured

      DistroWatch Weekly, Issue 1140

      September 21, 2025
      Recent

      DistroWatch Weekly, Issue 1140

      September 21, 2025

      Distribution Release: DietPi 9.17

      September 21, 2025

      Hyprland Made Easy: Preconfigured Beautiful Distros

      September 20, 2025
    • Learning Resources
      • Books
      • Cheatsheets
      • Tutorials & Guides
    Home»Development»How to Secure Mobile APIs in Flutter

    How to Secure Mobile APIs in Flutter

    May 6, 2025

    As mobile applications continue to evolve in functionality and scope, securing the APIs that power these apps has become more critical than ever.

    In the context of Flutter, a framework that enables cross-platform development, understanding how to secure mobile APIs is essential – not only for maintaining user trust but also for safeguarding sensitive business data.

    In this article, we’ll explore common API vulnerabilities in mobile applications, particularly Flutter apps, and outline practical strategies to mitigate these risks.

    Table of Contents

    • Why API Security Matters in Mobile Apps

    • Project Setup Example:

    • Common Vulnerabilities in Flutter Apps

    • Example: Secure API Call in Flutter

    • Best Practices for Securing APIs in Flutter Apps

    • Security Checklist for Flutter Developers

    • Additional Considerations

    • Conclusion

    • References

    Securing API keys in a Flutter application is essential to prevent unauthorized access to sensitive resources. API keys are often used for authentication with external services – but if they’re exposed, they can lead to security vulnerabilities.

    In this guide, we will discuss how to securely store and manage API keys using Firebase Remote Config, Flutter Secure Storage, AES encryption, and device-specific identifiers.

    There are several ways to manage API keys securely, including:

    • CI/CD Solutions: Services like Codemagic, CircleCI, and GitHub Actions allow you to store API keys as environment variables to keep them out of your codebase.

    • Backend Storage: Keeping API keys on a backend server and fetching them dynamically is another secure approach.

    • Keystore & Keychain: On Android and iOS, API keys can be securely stored using the device’s built-in keystore mechanisms.

    • Encrypted Storage: Using encrypted local storage solutions to save API keys on the device.

    Why API Security Matters in Mobile Apps

    APIs serve as the bridge between mobile applications and backend services. While they enable dynamic experiences, such as fetching user data, processing payments, and managing real-time content, they also become a major attack vector if left unsecured.

    Mobile applications, unlike web apps, are distributed in compiled form (for example, APKs). These can be decompiled to reveal logic, endpoints, and sometimes even secrets like API keys.

    Attackers may reverse engineer APKs, intercept traffic using proxy tools like Burp Suite, or abuse API endpoints via emulators or scripts. This can lead to data breaches, unauthorized data manipulation, or service disruption.

    Publicly exposing API keys in your Flutter application can lead to unauthorized access and potential abuse. This can result in quota exhaustion, service disruptions, or even security breaches. Using Firebase Remote Config, encryption, and secure local storage, we can keep API keys safe.

    Project Setup Example:

    For this example, we will focus on using Firebase Remote Config to securely retrieve API keys, encrypt them before storing them locally, and decrypt them when needed.

    We will structure an implementation using the following:

    • remote_config.dart: Handles fetching and encrypting API keys.

    • global_config.dart: Initializes Firebase, loads environment variables, and ensures API keys are available.

    • main.dart: Starts the application and initializes configurations.

    • app_strings.dart: Stores constant values used throughout the project.

    Step 1: Setting Up Environment Variables

    Create a .env file in your Flutter project root directory and define your encryption key:

    ENCRYPTION_KEY=<span class="hljs-number">32</span>-character-secure-key-here
    

    Add flutter_dotenv to your pubspec.yaml:

    dependencies:
      flutter:
        sdk: flutter
      encrypt: ^<span class="hljs-number">5.0</span><span class="hljs-number">.3</span>
      flutter_dotenv: ^<span class="hljs-number">5.2</span><span class="hljs-number">.1</span>
      device_info_plus: ^<span class="hljs-number">11.3</span><span class="hljs-number">.0</span>
      firebase_remote_config: ^<span class="hljs-number">5.4</span><span class="hljs-number">.0</span>
      flutter_secure_storage: ^<span class="hljs-number">9.0</span><span class="hljs-number">.0</span>
    

    Run:

    flutter pub <span class="hljs-keyword">get</span>
    

    Step 2: Secure Storage and Encryption

    app_strings.dart

    Define constants used throughout the project:

    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppStrings</span> </span>{
      <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> <span class="hljs-built_in">String</span> ENCRYPTION_KEY = <span class="hljs-string">"ENCRYPTION_KEY"</span>;
      <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> <span class="hljs-built_in">String</span> DEVICE_ID = <span class="hljs-string">"DEVICE_ID"</span>;
      <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> <span class="hljs-built_in">String</span> YOU_VERIFY_API_KEY = <span class="hljs-string">"YOU_VERIFY_API_KEY"</span>;
      <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> <span class="hljs-built_in">String</span> GEMINI_API_KEY = <span class="hljs-string">"GEMINI_API_KEY"</span>;
    }
    

    remote_config.dart

    Handles secure retrieval and storage of API keys using AES encryption. This is a big one, so I’ll break down each part after the code block:

    <span class="hljs-keyword">import</span> <span class="hljs-string">'dart:io'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'package:device_info_plus/device_info_plus.dart'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/foundation.dart'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_secure_storage/flutter_secure_storage.dart'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'package:firebase_remote_config/firebase_remote_config.dart'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'../constants/app_strings.dart'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'../../../domain/models/custom_error/custom_error.dart'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'package:encrypt/encrypt.dart'</span> <span class="hljs-keyword">as</span> encrypt;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_dotenv/flutter_dotenv.dart'</span>;
    
    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RemoteConfig</span> </span>{
      <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> FlutterSecureStorage _storage = FlutterSecureStorage();
      <span class="hljs-keyword">static</span> encrypt.Encrypter? _encrypter;
    
      <span class="hljs-comment">// Initialize AES encryption</span>
      <span class="hljs-keyword">static</span> Future<<span class="hljs-keyword">void</span>> initializeEncrypter() <span class="hljs-keyword">async</span> {
        encrypt.Key key = <span class="hljs-keyword">await</span> _generateEncryptionKey();
        _encrypter = encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.cbc));
      }
    
      <span class="hljs-keyword">static</span> encrypt.Encrypter getEncrypter() {
        <span class="hljs-keyword">if</span> (_encrypter == <span class="hljs-keyword">null</span>) {
          initializeEncrypter();
        }
        <span class="hljs-keyword">return</span> _encrypter!;
      }
    
      <span class="hljs-comment">// Generate a secure encryption key using env variable and device ID</span>
      <span class="hljs-keyword">static</span> Future<encrypt.Key> _generateEncryptionKey() <span class="hljs-keyword">async</span> {
        <span class="hljs-built_in">String</span> envKey = dotenv.env[AppStrings.ENCRYPTION_KEY] ?? <span class="hljs-string">"default_secure_key"</span>;
        <span class="hljs-built_in">String</span> deviceId = <span class="hljs-keyword">await</span> _getDeviceId();
        <span class="hljs-built_in">String</span> combinedKey = (envKey + deviceId).substring(<span class="hljs-number">0</span>, <span class="hljs-number">32</span>);
        <span class="hljs-keyword">return</span> encrypt.Key.fromUtf8(combinedKey);
      }
    
      <span class="hljs-comment">// Fetch device ID and store it securely</span>
      <span class="hljs-keyword">static</span> Future<<span class="hljs-built_in">String</span>> _getDeviceId() <span class="hljs-keyword">async</span> {
        <span class="hljs-built_in">String?</span> storedDeviceId = <span class="hljs-keyword">await</span> _storage.read(key: AppStrings.DEVICE_ID);
    
        <span class="hljs-keyword">if</span> (storedDeviceId != <span class="hljs-keyword">null</span>) {
          <span class="hljs-keyword">return</span> storedDeviceId;
        }
    
        DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
        <span class="hljs-built_in">String</span> deviceId;
    
        <span class="hljs-keyword">if</span> (Platform.isAndroid) {
          AndroidDeviceInfo androidInfo = <span class="hljs-keyword">await</span> deviceInfo.androidInfo;
          deviceId = androidInfo.id;
        } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (Platform.isIOS) {
          IosDeviceInfo iosInfo = <span class="hljs-keyword">await</span> deviceInfo.iosInfo;
          deviceId = iosInfo.identifierForVendor ?? <span class="hljs-string">"fallbackDeviceId"</span>;
        } <span class="hljs-keyword">else</span> {
          deviceId = <span class="hljs-string">"fallbackDeviceId"</span>;
        }
    
        <span class="hljs-keyword">await</span> _storage.write(key: AppStrings.DEVICE_ID, value: deviceId);
        <span class="hljs-keyword">return</span> deviceId;
      }
    
      <span class="hljs-comment">// Fetch and encrypt API keys</span>
      <span class="hljs-keyword">static</span> Future<<span class="hljs-keyword">void</span>> fetchApiKey({<span class="hljs-keyword">required</span> <span class="hljs-built_in">String</span> apiKeyName}) <span class="hljs-keyword">async</span> {
        <span class="hljs-built_in">String</span> key = <span class="hljs-string">''</span>;
        <span class="hljs-keyword">try</span> {
          <span class="hljs-keyword">final</span> remoteConfig = FirebaseRemoteConfig.instance;
          <span class="hljs-keyword">await</span> remoteConfig.setConfigSettings(
            RemoteConfigSettings(
              fetchTimeout: <span class="hljs-keyword">const</span> <span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">10</span>),
              minimumFetchInterval: <span class="hljs-keyword">const</span> <span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">10</span>),
            ),
          );
          <span class="hljs-keyword">await</span> remoteConfig.fetchAndActivate();
          key = remoteConfig.getString(apiKeyName);
        } <span class="hljs-keyword">catch</span> (e) {
          <span class="hljs-keyword">if</span> (kDebugMode) {
            <span class="hljs-built_in">print</span>(e);
          }
          <span class="hljs-keyword">throw</span> CustomError(
            errorMsg: <span class="hljs-string">"ERROR Retrieving <span class="hljs-subst">$apiKeyName</span> (<span class="hljs-subst">${e.toString()}</span>)"</span>,
            code: <span class="hljs-string">"configuration_error"</span>,
            plugin: <span class="hljs-string">""</span>,
          );
        }
    
        <span class="hljs-keyword">final</span> iv = encrypt.IV.fromSecureRandom(<span class="hljs-number">16</span>);
        <span class="hljs-keyword">final</span> encryptedKey = _encrypter?.encrypt(key, iv: iv).base64;
    
        <span class="hljs-keyword">await</span> _storage.write(key: apiKeyName, value: encryptedKey);
        <span class="hljs-keyword">await</span> _storage.write(key: <span class="hljs-string">"<span class="hljs-subst">${apiKeyName}</span>_iv"</span>, value: iv.base64);
      }
    
      <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-built_in">Map</span><<span class="hljs-built_in">String</span>, <span class="hljs-built_in">String</span>> _decryptedKeysCache = {};
    
      <span class="hljs-comment">// Retrieve and decrypt stored API keys</span>
      <span class="hljs-keyword">static</span> Future<<span class="hljs-built_in">String?</span>> getApiKey({<span class="hljs-keyword">required</span> <span class="hljs-built_in">String</span> key}) <span class="hljs-keyword">async</span> {
        <span class="hljs-keyword">if</span> (_decryptedKeysCache.containsKey(key)) {
          <span class="hljs-keyword">return</span> _decryptedKeysCache[key];
        }
    
        <span class="hljs-keyword">try</span> {
          <span class="hljs-keyword">final</span> encryptedKey = <span class="hljs-keyword">await</span> _storage.read(key: key);
          <span class="hljs-keyword">final</span> ivString = <span class="hljs-keyword">await</span> _storage.read(key: <span class="hljs-string">"<span class="hljs-subst">${key}</span>_iv"</span>);
    
          <span class="hljs-keyword">if</span> (encryptedKey != <span class="hljs-keyword">null</span> && ivString != <span class="hljs-keyword">null</span>) {
            <span class="hljs-keyword">final</span> iv = encrypt.IV.fromBase64(ivString);
            <span class="hljs-keyword">final</span> encrypted = encrypt.Encrypted.fromBase64(encryptedKey);
            <span class="hljs-keyword">final</span> decryptedKey = _encrypter?.decrypt(encrypted, iv: iv);
    
            _decryptedKeysCache[key] = decryptedKey!;
            <span class="hljs-keyword">return</span> decryptedKey;
          }
        } <span class="hljs-keyword">catch</span> (e) {
          <span class="hljs-keyword">throw</span> CustomError(
            errorMsg: <span class="hljs-string">"ERROR Retrieving <span class="hljs-subst">$key</span> (<span class="hljs-subst">${e.toString()}</span>)"</span>,
            code: <span class="hljs-string">"configuration_error"</span>,
            plugin: <span class="hljs-string">""</span>,
          );
        }
    
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
      }
    }
    

    This RemoteConfig class securely fetches, encrypts, stores, and retrieves sensitive API keys using Firebase Remote Config, AES encryption, secure storage, and device-specific information.

    Here’s a breakdown of what’s going on:

    1. Dependencies and Imports

    • dart:io: For platform-specific checks (Android, iOS).

    • device_info_plus: To get a unique device identifier.

    • flutter_secure_storage: For secure local key-value storage.

    • firebase_remote_config: To fetch API keys or configurations from Firebase.

    • encrypt: For AES encryption and decryption.

    • flutter_dotenv: To read environment variables.

    • CustomError: A custom error model used for error handling.

    • AppStrings: Presumably holds constant strings like keys.

    2. Class Properties

    <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> FlutterSecureStorage _storage = FlutterSecureStorage();
    <span class="hljs-keyword">static</span> encrypt.Encrypter? _encrypter;
    
    • _storage: For securely storing encrypted keys and IVs.

    • _encrypter: Used to encrypt and decrypt data using AES.

    3. initializeEncrypter()

    <span class="hljs-keyword">static</span> Future<<span class="hljs-keyword">void</span>> initializeEncrypter() <span class="hljs-keyword">async</span>
    
    • Sets up the AES encryptor using a combination of a .env key and the device ID to generate a 32-byte key.

    • Uses AES CBC mode.

    4. getEncrypter()

    <span class="hljs-keyword">static</span> encrypt.Encrypter getEncrypter()
    
    • Returns the existing encryptor or calls initializeEncrypter() if it’s not yet initialized.

    5. _generateEncryptionKey()

    <span class="hljs-keyword">static</span> Future<encrypt.Key> _generateEncryptionKey()
    
    • Combines an environment variable (ENCRYPTION_KEY) and the device ID to produce a 32-character key.

    • Returns an AES key (encrypt.Key.fromUtf8).

    6. _getDeviceId()

    <span class="hljs-keyword">static</span> Future<<span class="hljs-built_in">String</span>> _getDeviceId()
    
    • Checks if a device ID is already securely stored. If not, gets it from the device (Android: androidInfo.id, iOS: identifierForVendor).

    • Stores the device ID securely for future use.

    7. fetchApiKey()

    <span class="hljs-keyword">static</span> Future<<span class="hljs-keyword">void</span>> fetchApiKey({<span class="hljs-keyword">required</span> <span class="hljs-built_in">String</span> apiKeyName})
    
    • Fetches the specified API key from Firebase Remote Config.

    • Encrypts the key using AES and a random IV (initialization vector).

    • Stores both the encrypted key and the IV securely.

    8. getApiKey()

    <span class="hljs-keyword">static</span> Future<<span class="hljs-built_in">String?</span>> getApiKey({<span class="hljs-keyword">required</span> <span class="hljs-built_in">String</span> key})
    
    • Retrieves and decrypts the encrypted API key.

    • If already decrypted and cached in memory, returns it immediately.

    • Otherwise:

      • Reads the encrypted key and IV from secure storage.

      • Decrypts the key and returns it.

      • Caches the decrypted result in _decryptedKeysCache.

    9. Error Handling

    Custom CustomError exceptions are thrown if Firebase fetch or decryption fails.

    This class is built to:

    • Securely fetch API keys from Firebase.

    • Encrypt them using a key tied to both an environment variable and the specific device.

    • Store them locally in an encrypted form.

    • Allow retrieval and decryption with in-memory caching to minimize processing overhead.

    Step 3: Global Initialization

    global_config.dart

    Handles Firebase initialization, dependency injection, and API key retrieval:

    <span class="hljs-keyword">import</span> <span class="hljs-string">'package:firebase_core/firebase_core.dart'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/widgets.dart'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_dotenv/flutter_dotenv.dart'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'package:injectable/injectable.dart'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'remote_config.dart'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'app_strings.dart'</span>;
    
    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">GlobalConfig</span> </span>{
      <span class="hljs-keyword">static</span> Future<<span class="hljs-keyword">void</span>> fetchRequiredApiKeys() <span class="hljs-keyword">async</span> {
        <span class="hljs-keyword">final</span> apiKeys = [
          AppStrings.YOU_VERIFY_API_KEY,
          AppStrings.GEMINI_API_KEY,
        ];
        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">final</span> keyName <span class="hljs-keyword">in</span> apiKeys) {
          <span class="hljs-keyword">await</span> RemoteConfig.fetchApiKey(apiKeyName: keyName);
        }
      }
    
      <span class="hljs-keyword">static</span> Future<<span class="hljs-keyword">void</span>> initConfig() <span class="hljs-keyword">async</span> {
        WidgetsFlutterBinding.ensureInitialized();
        <span class="hljs-keyword">await</span> Firebase.initializeApp();
        <span class="hljs-keyword">await</span> dotenv.load(fileName: <span class="hljs-string">".env"</span>);
        <span class="hljs-keyword">await</span> RemoteConfig.initializeEncrypter();
        <span class="hljs-keyword">await</span> fetchRequiredApiKeys();
      }
    }
    

    Step 4: Utilizing the API Key in UI

    main.dart

    Initializes the application:

    <span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'global_config.dart'</span>;
    
    Future<<span class="hljs-keyword">void</span>> main() <span class="hljs-keyword">async</span> {
      <span class="hljs-keyword">await</span> GlobalConfig.initConfig();
      runApp(MyApp());
    }
    

    Step 5: Fetching API Key in Widget

    <span class="hljs-built_in">String</span> apiKey = <span class="hljs-string">""</span>;
    
    <span class="hljs-meta">@override</span>
    <span class="hljs-keyword">void</span> initState() {
      <span class="hljs-keyword">super</span>.initState();
      fetchAPIKey();
    }
    
    <span class="hljs-keyword">void</span> fetchAPIKey() <span class="hljs-keyword">async</span> {
      <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">final</span> key = <span class="hljs-keyword">await</span> RemoteConfig.getApiKey(key: AppStrings.GEMINI_API_KEY) ?? <span class="hljs-string">""</span>;
        setState(() {
          apiKey = key;
        });
      } <span class="hljs-keyword">catch</span> (e) {
        <span class="hljs-built_in">print</span>(<span class="hljs-string">"Error fetching API key: <span class="hljs-subst">$e</span>"</span>);
      }
    }
    

    Common Vulnerabilities in Flutter Apps

    1. Hardcoding Secrets

    Storing API keys or secrets in the codebase (even in .env or .dart files) is one of the most dangerous mistakes. Tools like apktool can extract these secrets easily from the compiled binary.

    Avoid this:

    <span class="hljs-comment">// Do not hardcode keys</span>
    <span class="hljs-keyword">const</span> apiKey = <span class="hljs-string">'YOUR_SECRET_API_KEY'</span>;
    

    Hardcoding secrets is unsafe because when the APK is reverse-engineered, anyone can read those values and misuse your APIs.

    Use secure storage instead:

    <span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_secure_storage/flutter_secure_storage.dart'</span>;
    
    <span class="hljs-keyword">final</span> storage = FlutterSecureStorage();
    <span class="hljs-keyword">await</span> storage.write(key: <span class="hljs-string">'api_key'</span>, value: <span class="hljs-string">'your_api_key'</span>);
    <span class="hljs-keyword">final</span> apiKey = <span class="hljs-keyword">await</span> storage.read(key: <span class="hljs-string">'api_key'</span>);
    

    Using flutter_secure_storage stores secrets securely in platform-specific secure storage mechanisms like Android’s Keystore or iOS’s Keychain.

    2. Lack of SSL/TLS Enforcement (MITM Attacks)

    A Man-in-the-Middle (MITM) attack occurs when an attacker intercepts and potentially alters communication between two parties. This is especially dangerous in unsecured HTTP connections, as sensitive information like login credentials and API keys can be stolen or modified.

    How SSL/TLS Secures Code:

    Secure Sockets Layer (SSL) and Transport Layer Security (TLS) are cryptographic protocols that ensure encrypted communication between a client and a server. This prevents MITM attacks by ensuring that the data is encrypted and cannot be read or altered while in transit. The connection is established over HTTPS (which is HTTP over SSL/TLS).

    Code Example to Enforce SSL/TLS:

    <span class="hljs-keyword">import</span> <span class="hljs-string">'package:http/http.dart'</span> <span class="hljs-keyword">as</span> http;
    
    <span class="hljs-keyword">void</span> makeSecureRequest() <span class="hljs-keyword">async</span> {
      <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> http.<span class="hljs-keyword">get</span>(<span class="hljs-built_in">Uri</span>.parse(<span class="hljs-string">'https://yourapi.com/endpoint'</span>));
    
      <span class="hljs-keyword">if</span> (response.statusCode == <span class="hljs-number">200</span>) {
        <span class="hljs-comment">// Handle successful response</span>
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-comment">// Handle error</span>
      }
    }
    

    In this case, making sure that the URL starts with https:// enforces the use of SSL/TLS for secure communication

    3. Weak Authentication

    Weak authentication methods are those that are easily guessed or bypassed, such as simple passwords, lack of multi-factor authentication, or weak hashing mechanisms.

    Instead, you should use robust authentication methods like Firebase and OAuth.

    • Firebase Authentication provides various authentication methods such as email/password login, Google sign-in, and phone number authentication. It is a secure and easy-to-implement solution.

    • OAuth is a protocol that allows third-party services (like Google or Facebook) to securely authenticate users without exposing their password to your app. OAuth uses tokens for authorization, ensuring that user credentials are not compromised.

    Use Firebase Auth or OAuth2:

    <span class="hljs-keyword">import</span> <span class="hljs-string">'package:firebase_auth/firebase_auth.dart'</span>;
    
    <span class="hljs-keyword">final</span> FirebaseAuth _auth = FirebaseAuth.instance;
    UserCredential userCredential = <span class="hljs-keyword">await</span> _auth.signInWithEmailAndPassword(
      email: <span class="hljs-string">'user@example.com'</span>,
      password: <span class="hljs-string">'securePassword'</span>,
    );
    <span class="hljs-keyword">final</span> token = <span class="hljs-keyword">await</span> userCredential.user?.getIdToken();
    

    Token-based authentication allows the backend to verify the identity of the user securely without relying on session cookies. Firebase Authentication handles token generation, validation, and expiration for you.

    4. Insufficient Authorization Checks

    Authorization checks are necessary to ensure that the authenticated user has the required permissions to perform certain actions. For example, an admin user may have access to all endpoints, while a regular user may only have access to limited resources.

    How to Verify User Roles/Permissions:

    On the server side, roles and permissions are typically stored in a database. When a user makes a request, the server checks their role and compares it against the required permissions for the requested resource.

    Code Example:

    <span class="hljs-comment">// Assuming user roles are stored in Firestore</span>
    Future<<span class="hljs-built_in">bool</span>> checkUserRole(<span class="hljs-built_in">String</span> userId, <span class="hljs-built_in">String</span> requiredRole) <span class="hljs-keyword">async</span> {
      <span class="hljs-keyword">final</span> userDoc = <span class="hljs-keyword">await</span> FirebaseFirestore.instance.collection(<span class="hljs-string">'users'</span>).doc(userId).<span class="hljs-keyword">get</span>();
      <span class="hljs-keyword">final</span> userRole = userDoc.data()?[<span class="hljs-string">'role'</span>];
    
      <span class="hljs-keyword">if</span> (userRole == requiredRole) {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">throw</span> CustomError(errorMsg: <span class="hljs-string">'User does not have the required role'</span>);
      }
    }
    

    Authorization ensures the user is not only authenticated but also has the rights to perform specific actions.

    5. Exposed Endpoints and Metadata

    Exposing Swagger documentation or test endpoints in production can allow attackers to easily discover vulnerabilities in your API. It provides them with detailed information about the structure and capabilities of your API, which can be exploited.

    How to Secure with Route Guards:

    A route guard can prevent unauthorized access to sensitive routes, ensuring that only authenticated and authorized users can access certain endpoints.

    <span class="hljs-keyword">void</span> checkRouteAccess(<span class="hljs-built_in">String</span> route) {
      <span class="hljs-keyword">if</span> (!isUserAuthenticated()) {
        <span class="hljs-keyword">throw</span> CustomError(errorMsg: <span class="hljs-string">'User not authorized'</span>);
      }
    }
    

    Avoid this:

    • Don’t deploy Swagger docs without authentication

    • Use route guards for admin/dev routes

    • Strip debug symbols and logs in production builds

    Example: Secure API Call in Flutter

    Here’s a simple example using Dio, a powerful HTTP client for Dart, to securely call an API with token-based authentication and HTTPS:

    <span class="hljs-keyword">import</span> <span class="hljs-string">'package:dio/dio.dart'</span>;
    <span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_secure_storage/flutter_secure_storage.dart'</span>;
    
    <span class="hljs-keyword">final</span> dio = Dio();
    <span class="hljs-keyword">final</span> storage = FlutterSecureStorage();
    
    Future<<span class="hljs-keyword">void</span>> fetchSecureData() <span class="hljs-keyword">async</span> {
      <span class="hljs-keyword">final</span> token = <span class="hljs-keyword">await</span> storage.read(key: <span class="hljs-string">'auth_token'</span>);
    
      dio.options.headers[<span class="hljs-string">'Authorization'</span>] = <span class="hljs-string">'Bearer <span class="hljs-subst">$token</span>'</span>;
    
      <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> dio.<span class="hljs-keyword">get</span>(<span class="hljs-string">'https://yourapi.com/secure-endpoint'</span>);
        <span class="hljs-built_in">print</span>(response.data);
      } <span class="hljs-keyword">catch</span> (e) {
        <span class="hljs-built_in">print</span>(<span class="hljs-string">'API call failed: <span class="hljs-subst">$e</span>'</span>);
      }
    }
    

    This example illustrates how to include an authorization token in your request header and securely make an HTTPS request using dio. Dio also supports interceptors, retries, and advanced options like certificate pinning.

    Best Practices for Securing APIs in Flutter Apps

    Always Use HTTPS

    Avoid plain HTTP at all costs. Use HTTPS to encrypt data in transit.

    <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> http.<span class="hljs-keyword">get</span>(<span class="hljs-built_in">Uri</span>.parse(<span class="hljs-string">'https://api.secure.com/data'</span>));
    

    Implement OAuth2 or Firebase Auth

    Use modern authentication packages like firebase_auth or oauth2_client. These offer secure, token-based authentication with built-in session and refresh token management.

    Use Firebase App Check

    Prevents abuse of your backend by verifying the legitimacy of the client app.

    <span class="hljs-keyword">await</span> FirebaseAppCheck.instance.activate(
      webRecaptchaSiteKey: <span class="hljs-string">'your-site-key'</span>,
    );
    

    Secure Storage of Sensitive Data

    Use flutter_secure_storage to safely store tokens and secrets locally.

    Obfuscate Dart Code

    Obfuscation makes your Dart code harder to reverse-engineer. You can do this by renaming classes, methods, and variables into meaningless names.

    flutter build apk --obfuscate --split-debug-info=build/symbols
    

    This command strips debug information and renames classes/functions, making it harder for attackers to understand your compiled code.

    Use Rate Limiting and Throttling

    Protect backend APIs from abuse by rate-limiting requests. Implement server-side rate-limiting using API Gateway tools or middleware libraries. Here’s a tutorial that’ll teach you more about this technique.

    Set Up Logging and Monitoring

    Use tools like Firebase Crashlytics or Sentry to track errors and suspicious activity.

    FirebaseCrashlytics.instance.recordError(e, stackTrace);
    

    API Gateway and WAF

    Use API management layers like Google Cloud Endpoints or AWS API Gateway along with Web Application Firewalls (WAF) to control and filter traffic.

    Security Checklist for Flutter Developers

    • Use HTTPS for all communications

    • Never hardcode secrets or credentials

    • Use token-based authentication (OAuth2, Firebase Auth)

    • Validate tokens on both client and server

    • Obfuscate and minify code before production

    • Securely store sensitive data using flutter_secure_storage

    • Enable Firebase App Check or equivalent

    • Use API Gateways and WAFs for traffic filtering

    • Monitor usage logs and set up alerts for anomalies

    • Implement rate limiting to prevent abuse

    Additional Considerations

    Certificate Pinning:

    Certificate pinning is a technique used to ensure that the app only communicates with a trusted server by comparing the server’s certificate against a pre-stored certificate or public key. This prevents attackers from using fraudulent certificates.

    Example: Certificate Pinning in Dio

    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CertPinningInterceptor</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Interceptor</span> </span>{
      <span class="hljs-meta">@override</span>
      <span class="hljs-keyword">void</span> onRequest(RequestOptions options, RequestInterceptorHandler handler) <span class="hljs-keyword">async</span> {
        <span class="hljs-keyword">final</span> context = SecurityContext(withTrustedRoots: <span class="hljs-keyword">false</span>);
        <span class="hljs-keyword">final</span> certBytes = (<span class="hljs-keyword">await</span> rootBundle.load(<span class="hljs-string">'assets/certs/myserver.cer'</span>)).buffer.asUint8List();
        context.setTrustedCertificatesBytes(certBytes);
    
        <span class="hljs-keyword">final</span> client = HttpClient(context: context);
        client.badCertificateCallback = (X509Certificate cert, <span class="hljs-built_in">String</span> host, <span class="hljs-built_in">int</span> port) {
          <span class="hljs-keyword">final</span> serverSha = sha256.convert(cert.der).toString();
          <span class="hljs-keyword">const</span> expectedSha = <span class="hljs-string">'your_cert_sha256_fingerprint'</span>;
          <span class="hljs-keyword">return</span> serverSha == expectedSha;
        };
    
        (options.extra[<span class="hljs-string">'httpClientAdapter'</span>] <span class="hljs-keyword">as</span> DefaultHttpClientAdapter?)
            ?.onHttpClientCreate = (_) => client;
    
        handler.next(options);
      }
    }
    
    • SecurityContext(withTrustedRoots: false): Starts with an empty trust store, meaning no system CAs are trusted by default.

    • setTrustedCertificatesBytes: Loads your own server’s certificate from local assets and sets it as the only trusted certificate.

    • HttpClient.badCertificateCallback: Compares the server’s certificate SHA-256 fingerprint against a known good value. If they match, the request proceeds.

    • onHttpClientCreate: Replaces the default Dio HTTP client with the custom client configured for pinning.

    This ensures that your app will only accept HTTPS connections from your own trusted server, protecting users from certificate spoofing or MITM attacks.

    TTL and Token Rotation:

    Time-to-Live (TTL) is a security measure that ensures tokens automatically expire after a defined period. This limits the duration a token can be used, reducing the attack surface if it’s compromised.

    Token Rotation enhances security further by issuing a new refresh token every time the existing one is used to request a new access token. The previous refresh token is then invalidated. This prevents replay attacks where an attacker might attempt to reuse a stolen refresh token.

    Real-World Token Lifecycle:

    1. Access Token:

      • TTL: Short (for example, 15 minutes)

      • Purpose: Used to authenticate and authorize API requests

      • Behavior: Expires quickly to minimize risk if exposed

    2. Refresh Token:

      • TTL: Longer (for example, 7 days)

      • Purpose: Used to request new access tokens without requiring the user to log in again

      • Rotation: A new refresh token is issued with each use

    Here’s an example Implementation (Dart-like Pseudo-code):

    Generate an access token (15-minute TTL):

    <span class="hljs-built_in">String</span> generateAccessToken(<span class="hljs-built_in">String</span> userId) {
      <span class="hljs-keyword">final</span> expiry = <span class="hljs-built_in">DateTime</span>.now().add(<span class="hljs-built_in">Duration</span>(minutes: <span class="hljs-number">15</span>));
      <span class="hljs-keyword">return</span> createJwtToken(userId: userId, expiresAt: expiry);
    }
    

    Then generate a refresh token (7-day TTL):

    <span class="hljs-built_in">String</span> generateRefreshToken(<span class="hljs-built_in">String</span> userId) {
      <span class="hljs-keyword">final</span> expiry = <span class="hljs-built_in">DateTime</span>.now().add(<span class="hljs-built_in">Duration</span>(days: <span class="hljs-number">7</span>));
      <span class="hljs-keyword">return</span> createSecureRandomToken(userId: userId, expiresAt: expiry);
    }
    

    Refresh the endpoint with rotation:

    <span class="hljs-built_in">Map</span><<span class="hljs-built_in">String</span>, <span class="hljs-built_in">String</span>> refreshAccessToken(<span class="hljs-built_in">String</span> refreshToken) {
      <span class="hljs-keyword">if</span> (isValidRefreshToken(refreshToken)) {
        <span class="hljs-keyword">final</span> userId = getUserIdFromRefreshToken(refreshToken);
    
        <span class="hljs-comment">// Invalidate old refresh token</span>
        invalidateRefreshToken(refreshToken);
    
        <span class="hljs-comment">// Rotate tokens</span>
        <span class="hljs-keyword">final</span> newRefreshToken = generateRefreshToken(userId);
        <span class="hljs-keyword">final</span> newAccessToken = generateAccessToken(userId);
    
        <span class="hljs-keyword">return</span> {
          <span class="hljs-string">'accessToken'</span>: newAccessToken,
          <span class="hljs-string">'refreshToken'</span>: newRefreshToken,
        };
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">throw</span> CustomError(errorMsg: <span class="hljs-string">'Invalid or expired refresh token'</span>);
      }
    }
    

    Why this matters:

    • Mitigates long-term exposure: Tokens automatically expire, reducing risk.

    • Prevents replay attacks: A rotated refresh token cannot be reused if intercepted.

    • Enhances session security: Even if a token is stolen, it becomes useless quickly.

    Backend Validation:

    Backend validation ensures that sensitive data, like API keys or JWT tokens, is checked on the server side, preventing tampering from malicious users.

    Never trust the client. Always re-validate all sensitive operations and user roles on the backend.

    Example:

    <span class="hljs-keyword">void</span> validateToken(<span class="hljs-built_in">String</span> token) {
      <span class="hljs-keyword">if</span> (isTokenExpired(token)) {
        <span class="hljs-keyword">throw</span> CustomError(errorMsg: <span class="hljs-string">'Token expired'</span>);
      }
    }
    
    • validateToken(String token): A function that takes a user’s token as input.

    • isTokenExpired(token): A hypothetical function that checks whether the token has expired (e.g., by decoding the token and checking its expiry timestamp).

    • throw CustomError(...): If the token is expired, an error is thrown — in this case, a CustomError with a message saying 'Token expired'.

    Why this matters:

    • Tokens can be stolen or manipulated on the client side, so trusting them blindly is dangerous.

    • Backend checks like this help enforce server-side control over user authentication and session validity.

    • Even if a user tampers with client-side code, they can’t bypass this server-side validation.

    Use Security-focused Tools like OWASP ZAP/Burp Suite/Postman:

    Use tools like OWASP ZAP, Burp Suite, and Postman to manually and automatically test your API endpoints for vulnerabilities.

    • OWASP ZAP: Used for penetration testing, finding vulnerabilities like XSS, SQL Injection, and so on.

    • Burp Suite: Another tool for testing security vulnerabilities in web apps.

    • Postman: Can be used for testing API endpoints and ensuring secure communications by adding necessary headers like Authorization.

    Conclusion

    Securing mobile APIs is a foundational requirement in modern app development. For Flutter developers, this means going beyond building beautiful UIs to ensuring the underlying API infrastructure is resilient against threats. The risks of exposed endpoints, leaked secrets, and insecure communication are very real, but preventable.

    Security is about proactive defense, and you should make it a core part of your development workflow. With consistent practices, regular audits, and attention to detail, you’ll protect both your users and your product from unnecessary risks. Flutter provides the flexibility and power to build fast, cross-platform apps – so don’t let poor API security undermine that potential.

    By following the best practices outlined in this article, such as using HTTPS, implementing proper authentication and authorization, securely storing credentials, and leveraging tools like Firebase App Check, you can significantly reduce your app’s attack surface.

    Remember: effective security starts with a mindset. It’s not just a one-time setup, but an ongoing process of vigilance, testing, and improvement.

    References

    • OWASP Mobile Security Project

    • OWASP API Security Top 10

    • Android Security Best Practices

    • Flutter Secure Storage – pub.dev

    • Encrypt Package – pub.dev

    • Firebase Remote Config – Firebase Docs

    • Device Info Plus – pub.dev

    • Flutter dotenv – pub.dev

    • Injectable for Dependency Injection – pub.dev

    • Flutter Fire (Firebase Initialization) – Firebase Docs

    Source: freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More 

    Facebook Twitter Reddit Email Copy Link
    Previous ArticleOpera’s Android browser just got a major tab management upgrade
    Next Article How to Create Documentation with docs.page – A Beginner’s Tutorial

    Related Posts

    Development

    @ts-ignore is almost always the worst option

    September 22, 2025
    Development

    MutativeJS v1.3.0 is out with massive performance gains

    September 22, 2025
    Leave A Reply Cancel Reply

    For security, use of Google's reCAPTCHA service is required which is subject to the Google Privacy Policy and Terms of Use.

    Continue Reading

    Student Insider Threats Driving Surge in UK School Data Breaches, ICO Warns

    Development

    CVE-2025-3812 – WordPress WPBot Pro File Deletion Vulnerability

    Common Vulnerabilities and Exposures (CVEs)

    CVE-2025-38164 – VirtualBox F2FS Inconsistent Segment Type

    Common Vulnerabilities and Exposures (CVEs)

    How to Set Up Coolify in AWS EC2 and Have the Power to Do Anything in the Cloud

    Development

    Highlights

    Machine Learning

    Automate AIOps with Amazon SageMaker Unified Studio projects, Part 1: Solution architecture

    August 12, 2025

    Amazon SageMaker Unified Studio represents the evolution towards unifying the entire data, analytics, and artificial…

    CVE-2025-57773 – DataEase JNDI Injection Vulnerability

    August 25, 2025

    Google fixes fourth actively exploited Chrome zero-day of 2025

    July 1, 2025

    CVE-2025-5549 – FreeFloat FTP Server PASV Command Handler Buffer Overflow

    June 3, 2025
    © DevStackTips 2025. All rights reserved.
    • Contact
    • Privacy Policy

    Type above and press Enter to search. Press Esc to cancel.