Skip to main content
The login_client_flutter package provides a secure, production-ready implementation of CredentialsStorage for the login_client package using flutter_secure_storage. This ensures OAuth2 credentials are stored securely on device.

Installation

Add both packages to your pubspec.yaml:
dependencies:
  login_client: ^3.2.0
  login_client_flutter: ^3.0.0
This package requires Flutter SDK >=3.10.0.

Quick Start

1

Import the package

import 'package:login_client/login_client.dart';
import 'package:login_client_flutter/login_client_flutter.dart';
2

Use FlutterSecureCredentialsStorage

Create a LoginClient with FlutterSecureCredentialsStorage:
final loginClient = LoginClient(
  oAuthSettings: OAuthSettings(
    authorizationUri: Uri.parse('https://api.example.com/oauth/token'),
    clientId: 'com.example.myapp',
  ),
  credentialsStorage: const FlutterSecureCredentialsStorage(),
);
3

Initialize and use

await loginClient.initialize();

// Credentials are automatically saved securely
await loginClient.logIn(
  ResourceOwnerPasswordStrategy('[email protected]', 'password'),
);

API Reference

FlutterSecureCredentialsStorage

A secure implementation of CredentialsStorage using flutter_secure_storage.
class FlutterSecureCredentialsStorage implements CredentialsStorage {
  const FlutterSecureCredentialsStorage();

  Future<Credentials?> read();
  Future<void> save(Credentials credentials);
  Future<void> clear();
}

Storage Details

  • Key: login_client_flutter_credentials
  • Storage: Uses FlutterSecureStorage under the hood
  • Platform-specific:
    • iOS: Keychain
    • Android: AES encryption via Android Keystore
    • macOS: Keychain
    • Linux: libsecret
    • Windows: DPAPI
    • Web: Not supported (use alternative storage)

Methods

read()
Reads stored credentials from secure storage.
final credentials = await storage.read();
if (credentials != null) {
  print('Found saved credentials');
}
Returns:
  • Future<Credentials?> - The stored credentials, or null if none exist or parsing fails
save()
Saves credentials to secure storage.
final credentials = Credentials(
  accessToken: 'token',
  refreshToken: 'refresh',
  tokenEndpoint: Uri.parse('https://api.example.com/oauth/token'),
);

await storage.save(credentials);
Parameters:
  • credentials (Credentials) - The credentials to store
clear()
Removes credentials from secure storage.
await storage.clear();
print('Credentials cleared');

Android Configuration

Android requires special configuration to prevent javax.crypto.BadPaddingException errors when restoring from backup.

Exclude from Android Backup

Flutter Secure Storage must be excluded from Android’s automatic backup system:
1

Update AndroidManifest.xml

Add android:fullBackupContent to your application tag:
<!-- android/app/src/main/AndroidManifest.xml -->
<manifest>
    <application
        android:name="${applicationName}"
        android:label="your_app_name"
        android:icon="@mipmap/ic_launcher"
        android:fullBackupContent="@xml/backup_rules">
        <!-- ... -->
    </application>
</manifest>
2

Create backup rules

Create a new file at android/app/src/main/res/xml/backup_rules.xml:
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
    <exclude domain="sharedpref" path="FlutterSecureStorage" />
</full-backup-content>
3

Create res/xml directory if needed

If the res/xml directory doesn’t exist, create it:
mkdir -p android/app/src/main/res/xml

Why This Is Required

Android’s automatic backup can restore encrypted credentials, but the encryption keys may have changed, causing decryption failures. Excluding FlutterSecureStorage from backups prevents this issue.

iOS Configuration

No additional configuration is required for iOS. Credentials are automatically stored in the iOS Keychain.
If you need to share credentials between apps, configure Keychain access groups:
// Note: FlutterSecureCredentialsStorage uses default configuration
// For custom configurations, use FlutterSecureStorage directly
See flutter_secure_storage documentation for advanced configuration.

Complete Example

import 'package:flutter/material.dart';
import 'package:login_client/login_client.dart';
import 'package:login_client_flutter/login_client_flutter.dart';

class AuthService {
  late final LoginClient _loginClient;
  
  Future<void> initialize() async {
    final oAuthSettings = OAuthSettings(
      authorizationUri: Uri.parse('https://api.example.com/oauth/token'),
      clientId: 'com.example.myapp',
      scopes: ['read', 'write'],
    );

    _loginClient = LoginClient(
      oAuthSettings: oAuthSettings,
      // Use secure storage for credentials
      credentialsStorage: const FlutterSecureCredentialsStorage(),
    );

    // Restore saved credentials if they exist
    await _loginClient.initialize();

    // Check if user is already logged in
    if (_loginClient.loggedIn) {
      print('User is already authenticated');
    }
  }

  Future<void> login(String email, String password) async {
    try {
      await _loginClient.logIn(
        ResourceOwnerPasswordStrategy(email, password),
      );
      print('Login successful - credentials saved securely');
    } on AuthorizationException catch (e) {
      print('Login failed: ${e.description}');
      rethrow;
    }
  }

