critical priority low complexity backend pending backend specialist Tier 2

Acceptance Criteria

Class AuthTokenStore is defined in lib/core/auth/auth_token_store.dart
saveTokens(AuthTokens tokens) → Future<void> writes access_token, refresh_token, and token_expiry (ISO 8601 string) to SecureStorageAdapter
getTokens() → Future<AuthTokens?> reads and deserializes all three fields; returns null if any required field is missing
clearTokens() → Future<void> deletes all three keys from SecureStorageAdapter
isTokenExpired() → Future<bool> compares stored token_expiry against DateTime.now().toUtc(); returns true if missing or past
AuthTokens value object is defined with final fields: accessToken, refreshToken, expiresAt (DateTime)
All SecureStorageKey values used by AuthTokenStore are defined in the SecureStorageKey enum (access_token, refresh_token, token_expiry)
AuthTokenStore is exposed as a Riverpod Provider<AuthTokenStore> using ref.watch(secureStorageAdapterProvider) for its dependency
Unit tests cover: saveTokens + getTokens round-trip; clearTokens makes getTokens return null; isTokenExpired returns true for past timestamp; isTokenExpired returns false for future timestamp; isTokenExpired returns true when token_expiry key is absent
dart analyze passes with zero issues

Technical Requirements

frameworks
Flutter
Riverpod
flutter_test
apis
SecureStorageAdapter interface (internal)
data models
AuthTokens (value object)
SecureStorageKey (enum)
performance requirements
isTokenExpired must not make a network call — reads only from local secure storage
DateTime parsing must use DateTime.parse() with UTC normalization to avoid timezone bugs
security requirements
Tokens must never be stored as plain class fields or in-memory singletons outside SecureStorageAdapter — all persistence goes through the adapter
expiresAt must be stored as UTC ISO 8601 to prevent timezone-related early/late expiry decisions
clearTokens must be called on logout — AuthTokenStore must not retain any in-memory copy of tokens after clear

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Keep AuthTokenStore as a simple, non-reactive class (no Stream, no StateNotifier) — it is a persistence helper, not a state manager. Authentication state reactivity belongs in a separate AuthStateNotifier (different epic). Token expiry: store as expiresAt.toUtc().toIso8601String() and parse with DateTime.parse(...).toUtc() on read. Add a private _bufferSeconds constant (default 60) so isTokenExpired considers a token expiring within the next 60 seconds as already expired — this prevents races during token refresh.

The Riverpod provider should be a simple Provider (not StateProvider or AsyncNotifierProvider) since AuthTokenStore has no reactive state. Place provider registration in lib/core/auth/auth_providers.dart.

Testing Requirements

Unit tests using flutter_test with a FakeSecureStorageAdapter (in-memory Map from task-001). Test file at test/core/auth/auth_token_store_test.dart. Required scenarios: (1) saveTokens followed by getTokens returns identical AuthTokens; (2) clearTokens followed by getTokens returns null; (3) isTokenExpired returns false when expiresAt is one hour in the future; (4) isTokenExpired returns true when expiresAt is one hour in the past; (5) isTokenExpired returns true when no token_expiry key exists; (6) getTokens returns null when only access_token is present (partial write scenario). No mocking framework needed — inject FakeSecureStorageAdapter directly.

Component
Auth Token Store
data low
Epic Risks (3)
high impact medium prob technical

Flutter Secure Storage behavior differs between iOS Keychain and Android Keystore — key accessibility attributes (kSecAttrAccessibleWhenUnlocked vs. WhenUnlockedThisDeviceOnly) may cause tokens to become inaccessible after device restart or OS upgrade, breaking session restoration for returning users.

Mitigation & Contingency

Mitigation: Define explicit Keychain accessibility attributes during implementation and write integration tests on both platforms. Follow flutter_secure_storage documentation for cross-platform accessibility configuration.

Contingency: Implement a recovery flow that detects secure storage read failures and falls back to full re-authentication rather than crashing. Add a migration utility to re-write tokens with corrected attributes if a misconfiguration is discovered post-release.

high impact medium prob security

Personnummer is a legally sensitive national identifier under Norwegian GDPR implementation. If encryption-at-rest or data minimization requirements are not met before launch, the feature could be blocked by legal/compliance review from any of the four partner organizations.

Mitigation & Contingency

Mitigation: Ensure personnummer is only persisted after explicit user consent via the personnummer confirmation widget. Use Supabase column-level encryption for the personnummer field. Document the data processing basis and retention policy before the first TestFlight release.

Contingency: If legal review blocks the personnummer write-back, implement the feature as opt-in only with a deferred sync model, allowing BankID/Vipps login to proceed without storing the personnummer until compliance is confirmed.

medium impact high prob dependency

If the VippsOrgCostConfig data is not populated in Supabase for all four partner organizations before the feature ships, users from unconfigured organizations will see no Vipps login option and may report it as broken, creating confusion and support load.

Mitigation & Contingency

Mitigation: Create a seed migration script for Vipps org configuration and include it in the deployment checklist. Implement a clear admin UI warning when an organization is missing Vipps configuration.

Contingency: Add a feature flag in VippsOrgCostConfig so individual organizations can be enabled/disabled without a code deploy, allowing rapid remediation.