high priority medium complexity backend pending backend specialist Tier 7

Acceptance Criteria

When the user selects biometric login and biometrics are enrolled, local_auth authenticates the user before any Supabase call is made — the token is never retrieved from Secure Storage without a successful biometric challenge.
After successful biometric authentication, the Supabase refresh token is retrieved from Secure Storage under the correct userId-scoped key.
The Auth Session Manager's `resumeSession(refreshToken)` is called; on success, the new access token and refresh token pair is written back to Secure Storage, replacing the old refresh token.
The auth state stream emits `Authenticated(user, trustLevel: biometric)` upon successful session resumption.
If the Supabase session is expired or revoked (`AuthException` with code 400/401), the service emits `BiometricFallbackRequired` and triggers the full BankID/Vipps re-authentication flow.
If the biometric challenge fails (user cancels, too many attempts, lockout), the service emits `BiometricChallengeFailed` without touching the refresh token.
If the refresh token is absent from Secure Storage (user cleared app data), the service emits `BiometricFallbackRequired` — it must not crash or emit an unhandled error.
On biometric lockout (LAErrorBiometryLockout on iOS / BiometricPrompt.ERROR_LOCKOUT on Android), a specific `BiometricLockedOut` error is emitted and the user is guided to re-authenticate with BankID/Vipps.
The access token rotation (new refresh token written to Secure Storage) must complete atomically — a partial write must not leave a stale token that causes an authentication loop.
Session resumption must complete within 2000ms on a 4G network connection.

Technical Requirements

frameworks
Flutter
Riverpod
BLoC
Dart
local_auth
flutter_secure_storage
apis
local_auth LocalAuthentication.authenticate()
flutter_secure_storage FlutterSecureStorage.read() / write()
Supabase Auth refreshSession(refreshToken)
Auth Session Manager resumeSession() — from task-002
data models
BiometricSessionResult (Authenticated | ChallengeFailed | FallbackRequired | LockedOut | TokenAbsent)
SupabaseSession (accessToken, refreshToken, expiresAt, userId)
AuthState stream (Authenticated | Unauthenticated | Loading | Error)
performance requirements
End-to-end session resumption (biometric prompt → Supabase refresh → token write) must complete within 2000ms on 4G
The biometric prompt must appear within 300ms of the user tapping the biometric login option
Refresh token write-back to Secure Storage must be awaited before emitting Authenticated state
security requirements
The refresh token must only be read AFTER a successful local_auth challenge — never pre-fetched speculatively
On biometric lockout, the refresh token must be cleared from Secure Storage to force full re-authentication
Token rotation: always write the new refresh token before invalidating the old one to prevent lockout on write failure
Biometric authentication must use stickyAuth: true to prevent the prompt from dismissing on app background/foreground
local_auth authenticate() must set biometricOnly: true to prevent PIN/passcode bypass of the biometric gate

Execution Context

Execution Tier
Tier 7

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.

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

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.

medium impact medium prob technical

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.

high impact medium prob integration

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.

medium impact high prob dependency

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.