Authentication & Security

Secure Storage with flutter_secure_storage

15 min Lesson 6 of 12

Secure Storage with flutter_secure_storage

Most Flutter apps need to persist sensitive data — authentication tokens, API keys, refresh tokens, or user credentials — between sessions. Storing this data in plain text (for example via SharedPreferences) is a serious security risk. The flutter_secure_storage package solves this by writing data to the OS-level secure enclave: iOS Keychain on Apple devices and the Android Keystore backed by hardware-level encryption on Android.

Note: SharedPreferences stores key-value pairs in an XML file on Android (shared_prefs/) and NSUserDefaults on iOS. Both are plain-text and readable by anyone with access to the device filesystem or a backup. Never store tokens or passwords there.

SharedPreferences vs flutter_secure_storage

Understanding the difference is critical before choosing a storage mechanism:

  • SharedPreferences — Unencrypted, suitable for non-sensitive preferences (theme, language, onboarding flags).
  • flutter_secure_storage — AES-256 encrypted on Android (via Keystore), Keychain on iOS. Suitable for tokens, passwords, and API secrets.
  • On Android, data is encrypted with a key that is itself stored in the Android Keystore and never leaves secure hardware on supported devices.
  • On iOS, items are stored in the Keychain with the accessibility option you configure (e.g., only accessible when the device is unlocked).

Adding the Dependency

Add the package to pubspec.yaml:

pubspec.yaml

dependencies:
  flutter_secure_storage: ^9.2.2

On Android you must set the minimum SDK version to 18 in android/app/build.gradle:

android/app/build.gradle

android {
    defaultConfig {
        minSdkVersion 18
        // ...
    }
}
Tip: On iOS, flutter_secure_storage uses Keychain Services, which requires no extra configuration. On Android 6+ (API 23+), the encryption key is backed by hardware-secured Keystore. On older devices, it falls back to RSA + AES in software.

Core API: Read, Write, Delete

The API is intentionally simple. All methods are async and return Futures. Instantiate FlutterSecureStorage once (or use a singleton/injected instance):

Basic CRUD Operations

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class TokenRepository {
  final FlutterSecureStorage _storage = const FlutterSecureStorage();

  static const _accessTokenKey  = 'access_token';
  static const _refreshTokenKey = 'refresh_token';

  // Write / overwrite a value
  Future<void> saveTokens({
    required String accessToken,
    required String refreshToken,
  }) async {
    await _storage.write(key: _accessTokenKey,  value: accessToken);
    await _storage.write(key: _refreshTokenKey, value: refreshToken);
  }

  // Read (returns null if key does not exist)
  Future<String?> getAccessToken() async {
    return _storage.read(key: _accessTokenKey);
  }

  // Check existence without reading the value
  Future<bool> hasSession() async {
    final token = await _storage.read(key: _accessTokenKey);
    return token != null && token.isNotEmpty;
  }

  // Delete a single key
  Future<void> deleteAccessToken() async {
    await _storage.delete(key: _accessTokenKey);
  }

  // Wipe all entries written by this app (use on logout)
  Future<void> clearAll() async {
    await _storage.deleteAll();
  }
}

Platform-Specific Options

You can customise behaviour per platform by passing option objects to the constructor. This is important for production apps that need specific accessibility or authentication policies:

Android & iOS Options

final storage = FlutterSecureStorage(
  // Android: require user authentication before reading the key
  aOptions: const AndroidOptions(
    encryptedSharedPreferences: true, // uses EncryptedSharedPreferences API
  ),
  // iOS: item is only accessible when device is unlocked
  iOptions: const IOSOptions(
    accessibility: KeychainAccessibility.unlocked,
  ),
);

// Reading with the same options object
Future<String?> readSensitiveKey(String key) async {
  return storage.read(key: key);
}
Warning: On Android, if you change the AndroidOptions (e.g., toggle encryptedSharedPreferences) after data has already been written, the old entries become unreadable because the encryption scheme differs. Always migrate or wipe existing storage when changing options in a production update.

Storing and Restoring Auth State on App Start

A common pattern is to check for a stored token when the app launches and redirect to the main screen if the session is still valid:

Splash / Auth Gate Pattern

class AuthGate extends StatelessWidget {
  const AuthGate({super.key});

  Future<bool> _checkSession() async {
    const storage = FlutterSecureStorage();
    final token = await storage.read(key: 'access_token');
    return token != null && token.isNotEmpty;
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<bool>(
      future: _checkSession(),
      builder: (context, snapshot) {
        if (snapshot.connectionState != ConnectionState.done) {
          return const Scaffold(
            body: Center(child: CircularProgressIndicator()),
          );
        }
        final isLoggedIn = snapshot.data ?? false;
        return isLoggedIn
            ? const HomeScreen()
            : const LoginScreen();
      },
    );
  }
}

Security Best Practices

  • Always call deleteAll() on logout — never leave tokens in storage after the session ends.
  • Prefer short-lived access tokens with refresh tokens, rotating on each use.
  • Do not store plaintext passwords — use tokens or hashed credentials only.
  • Combine with local_auth (biometrics) for an additional layer of security before reading from the Keystore/Keychain.
  • On Android, set encryptedSharedPreferences: true to use Jetpack Security's EncryptedSharedPreferences API rather than the legacy RSA-wrapped AES scheme.
Tip: In unit tests, inject a mock or fake storage instead of the real FlutterSecureStorage. The package's constructor accepts no default, so wrapping it behind an abstract interface (e.g., abstract class SecureStorage) makes your repository 100% testable without touching device Keychain/Keystore.

Summary

flutter_secure_storage is the standard solution for persisting sensitive data in Flutter. It wraps iOS Keychain and Android Keystore behind a clean async API. Use write, read, delete, and deleteAll for full lifecycle management of secrets. Always prefer it over SharedPreferences for any data you would not want a malicious actor to read, and call deleteAll() on logout to leave no credentials behind on the device.