critical priority medium complexity backend pending backend specialist Tier 0

Acceptance Criteria

AuthSessionManager class is implemented in Dart with a clean public API: storeSession(AuthSession), getSession(), clearSession(), and isSessionValid() methods
AuthSession model contains: accessToken (String), refreshToken (String), expiresAt (DateTime), userId (String), orgId (String), and roles (List<String>)
storeSession() persists all session fields to SecureStorageAdapter under namespaced keys (e.g., 'auth_session_access_token')
getSession() reconstructs a full AuthSession from SecureStorage or returns null if any required field is missing
isSessionValid() returns false when expiresAt is in the past or within a configurable grace period (default: 60 seconds)
isSessionValid() returns false when getSession() returns null
clearSession() removes all session keys from SecureStorage and emits a session-cleared event via a Riverpod StateNotifier or stream
AuthSessionManager is exposed via a Riverpod provider (authSessionManagerProvider) for dependency injection
A sessionStateProvider emits AuthSessionState (authenticated, unauthenticated, loading) for UI consumption
JWT expiry is parsed from the token payload (base64 decoded exp claim) as a fallback when expiresAt is not explicitly set
All methods are null-safe and handle SecureStorage read/write failures gracefully with typed exceptions (AuthStorageException)
Unit tests cover all public methods with mocked SecureStorageAdapter

Technical Requirements

frameworks
Flutter
Riverpod
apis
flutter_secure_storage (via SecureStorageAdapter abstraction)
dart:convert (base64 decoding of JWT payload)
data models
AuthSession
AuthSessionState
AuthToken
performance requirements
storeSession() and getSession() must complete in under 100ms on mid-range devices
isSessionValid() must be synchronous (no async I/O) — compute from cached in-memory state after initial load
In-memory cache of current session must be maintained to avoid repeated SecureStorage reads on every isSessionValid() call
security requirements
Access token and refresh token must never be stored in SharedPreferences, Hive, or any unencrypted storage — only SecureStorageAdapter
JWT payload must not be logged, even at debug level
Session keys must be namespaced to prevent collision with other plugins using flutter_secure_storage
clearSession() must be called on logout, user switch, and app uninstall detection (where possible)
ui components
sessionStateProvider consumers (splash screen, route guard widget)

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

This is the foundation for all auth services — design the public interface carefully as it will be hard to change later. Use an in-memory AuthSession? _cachedSession field to serve isSessionValid() synchronously after the first getSession() call. Invalidate the cache in clearSession().

For JWT expiry parsing, use a private _parseJwtExpiry(String token) helper that base64-decodes the payload segment (token.split('.')[1]) and extracts the 'exp' Unix timestamp — handle padding with base64Url.normalize() before decoding. The configurable grace period should be a constructor parameter with a default of const Duration(seconds: 60) to allow tests to override it. Define AuthSessionState as a sealed class or enum (loading, authenticated, unauthenticated, error) for exhaustive pattern matching in route guards. The Riverpod provider should be an AsyncNotifier that loads the initial session from SecureStorage in its build() method.

Ensure clearSession() is idempotent — calling it twice should not throw.

Testing Requirements

Write unit tests using flutter_test and mocktail. Mock SecureStorageAdapter at the constructor level. Test cases: (1) storeSession writes all fields to correct namespaced keys, (2) getSession returns correct AuthSession when all keys present, (3) getSession returns null when accessToken key missing, (4) isSessionValid returns true for token expiring in 10 minutes, (5) isSessionValid returns false for already-expired token, (6) isSessionValid returns false within 60-second grace period, (7) clearSession removes all keys and emits unauthenticated state, (8) JWT exp claim parsing correctly extracts expiry when expiresAt is null, (9) SecureStorage write failure throws AuthStorageException with informative message, (10) Concurrent storeSession and clearSession calls do not leave partial state. Target 95% line coverage on AuthSessionManager.

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.