high priority medium complexity backend pending backend specialist Tier 6

Acceptance Criteria

BiometricAuthService.requestStepUpAuth(reason: String) triggers a fresh biometric prompt regardless of existing session state
Returns StepUpAuthResult.granted when biometric succeeds, StepUpAuthResult.denied when biometric fails or is cancelled
Returns StepUpAuthResult.unavailable when biometric hardware is missing or not enrolled — caller must fall back to PIN or deny access
Step-up auth does NOT issue a new Supabase token refresh — it only verifies physical presence via biometric
A configurable step-up validity window (e.g., 5 minutes) prevents requiring re-verification on every navigation within a sensitive section
Sensitive screens (e.g., encrypted contact assignment views for Blindeforbundet) gate content behind StepUpAuthResult.granted before decrypting/displaying data
The reason string passed to requestStepUpAuth() appears in the native biometric dialog on both iOS and Android
Step-up auth result is NOT persisted to disk — it is an in-memory gate that resets on app backgrounding
Unit tests cover granted, denied, unavailable, and validity-window-still-valid paths
Accessibility: VoiceOver/TalkBack users can complete step-up auth via the native biometric dialog with full screen reader support

Technical Requirements

frameworks
Flutter
BLoC
flutter_local_auth
apis
flutter_local_auth (LocalAuthentication.authenticate with localizedReason)
BiometricAuthBloc (task-006)
data models
assignment
contact
confidentiality_declaration
performance requirements
Step-up validity window check is in-memory — sub-millisecond
No network calls triggered by step-up auth — purely local biometric verification
security requirements
Step-up auth result must reset when app enters background (AppLifecycleState.paused or inactive) to prevent shoulder-surfing exploitation
Biometric data never leaves the device — boolean result only consumed from local_auth
Cannot be used as sole authentication for first login — step-up is only valid when a base Supabase session already exists
Reason string passed to native dialog must not include sensitive data (e.g., the contact name) — use generic descriptions
Encrypted contact assignment data (Blindeforbundet) must not be cached unencrypted in memory after step-up expires — re-decrypt on next grant
ui components
EncryptedContactAssignmentView
StepUpAuthPromptOverlay

Execution Context

Execution Tier
Tier 6

Tier 6 - 158 tasks

Can start after Tier 5 completes

Implementation Notes

Implement requestStepUpAuth() as a separate method on BiometricAuthService distinct from the session-login authenticate() method — different semantics (presence proof vs. session establishment). Store the step-up grant as a DateTime? _stepUpGrantedAt field on the service instance (null = not granted, non-null = granted at that time).

In each requestStepUpAuth() call, first check if _stepUpGrantedAt is non-null and within the validity window — if so, return StepUpAuthResult.granted immediately. Register a WidgetsBindingObserver on the service to clear _stepUpGrantedAt when AppLifecycleState transitions to paused or inactive. Pass the localizedReason parameter to LocalAuthentication.authenticate() so the iOS/Android native dialog displays the context to the user. For Blindeforbundet's encrypted assignment views, document a clear pattern: decrypt content only after awaiting StepUpAuthResult.granted, and clear the decrypted data from memory when step-up expires.

Testing Requirements

Unit tests: step-up returns granted on biometric success, denied on failure/cancel, unavailable on hardware absence. Test validity window: a second requestStepUpAuth() call within 5 minutes returns granted immediately without re-prompting. Test validity window expiry: a call after window expires re-triggers biometric prompt. Test app-background reset: simulating AppLifecycleState.paused invalidates the step-up grant.

Integration test on device verifying the native dialog shows the reason string. Test that sensitive screen content is not rendered until StepUpAuthResult.granted is received.

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.