critical priority high complexity integration pending integration specialist Tier 3

Acceptance Criteria

PKCE code verifier generated as a cryptographically random 43–128 character string using only unreserved URI characters (A-Z, a-z, 0-9, -, ., _, ~)
Code challenge computed as BASE64URL(SHA-256(ASCII(code_verifier))) with no padding characters
Authorization URL constructed includes: response_type=code, client_id, redirect_uri, scope=openid+phoneNumber+address+nin, state (random anti-CSRF token), code_challenge, code_challenge_method=S256
Code verifier stored in flutter_secure_storage under a namespaced key (e.g., vipps_pkce_verifier) before URL launch — never in SharedPreferences
Anti-CSRF state parameter stored in flutter_secure_storage alongside the verifier
Deep link launched via url_launcher or app_links to the Vipps authorization endpoint; falls back to browser if Vipps app not installed
AuthState transitions to loading immediately upon flow initiation
If url_launcher fails to open the Vipps app, AuthState transitions to error with a user-friendly message
Code verifier and state are deleted from secure storage if the flow is cancelled or times out (30-second timeout)
Phone number, NIN, and address scopes are requested only with prior user consent prompt displayed before redirect
Unit tests verify code verifier entropy, challenge computation, and URL construction

Technical Requirements

frameworks
Flutter
Riverpod
BLoC
flutter_secure_storage
url_launcher
dart:convert
dart:math
apis
Vipps Login API OAuth 2.0 authorization endpoint
Supabase Auth
performance requirements
Code verifier and challenge generation completes in under 50ms
Deep link launch within 200ms of user tapping 'Login with Vipps'
No blocking main thread: all crypto operations on isolate if >10ms
security requirements
Code verifier generated using dart:math Random.secure() — never Math.random() or timestamp-based seeds
Code verifier stored exclusively in flutter_secure_storage (iOS Keychain / Android Keystore) — never in SharedPreferences, memory cache, or local files
Anti-CSRF state parameter must be a separate random value from the code verifier
PKCE challenge_method must be S256 — plain method must never be used
NIN scope (nin) requires documented lawful basis per GDPR — consent dialog must be shown before including nin in scope
Authorization URL must use HTTPS — reject any http:// redirect or endpoint URL
Vipps session tokens must not be persisted beyond token exchange
ui components
ConsentDialog (pre-scope-request)
LoadingOverlay (during redirect)

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Use the crypto package (dart) for SHA-256 hashing. The BASE64URL encoding must strip padding ('=') characters. Store both the code_verifier and the state nonce in flutter_secure_storage under versioned keys (e.g., 'vipps_pkce_v1_verifier') to allow future key rotation without collisions. Consider wrapping the Vipps PKCE logic in a dedicated VippsPkceGenerator class for testability.

The redirect_uri must match exactly what is registered in the Vipps Developer Portal — use a custom scheme (e.g., com.yourapp://vipps-callback) registered in AndroidManifest.xml and Info.plist. Implement a 30-second timer that fires AuthState.error if the callback is not received; cancel the timer upon successful callback receipt. Always emit AuthState.loading before launching the deep link so the UI disables the login button and prevents double-taps. For the consent dialog, display which data Vipps will share (phone, address, national ID) and require explicit acknowledgement before proceeding.

Testing Requirements

Unit tests (flutter_test): verify code verifier length (43–128 chars), character set compliance, and that two consecutive calls produce distinct verifiers. Verify S256 challenge matches known test vectors (RFC 7636 Appendix B). Verify URL construction includes all required parameters. Verify flutter_secure_storage.write() is called before url_launcher.launch().

Mock flutter_secure_storage and url_launcher for isolation. Integration test: run the full initiation on a device/emulator, verify the Vipps deep link opens (or browser fallback). Security test: verify that code_challenge_method=S256 is always set, verify plain method is rejected, verify NIN scope is absent if consent is denied. Timeout test: simulate 31-second delay and verify secure storage is cleared.

Target 90%+ branch coverage.

Component
Vipps Authentication Service
service high
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.