  Future<void> logout() async {
    await _loginClient.logOut();
    print('Logged out - credentials cleared from secure storage');
  }

  bool get isLoggedIn => _loginClient.loggedIn;

  LoginClient get client => _loginClient;

  void dispose() {
    _loginClient.dispose();
  }
}

// Usage in Flutter app
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  final authService = AuthService();
  await authService.initialize();
  
  runApp(MyApp(authService: authService));
}

class MyApp extends StatelessWidget {
  const MyApp({required this.authService, Key? key}) : super(key: key);
  
  final AuthService authService;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: authService.isLoggedIn
          ? const HomeScreen()
          : const LoginScreen(),
    );
  }
}

Integration with CQRS

import 'package:login_client/login_client.dart';
import 'package:login_client_flutter/login_client_flutter.dart';
import 'package:cqrs/cqrs.dart';

class ApiService {
  late final LoginClient _loginClient;
  late final Cqrs _cqrs;

  Future<void> initialize() async {
    // Setup authenticated client with secure storage
    _loginClient = LoginClient(
      oAuthSettings: OAuthSettings(
        authorizationUri: Uri.parse('https://api.example.com/oauth/token'),
        clientId: 'com.example.myapp',
      ),
      credentialsStorage: const FlutterSecureCredentialsStorage(),
    );

    await _loginClient.initialize();

    // Setup CQRS client using authenticated HTTP client
    _cqrs = Cqrs(
      _loginClient,  // Automatically includes auth headers
      Uri.parse('https://api.example.com/api/'),
    );
  }

  Future<void> login(String username, String password) async {
    await _loginClient.logIn(
      ResourceOwnerPasswordStrategy(username, password),
    );
  }

  Future<QueryResult<T>> query<T>(Query<T> query) => _cqrs.get(query);
  Future<CommandResult> command(Command cmd) => _cqrs.run(cmd);

  bool get isAuthenticated => _loginClient.loggedIn;
}

Security Considerations

FlutterSecureStorage provides platform-specific secure storage:
  • iOS/macOS: Keychain with kSecAttrAccessibleAfterFirstUnlock
  • Android: AES encryption with keys stored in Android Keystore
  • Linux: libsecret
  • Windows: Windows Credential Manager (DPAPI)
Always exclude credentials from backups to prevent decryption errors:
  • Android: Use backup_rules.xml (required)
  • iOS: Keychain items are automatically excluded from iCloud backup
If encryption keys are lost (device reset, OS reinstall), credentials cannot be recovered. Your app should handle this gracefully by logging the user out.
On compromised devices, secure storage may be accessible. Consider additional security measures for high-security applications.

Testing

For testing, you can use InMemoryCredentialsStorage instead:
import 'package:login_client/login_client.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('Login flow test', (tester) async {
    final loginClient = LoginClient(
      oAuthSettings: OAuthSettings(
        authorizationUri: Uri.parse('https://test.example.com/oauth/token'),
        clientId: 'test_client',
      ),
      // Use in-memory storage for tests
      credentialsStorage: InMemoryCredentialsStorage(),
    );

    await loginClient.initialize();
    
    // Test login logic...
  });
}

Migration from Other Storage

If you’re migrating from custom storage to FlutterSecureCredentialsStorage:
Future<void> migrateCredentials(
  CredentialsStorage oldStorage,
  FlutterSecureCredentialsStorage newStorage,
) async {
  final credentials = await oldStorage.read();
  if (credentials != null) {
    await newStorage.save(credentials);
    await oldStorage.clear();
    print('Credentials migrated successfully');
  }
}

Troubleshooting

This occurs when Android restores encrypted data but encryption keys have changed.Solution: Follow the Android Configuration steps above to exclude FlutterSecureStorage from backups.
Ensure you’ve called await loginClient.initialize() after creating the client.
final loginClient = LoginClient(
  oAuthSettings: settings,
  credentialsStorage: const FlutterSecureCredentialsStorage(),
);

// This is required!
await loginClient.initialize();
Check if you have the correct permissions and configurations:
  • Android: Verify backup_rules.xml is properly configured
  • iOS: Ensure keychain entitlements are set (done automatically by Flutter)

login_client

Core OAuth2 client implementation

flutter_secure_storage

Underlying secure storage package

cqrs

CQRS client for authenticated API calls

Best Practices

1

Always use secure storage in production

Never use InMemoryCredentialsStorage in production apps. Always use FlutterSecureCredentialsStorage.
2

Configure Android backup exclusion

Set up backup_rules.xml before releasing to prevent credential decryption errors.
3

Handle initialization errors

Wrap initialization in try-catch to handle potential storage errors gracefully.
try {
  await loginClient.initialize();
} catch (e) {
  // Handle storage errors - credentials may be corrupted
  await loginClient.logOut();
}
4

Test on multiple devices

Test credential persistence across app restarts on different Android and iOS devices.

Build docs developers (and LLMs) love