critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

SecureSessionStorage class is implemented in lib/src/features/auth/data/secure_session_storage.dart
Constructor accepts a FlutterSecureStorage instance for dependency injection (enables mocking in tests)
writeSession(StoredSession session) writes all fields (token, expiry, userId) as separate keys using SecureStorageKey enum, with platform-specific options applied
readSession() reads all session fields, returns null if any required key is missing, returns null if StoredSession.isExpired is true (expired sessions treated as absent)
clearSession() deletes all session-related keys (sessionToken, sessionExpiry, userId, refreshToken) atomically where possible
writeBiometricPreference(bool enabled) writes the biometricEnabled key
readBiometricPreference() returns the stored bool, defaulting to false if key is absent
All write operations use IOSOptions(accessibility: KeychainAccessibility.firstUnlockThisDeviceOnly) on iOS
All write operations use AndroidOptions(encryptedSharedPreferences: true) on Android
All methods handle PlatformException from flutter_secure_storage and rethrow as a typed SecureStorageException with a descriptive message
The class is exposed via a Riverpod Provider (secureSessionStorageProvider) for injection into other providers
Unit tests covering all public methods pass with a mocked FlutterSecureStorage

Technical Requirements

frameworks
flutter_secure_storage (^9.0.0)
Riverpod (Provider for DI registration)
dart:io (Platform.isIOS / Platform.isAndroid for platform-conditional options)
apis
FlutterSecureStorage.write(key, value, iOptions, aOptions)
FlutterSecureStorage.read(key, iOptions, aOptions)
FlutterSecureStorage.delete(key, iOptions, aOptions)
FlutterSecureStorage.deleteAll(iOptions, aOptions)
data models
StoredSession
SecureStorageKey enum
SecureStorageException (custom exception class)
performance requirements
readSession() must complete within 100ms on a mid-range device — Keychain/Keystore reads are fast but avoid multiple round-trips by reading all keys in parallel using Future.wait()
writeSession() must complete within 150ms — batch writes where the platform API allows
security requirements
IOSOptions accessibility must be KeychainAccessibility.firstUnlockThisDeviceOnly — this prevents access when device is locked and blocks iCloud Keychain backup of session tokens
encryptedSharedPreferences on Android uses AES-256-GCM with a key stored in Android Keystore hardware — do not downgrade to standard SharedPreferences as a fallback
clearSession() must clear ALL session keys including refreshToken — partial clears leave the session in an inconsistent state
Do not use deleteAll() in clearSession() — it would delete ALL app-stored secure data including potentially user preferences from other features
Never log token values — exception messages must redact token content

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Use Future.wait() to parallelize the multiple reads in readSession() rather than awaiting each sequentially — this reduces total read latency by ~60% on 5 keys. Pattern: 'final results = await Future.wait([storage.read(key: SecureStorageKey.sessionToken.key, ...), ...])'. Platform detection: use 'Platform.isIOS ? IOSOptions(...) : const IOSOptions()' — avoid dart:io imports in the domain layer by keeping platform logic inside this data-layer class only.

Define a private _iosOptions getter and _androidOptions getter to avoid repeating the options construction on every call. For the Riverpod provider, use a Provider at the app root — it is a stateless service, not a StateNotifier. Example: 'final secureSessionStorageProvider = Provider((ref) => SecureSessionStorage(const FlutterSecureStorage()))'. Create SecureStorageException as a simple class extending Exception with a message field and optional cause field for the original PlatformException.

Avoid using flutter_secure_storage's deleteAll() — it is a nuclear option. In the clearSession() method, explicitly delete each session-related key by name using a list: '[SecureStorageKey.sessionToken, SecureStorageKey.sessionExpiry, SecureStorageKey.userId, SecureStorageKey.refreshToken].forEach((key) => storage.delete(key: key.key, ...))'.

Testing Requirements

Write unit tests in test/src/features/auth/data/secure_session_storage_test.dart. Use mocktail to mock FlutterSecureStorage. Test cases: (1) writeSession calls storage.write for each field with correct key strings and platform options, (2) readSession returns StoredSession when all keys present and not expired, (3) readSession returns null when sessionToken key is absent, (4) readSession returns null when session is expired (expiry in the past), (5) clearSession calls storage.delete for all session keys, (6) writeBiometricPreference stores 'true'/'false' string correctly, (7) readBiometricPreference returns false when key absent, (8) readBiometricPreference parses 'true' and 'false' strings correctly, (9) PlatformException from storage is caught and rethrown as SecureStorageException. Run with flutter test test/src/features/auth/data/.

Platform-specific options (IOSOptions/AndroidOptions) should be verified by inspecting the captured write call arguments in mock expectations.

Component
Secure Session Storage
data low
Epic Risks (3)
high impact medium prob technical

iOS Keychain access requires correct entitlement configuration and provisioning profile setup. Misconfigured entitlements cause silent failures in CI/CD and on physical devices, where the plugin appears to work in the simulator but fails at runtime. This can delay foundation delivery and block all downstream epics.

Mitigation & Contingency

Mitigation: Add a dedicated integration test running on a physical iOS device early in the epic. Document required entitlements and provisioning steps in a developer runbook. Validate Keychain access in the CI pipeline using an iOS simulator with correct entitlements enabled.

Contingency: If Keychain entitlements cannot be resolved quickly, temporarily use in-memory storage behind the SecureSessionStorage interface to unblock downstream epics, then resolve the Keychain issue in a hotfix before release.

medium impact medium prob dependency

The Flutter local_auth plugin has a history of breaking API changes between major versions, and its Android implementation depends on BiometricPrompt which behaves differently across Android API levels (23-34). An incompatible plugin version or unexpected Android API behaviour can cause authentication failures on a significant portion of the target device fleet.

Mitigation & Contingency

Mitigation: Pin local_auth to a specific stable version in pubspec.yaml. Test against Android API levels 23, 28, and 33 in the CI matrix. Review the plugin changelog and migration guide before adopting any version bump.

Contingency: If the pinned version proves incompatible with target devices, evaluate flutter_local_auth_android as a replacement or fork the plugin adapter to isolate the breaking surface.

high impact low prob security

If users upgrade from a version of the app that stored session data in non-encrypted storage (SharedPreferences), a migration path is required. Failing to migrate silently leaves old tokens in plain storage, creating a security gap and potentially causing confusing authentication state on first launch of the new version.

Mitigation & Contingency

Mitigation: Audit existing storage usage across the codebase before writing SecureSessionStorage. If legacy plain storage keys exist, implement a one-time migration routine that reads from SharedPreferences, writes to Keychain/Keystore, and deletes the plain-text entry.

Contingency: If migration is discovered late, ship the migration as a mandatory patch release before the biometric feature is enabled for users, and add a startup check that blocks biometric opt-in until migration is confirmed complete.