critical priority high complexity integration pending integration specialist Tier 4

Acceptance Criteria

Deep link callback URI is captured via app_links or uni_links and routed to VippsAuthService
Received state parameter is compared to the value stored in flutter_secure_storage; mismatched state causes immediate AuthState.error and clears storage
Authorization code and code verifier are sent to the Vipps token endpoint via HTTPS POST with correct Content-Type and PKCE parameters
Token exchange response is validated: access_token, id_token, and token_type=Bearer must be present
ID token signature is validated against Vipps JWKS endpoint (or delegated to server-side Edge Function)
ID token claims validated: iss (issuer), aud (audience matches client_id), exp (not expired), iat (not in future)
Personnummer (NIN) extracted from the id_token sub or nin claim and held in memory only — never logged
Phone number and address claims extracted if present in the token payload
Code verifier and state nonce deleted from flutter_secure_storage immediately after successful token exchange
On token exchange failure (network error, invalid_grant, etc.), AuthState.error is emitted with a typed error code
On user cancellation of Vipps flow (error=access_denied in callback), AuthState.unauthenticated is emitted (not error)
Entire callback processing completes within 5 seconds; timeout triggers AuthState.error

Technical Requirements

frameworks
Flutter
Riverpod
BLoC
flutter_secure_storage
app_links
http or dio
apis
Vipps Login API token endpoint
Vipps JWKS endpoint (for ID token validation)
Supabase Edge Functions (server-side JWKS validation option)
performance requirements
Token exchange HTTP request completes within 3 seconds under normal network conditions
JWKS validation cached for 1 hour to avoid repeated network calls
Deep link to callback processing pipeline under 500ms excluding network I/O
security requirements
State parameter validation is mandatory — skip-validation code paths must not exist
ID token MUST be validated (signature + claims) — accepting unvalidated tokens is a critical security flaw
Personnummer must not appear in logs, crash reports, or analytics events at any point
Token exchange request must be made over TLS; certificate pinning recommended for the Vipps endpoint
Access token and refresh token stored in flutter_secure_storage if persistence is needed; never in plain SharedPreferences
GDPR: NIN retrieved from Vipps requires documented lawful basis and DPA — ensure legal team has signed off before enabling nin scope
Vipps session tokens short-lived — do not persist beyond the duration of the Supabase session establishment in task-006
ui components
LoadingOverlay (during token exchange)
ErrorSnackbar (on failure)

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Use app_links package to capture the deep link callback — register the custom URI scheme in both AndroidManifest.xml (intent-filter) and Info.plist (CFBundleURLTypes). Parse the callback URI query parameters using Uri.parse().queryParameters. The token exchange POST body must use application/x-www-form-urlencoded encoding with: grant_type=authorization_code, code, redirect_uri, client_id, code_verifier. For ID token validation, prefer delegating JWKS signature verification to a Supabase Edge Function (Deno) to keep the private validation logic server-side and avoid exposing JWKS parsing bugs to the client.

Cache the JWKS response in memory for 1 hour using a simple TTL map. NIN extraction: check both the 'nin' claim and the 'sub' claim (Vipps may encode it differently per environment). Hold the personnummer as a final String in a sealed result class passed to task-006 — never store in any persistent layer at this stage. Handle the case where the user has no NIN available (Vipps may return null for foreign nationals) gracefully with a typed result.

Testing Requirements

Unit tests (flutter_test): test state parameter mismatch → AuthState.error path. Test valid callback parsing (authorization code extraction from URI query params). Mock HTTP client to return a valid token response and verify all claims are extracted. Mock HTTP client to return 400 invalid_grant and verify AuthState.error.

Test access_denied callback → AuthState.unauthenticated. Test ID token expiry validation using a token with exp in the past. Test that secure storage is cleared after both success and failure paths. Integration tests: use Vipps test environment (sandbox) to perform a real end-to-end token exchange.

Security tests: send a mismatched state, verify rejection. Send a token with invalid signature, verify rejection. Target 95%+ branch coverage on the callback handler.

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.