high priority high complexity backend pending backend specialist Tier 7

Acceptance Criteria

When device has no connectivity, BiometricAuthService checks cached JWT expiry from flutter_secure_storage before proceeding
If cached JWT is not expired, offline biometric auth succeeds and returns BiometricAuthSuccess(offlineMode: true) without a Supabase call
If cached JWT is expired while offline, BiometricAuthService returns BiometricAuthSessionExpired with an offlineExpired flag so UI can show an appropriate message
A pending session refresh is queued (e.g., via a WorkManager-equivalent or app lifecycle listener) to execute when connectivity resumes
When connectivity resumes, the queued refresh executes automatically and updates the stored JWT without user interaction
If the queued refresh fails after connectivity restoration (e.g., refresh token revoked server-side), user is redirected to full re-login
Offline mode is detected via connectivity_plus package (or equivalent) — not by catching SocketExceptions reactively
Offline biometric auth works in Airplane Mode on both iOS and Android (validated by manual test)
Peer mentors in low-connectivity areas do not receive unexpected logout screens during normal app use
Unit tests cover: offline + valid JWT → success, offline + expired JWT → session expired, online → normal flow unchanged

Technical Requirements

frameworks
Flutter
BLoC
connectivity_plus
flutter_secure_storage
apis
connectivity_plus (ConnectivityResult)
flutter_secure_storage (JWT read)
Supabase Auth SDK (deferred refresh)
WorkManager or app lifecycle WidgetsBindingObserver for deferred tasks
performance requirements
Connectivity check must complete within 100ms — use cached ConnectivityResult, not a live probe
JWT expiry check from flutter_secure_storage must complete synchronously or within 50ms
Offline auth path must add no more than 200ms overhead compared to the online path
security requirements
Offline biometric auth must only be permitted within a configurable max offline window (e.g., 72 hours) to prevent indefinite offline access
Cached JWT expiry is checked against device clock — note that device clock manipulation is a risk; use server-issued iat/exp claims only
Biometric data never leaves the device — only boolean result used from local_auth even in offline mode
Pending refresh queue must not expose session tokens in plaintext — use flutter_secure_storage for any queued state
If a compromised device is reported, server-side session revocation must be checked on next connectivity — offline grace period must not bypass revocation

Execution Context

Execution Tier
Tier 7

Tier 7 - 84 tasks

Can start after Tier 6 completes

Implementation Notes

Inject a ConnectivityService abstraction (wrapping connectivity_plus) into BiometricAuthService for testability. Parse JWT expiry (exp claim) from the stored token in flutter_secure_storage using dart:convert + base64 decode of the payload segment — no external JWT library needed for just reading claims. For the deferred refresh queue, use WidgetsBindingObserver.didChangeAppLifecycleState to trigger the refresh when the app resumes to foreground with connectivity. Alternatively, use a simple Completer stored on the service that the app's ConnectivityService resolves when online.

Define a maxOfflineDurationHours constant (default 72) sourced from remote config or hardcoded — document the security trade-off. The offline success result should be clearly differentiated (BiometricAuthSuccess with an offlineMode bool) so callers can optionally display a 'working offline' banner.

Testing Requirements

Unit tests with mocked connectivity_plus stream covering: online path unchanged, offline + valid JWT → offline success, offline + expired JWT → session expired result. Integration test on a physical device with Airplane Mode enabled. Test the deferred refresh queue: mock connectivity restored event triggers refreshSession() call. Test the max offline window: mock a JWT with iat > 72 hours ago → offline auth denied even if not expired.

Test that queued refresh failure on reconnect triggers re-login flow.

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.