critical priority low complexity backend pending backend specialist Tier 1

Acceptance Criteria

BiometricAuthService exposes a method Future<BiometricAuthResult> checkBiometricCapability() (or equivalent name agreed by team)
When flutter_local_auth reports canCheckBiometrics == false, the method returns BiometricAuthUnavailable(reason: BiometricUnavailableReason.hardwareNotSupported)
When flutter_local_auth reports canCheckBiometrics == true but getAvailableBiometrics() returns an empty list, the method returns BiometricAuthUnavailable(reason: BiometricUnavailableReason.notEnrolled)
When flutter_local_auth reports at least one available biometric type (face, fingerprint, iris), the method returns BiometricAuthSuccess (or a dedicated BiometricCapabilityAvailable type if the team prefers)
The method handles PlatformException from flutter_local_auth and maps it to BiometricAuthFailure with a sanitized errorMessage
LocalAuthIntegration is injected as an interface (not instantiated directly in BiometricAuthService) to support unit testing with a mock
The method is idempotent — calling it multiple times without state changes returns consistent results
No biometric challenge dialog is shown to the user during capability check — this is a silent query only
Unit tests cover all three capability states and the PlatformException error path

Technical Requirements

frameworks
Flutter
flutter_local_auth (flutter_local_auth_android + flutter_local_auth_darwin)
BLoC (flutter_bloc)
apis
flutter_local_auth: LocalAuthentication.canCheckBiometrics
flutter_local_auth: LocalAuthentication.getAvailableBiometrics
performance requirements
Capability check completes within 200ms on both iOS and Android — it is a synchronous OS query with no network I/O
security requirements
Biometric data never leaves the device — this method only queries OS capability flags
No capability result is logged to analytics or remote logging services
Per flutter_local_auth security guidance: cannot be used as sole authentication for first login — result must gate session re-authentication only, not initial BankID/Vipps login

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Define a LocalAuthIntegration abstract class (or interface) in lib/data/auth/local_auth_integration.dart that wraps LocalAuthentication from flutter_local_auth. BiometricAuthService depends on this abstraction, not the concrete plugin. This inversion is critical for testability — the real platform plugin cannot be invoked in flutter_test unit tests. The concrete LocalAuthIntegrationImpl is registered in the DI container (e.g., Riverpod provider or get_it) and used only at runtime.

Sequence: call canCheckBiometrics first (cheap), then getAvailableBiometrics only if true (slightly more expensive). Wrap both calls in try/catch PlatformException. Do not call isDeviceSupported() as an additional check — canCheckBiometrics already implies device support on modern flutter_local_auth versions.

Testing Requirements

Unit tests in test/services/biometric_auth_service_test.dart using a MockLocalAuthIntegration (mocktail). Test cases: (1) canCheckBiometrics returns false → BiometricAuthUnavailable(hardwareNotSupported); (2) canCheckBiometrics true + empty biometrics list → BiometricAuthUnavailable(notEnrolled); (3) canCheckBiometrics true + [BiometricType.face] → BiometricAuthSuccess or capability-available result; (4) canCheckBiometrics throws PlatformException → BiometricAuthFailure with non-empty errorMessage. Verify no real platform calls are made by asserting MockLocalAuthIntegration interaction counts. All four paths must be covered.

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.