critical priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

AuthSessionManager exposes a refreshSessionIfNeeded() method that is safe to call before any authenticated API request
refreshSessionIfNeeded() detects tokens expiring within a configurable proactive refresh window (default: 5 minutes before expiry)
refreshSessionIfNeeded() calls the Supabase Auth refreshSession() API using the stored refresh token when within the refresh window
On successful refresh, new accessToken, refreshToken, and expiresAt values are atomically written to SecureStorage in a single transaction-like operation
In-memory session cache is updated atomically with the new tokens before the method returns
If the Supabase refresh call fails with a network error, the method retries once after a 2-second delay before propagating the error
If the Supabase refresh call fails with an auth error (invalid or expired refresh token), clearSession() is called and an AuthSessionExpiredException is thrown
A background refresh timer is started when a session is stored, scheduling a refresh at (expiresAt - refreshWindow - 30 seconds)
The background timer is cancelled when clearSession() is called
No concurrent refresh calls are triggered — if a refresh is already in progress, subsequent calls await the same Future rather than starting a new request
refreshSessionIfNeeded() is a no-op (returns immediately) if the session is not within the refresh window
Unit tests cover all refresh scenarios: proactive refresh triggered, no refresh needed, network failure retry, auth failure invalidation, concurrent call deduplication

Technical Requirements

frameworks
Flutter
Riverpod
Supabase
apis
Supabase Auth refreshSession() API
Dart Timer for background scheduling
Dart async/Completer for concurrent call deduplication
data models
AuthSession
AuthSessionState
AuthToken
performance requirements
refreshSessionIfNeeded() must add less than 50ms overhead on the hot path when no refresh is needed (synchronous check only)
Refresh API call must complete within 5 seconds or be considered a network failure
Atomic token write must use a lock or serialized queue to prevent partial updates under concurrent callers
security requirements
Refresh token must never be included in logs or error messages
After a refresh, the old refresh token must be overwritten immediately — never leave stale refresh tokens in SecureStorage
Auth failure during refresh must immediately invalidate the session to prevent use of an expired access token for subsequent requests
The background refresh timer must not fire after the user has logged out — always cancel in clearSession()
ui components
SessionExpiredDialog (shown when AuthSessionExpiredException propagates to UI layer)
AuthSessionState consumers that handle the unauthenticated state from session invalidation

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Implement concurrent call deduplication using a Completer? _pendingRefresh field: if _pendingRefresh is not null, return _pendingRefresh.future instead of starting a new request. Complete the Completer (with or without error) when the refresh finishes and set it back to null. This is a critical correctness requirement — without it, rapid API calls near token expiry could trigger multiple parallel refresh requests, causing a race condition that invalidates the refresh token chain.

For the background timer, use Timer(refreshDeadline.difference(DateTime.now()), _backgroundRefresh) where refreshDelay = expiresAt.subtract(refreshWindow).subtract(const Duration(seconds: 30)). Guard against negative durations (token already in refresh window at login time) by clamping to Duration.zero. The atomic write of new tokens should call SecureStorageAdapter methods in a specific order (refreshToken first, then accessToken, then expiresAt) so that if a partial write occurs and the app restarts, getSession() will see a missing or stale accessToken and trigger a fresh login rather than using a corrupted partial state. Document this ordering decision with a code comment.

Testing Requirements

Write unit tests using flutter_test and mocktail. Mock both the SecureStorageAdapter and the Supabase Auth client. Test cases: (1) token with 4 minutes remaining triggers refresh (within 5-min window), (2) token with 10 minutes remaining does not trigger refresh, (3) successful refresh updates in-memory cache and SecureStorage with new tokens, (4) network error on first attempt triggers exactly one retry after 2 seconds, (5) network error on retry propagates as NetworkRefreshException, (6) auth error (e.g., 400 from Supabase) calls clearSession() and throws AuthSessionExpiredException, (7) two concurrent calls to refreshSessionIfNeeded() result in exactly one Supabase API call (deduplication), (8) background timer fires at correct time relative to expiresAt, (9) clearSession() cancels the background timer, (10) calling refreshSessionIfNeeded() after clearSession() is a no-op. Use fake timers (FakeAsync) to test timer behavior deterministically without real time delays.

Component
Authentication Session Manager
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.