critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

LocalAuthIntegration exposes an abstract interface (ILocalAuthIntegration or LocalAuthIntegration as abstract class) so tests can inject a mock
isAvailable() calls LocalAuthentication.isDeviceSupported() and .canCheckBiometrics, returning true only if both are true
getAvailableBiometrics() calls LocalAuthentication.getAvailableBiometrics() and maps each local_auth BiometricType to the domain BiometricType enum; unknown local_auth types are dropped from the list
authenticate(localizedReason) calls LocalAuthentication.authenticate with useErrorDialogs: true, stickyAuth: true and the provided reason string
On successful authentication, authenticate returns Result.success(null) (or equivalent Ok/Right)
On any exception thrown by the local_auth package, authenticate catches it, passes it to mapLocalAuthException, and returns Result.failure(localAuthFailure)
iOSAuthStrings and AndroidAuthStrings are not hardcoded; localizedReason parameter is forwarded as the biometricHint / localizedReason
Domain BiometricType enum covers at minimum: face, fingerprint, iris, unknown
The concrete implementation class is not directly instantiated by consumers — used only through its interface

Technical Requirements

frameworks
Flutter
Riverpod
local_auth package
apis
LocalAuthentication.isDeviceSupported()
LocalAuthentication.canCheckBiometrics
LocalAuthentication.getAvailableBiometrics()
LocalAuthentication.authenticate()
data models
BiometricType (domain enum)
LocalAuthFailure (sealed class)
Result<void, LocalAuthFailure>
performance requirements
isAvailable() must complete within 300ms on a mid-range device
getAvailableBiometrics() must not block the UI thread — always awaited from an async context
authenticate() must not set a timeout — the OS controls the authentication dialog lifetime
security requirements
stickyAuth: true must be set so authentication is not dismissed on app backgrounding (important for Face ID which requires the user to re-look)
localizedReason must never be empty — caller must provide a non-empty string; validate and throw ArgumentError if blank
Do not store or log the outcome of individual authentication attempts

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Define an abstract class ILocalAuthIntegration in the domain layer, and place the concrete LocalAuthIntegrationImpl in the infrastructure/platform layer. This way BiometricAuthService depends only on the interface and the Riverpod provider can swap in a fake during testing. Use the Result type already established in task-002 — do not introduce a second Result type. For the BiometricType mapping, use a switch expression (Dart 3) for exhaustive mapping, with a default returning null, then filter nulls out of the list.

Be careful on iOS: canCheckBiometrics can return true even when Face ID is not enrolled but hardware is present — isDeviceSupported() is the more reliable gate. On Android, canCheckBiometrics covers both fingerprint and face sensors.

Testing Requirements

Unit tests using flutter_test with a mocked LocalAuthentication (implement using mockito or manual fake). Cover: (1) isAvailable returns false when isDeviceSupported returns false, (2) isAvailable returns false when canCheckBiometrics returns false, (3) isAvailable returns true when both return true, (4) getAvailableBiometrics maps BiometricType.face to domain face, (5) getAvailableBiometrics maps BiometricType.fingerprint to domain fingerprint, (6) getAvailableBiometrics drops unknown types, (7) authenticate returns Result.success on true return from local_auth, (8) authenticate returns Result.failure(LocalAuthFailure.cancelled) when PlatformException with cancelled code is thrown, (9) each exception type maps to the expected domain failure. All tests must be pure Dart (no device required).

Component
Local Auth Integration
infrastructure 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.