Implement biometric Supabase session resumption
epic-bankid-vipps-login-services-task-012 — Build the Biometric Authentication Service session resumption path: authenticate the user via local_auth, retrieve the persisted Supabase refresh token from Secure Storage, call the Auth Session Manager to resume the session using the refresh token, handle expired or revoked tokens by falling back to full BankID/Vipps re-authentication, and update the auth state stream on success.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 7 - 84 tasks
Can start after Tier 6 completes
Implementation Notes
Implement `resumeWithBiometrics(String userId)` on `BiometricAuthServiceImpl`. The flow is strictly sequential: (1) `localAuth.authenticate(localizedReason: ..., options: AuthenticationOptions(biometricOnly: true, stickyAuth: true))`; (2) on success, `secureStorage.read(key: 'supabase_refresh_token_$userIdHash')`; (3) `authSessionManager.resumeSession(refreshToken)`; (4) on success, write new tokens, emit Authenticated; (5) on AuthException, determine if expired/revoked → emit FallbackRequired or other specific error. Use a sealed class `BiometricSessionResult` for all return states to force exhaustive handling at the BLoC layer. The BLoC handles `ResumeBiometricSessionEvent` and maps `BiometricSessionResult` to app-level `AuthState`.
Ensure `biometricOnly: true` is set — the accessibility fallback to PIN/passcode is a product decision, not a default behavior. Coordinate with the Auth Method Selector Screen (task-001 in UI epic) for the fallback navigation trigger.
Testing Requirements
Unit tests for session resumption: (1) biometric challenge succeeds, token present → session refreshed, Authenticated state emitted, new token written to Secure Storage; (2) biometric challenge fails (user cancels) → BiometricChallengeFailed emitted, Secure Storage NOT read; (3) token absent in Secure Storage → BiometricFallbackRequired emitted without crash; (4) Supabase refreshSession returns 401 → BiometricFallbackRequired emitted, fallback BankID flow triggered; (5) biometric lockout exception → BiometricLockedOut emitted, token cleared from Secure Storage; (6) token rotation — new refresh token written before old one is overwritten. Mock local_auth and flutter_secure_storage. Verify state stream emissions in order using BLoC test package. Target 90% coverage on the session resumption path.
The PKCE OAuth flow requires the code verifier to survive an app backgrounding during the Vipps redirect, which can trigger OS memory pressure and clear in-memory state. If the verifier is lost between authorization request and callback, the token exchange fails and the user is stranded with a confusing error.
Mitigation & Contingency
Mitigation: Store the PKCE code verifier in AuthTokenStore (Flutter Secure Storage) immediately after generation, before launching the Vipps redirect. Clear it only after a successful or explicitly failed token exchange.
Contingency: If state loss occurs in production, implement a retry flow that generates a new PKCE pair and restarts the authorization URL request, with a user-visible 'Try again' prompt rather than a generic error.
Resuming a Supabase session after biometric verification requires the session token to still be valid. If the session has expired in the background (e.g., after a long device offline period), biometric success will not produce a valid session, and the user will see a confusing 'Face ID worked but still logged out' experience.
Mitigation & Contingency
Mitigation: Before presenting the biometric prompt, check session token expiry. If expired, skip biometrics and route directly to full BankID/Vipps re-authentication. Only offer biometric re-auth if the stored refresh token is still within its validity window.
Contingency: If session expiry during biometric flow occurs in production, implement a graceful transition message ('Your session has expired — please log in again') that preserves the user's last-used authentication method preference.
BankID and Vipps may return different user identifiers (personnummer, phone number, sub claim) that must be correctly linked to an existing Supabase auth user. If the linking logic has edge cases (e.g., user previously registered via email/password), duplicate Supabase accounts may be created.
Mitigation & Contingency
Mitigation: Design the identity linking logic with explicit disambiguation: check for existing users by personnummer before creating a new Supabase identity. Implement the linking via Supabase Edge Function to keep the logic server-side and auditable.
Contingency: Implement an admin-facing account merge tool in the admin portal to resolve duplicate accounts if they occur. Add a Supabase unique constraint on the personnummer field to make duplicates fail loudly rather than silently.
The Vipps nin (personnummer) scope requires explicit approval from Vipps as part of the merchant agreement. If this scope approval is not in place before the production release, the Vipps flow will succeed but return no personnummer, making the primary business value (membership data gap fill) non-functional without user-visible error.
Mitigation & Contingency
Mitigation: Apply for Vipps nin scope approval as part of the merchant onboarding process, well before Phase 2 launch. Implement the service to gracefully handle absent nin claims and show users a clear message if personnummer could not be retrieved.
Contingency: If nin scope is delayed, ship the Vipps login flow without personnummer write-back first (delivering login value immediately) and add personnummer sync as a post-approval update with no UI changes required.