critical priority medium complexity backend pending backend specialist Tier 3

Acceptance Criteria

BiometricAuthService.authenticate() returns BiometricAuthSuccess sealed type when LocalAuth reports success
BiometricAuthService returns BiometricAuthFailure with retryCount incremented on each failed biometric match
BiometricAuthService returns BiometricAuthCancelled when user dismisses the native dialog without attempting
BiometricAuthService returns BiometricAuthLocked when LocalAuthException.permanentlyLockedOut or maxAttempts exceeded
BiometricAuthService returns BiometricAuthFallbackRequired when lockout state is reached, triggering PIN fallback flow
All outcome paths are covered by unit tests with mocked LocalAuthentication responses
No raw platform exceptions are surfaced to callers — all are mapped to sealed result types
Retry count state resets to zero on successful authentication
Outcome mapping logic is pure and does not trigger side effects (no Supabase calls in this layer)
iOS-specific LAError codes (e.g., LAErrorAuthenticationFailed, LAErrorUserCancel, LAErrorBiometryLockout) are all mapped
Android-specific BiometricPrompt error codes are all mapped to the same sealed types

Technical Requirements

frameworks
Flutter
flutter_local_auth (local_auth package)
BLoC
apis
flutter_local_auth iOS LocalAuthentication
flutter_local_auth Android BiometricPrompt
performance requirements
Outcome mapping must complete synchronously after receiving LocalAuth result — no additional async work
No memory allocations beyond sealed result object construction per call
security requirements
Biometric data never leaves the device — only boolean pass/fail consumed from local_auth
Raw platform error messages must not be surfaced in sealed result payloads to avoid leaking OS internals
Retry count must be stored in-memory only (not persisted) to avoid attack surface
Fallback-to-PIN path must not bypass Supabase session validation

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Use a switch expression on the LocalAuthException type (or error code integer) to produce sealed results — avoid if/else chains. Define a private _mapPlatformError(Object error) helper that centralises all platform-to-domain mapping. Maintain a final int _retryCount field on the service instance (reset in the success path). The fallback-required state should only be emitted after the lockout threshold is crossed, not on the first failure.

Ensure the cancellation case distinguishes between user-initiated cancel and system-initiated cancel (e.g., incoming call on iOS) — map both to BiometricAuthCancelled but include a CancellationReason enum field. Reference the sealed class hierarchy from task-001 to ensure result types are used exactly as defined.

Testing Requirements

Unit tests required for all 5 outcome paths (success, failure, cancelled, locked, fallback-required) using MockLocalAuthentication. Test that retry counter increments correctly on successive failures and resets on success. Test that all iOS LAError codes and Android BiometricPrompt error codes map to the correct sealed type. Minimum 95% branch coverage on the outcome mapping switch/if tree.

No integration tests needed at this layer — the mapping is pure logic.

Component
Biometric Authentication Service
service medium
Epic Risks (3)
high impact medium prob technical

Multiple concurrent callers (e.g., SessionResumeManager and a background sync service) could simultaneously detect a near-expired token and each invoke SupabaseSessionManager.refreshSession(), causing duplicate refresh API calls and potentially a token invalidation race condition on the Supabase Auth server. This can result in one caller receiving a valid refreshed token while another receives a 401, causing intermittent authentication failures.

Mitigation & Contingency

Mitigation: Implement a single-flight pattern inside SupabaseSessionManager so that concurrent refresh calls coalesce into one in-flight request. Use a Dart Completer or AsyncMemoizer to ensure all waiters receive the same refreshed token. Write a concurrent integration test to validate the single-flight behaviour.

Contingency: If the single-flight pattern introduces deadlocks or timeout complexity, fall back to a mutex-based lock with a 10-second timeout, logging a warning if the lock is held longer than expected, and triggering a full re-login if the refresh ultimately fails.

high impact low prob security

Supabase Row-Level Security policies evaluate the JWT claims (user_id, role, org_id) on every query. If the refreshed token contains stale or changed claims — for example if a coordinator's role was updated server-side — RLS may silently block data access even though the session appears valid from the client's perspective, causing confusing empty screens rather than an authentication error.

Mitigation & Contingency

Mitigation: After every token refresh, decode the new JWT and compare key claims (role, org_id) with the cached values. If claims have changed, emit a session-claims-changed event that triggers a role re-resolution and navigation reset. Document this behaviour in the SupabaseSessionManager API contract.

Contingency: If claims drift is detected in production and causes data visibility issues, provide a force-refresh mechanism in the UI (pull-to-refresh on home screen) that clears cached role state and re-fetches from Supabase, accompanied by a user-visible toast indicating the session was refreshed.

medium impact medium prob security

Allowing session resumption from cached local token when offline introduces a window where a revoked or invalidated session can still grant app access. For example, if a coordinator deactivates a peer mentor's account while the mentor is offline, the mentor continues to have access until connectivity is restored and the token is validated server-side.

Mitigation & Contingency

Mitigation: Set a maximum offline grace period (e.g., 24 hours) stored alongside the token in SecureSessionStorage. If the grace period is exceeded, force a full credential re-login regardless of connectivity status. Scope offline access to read-only operations only, requiring connectivity for any write that reaches Supabase.

Contingency: If the offline grace period logic is found to be insufficient for compliance, implement remote session invalidation via a lightweight push notification that clears SecureSessionStorage even when the app is backgrounded, using FCM with a data-only message.