critical priority high complexity backend pending backend specialist Tier 5

Acceptance Criteria

Supabase Edge Function receives the Vipps access token and performs the upsert server-side — mobile client never calls Supabase with service role key
If the Vipps email/phone matches an existing Supabase Auth user, the accounts are linked (no duplicate accounts created)
If no existing account is found, a new Supabase Auth user is created with the Vipps identity as provider
Personnummer is encrypted before writing to the User Identity Repository — plaintext NIN never reaches any database column
Personnummer write is gated behind an explicit in-app consent prompt: user must accept data processing terms before NIN is stored
If user denies NIN consent, authentication proceeds to authenticated state without persisting the personnummer
Supabase session (JWT) is returned from the Edge Function and stored in flutter_secure_storage via the Auth Session Manager
AuthState transitions to authenticated(user) after successful session establishment
If the upsert fails (network, constraint violation, encryption error), AuthState.error is emitted and no partial data is written
The user's organization membership is resolved from the Supabase database during this step and included in the authenticated user profile
All database writes are atomic: either all succeed or none are committed (use Supabase transaction or Edge Function transaction block)
Audit log entry created in a server-side audit table recording Vipps authentication event (without storing NIN)

Technical Requirements

frameworks
Flutter
Riverpod
BLoC
flutter_secure_storage
Supabase Flutter SDK
apis
Supabase Edge Functions (Deno)
Supabase Auth admin API (server-side only)
Supabase PostgreSQL RLS
data models
contact
assignment
performance requirements
End-to-end Vipps callback to authenticated state under 4 seconds on 4G network
Supabase upsert operation completes within 2 seconds
Edge Function cold start handled gracefully with retry logic (max 2 retries with 500ms backoff)
security requirements
Personnummer encrypted at rest using AES-256-GCM with per-user encryption keys managed by Supabase Vault or KMS
NIN never transmitted in HTTP response bodies — only a boolean 'nin_stored: true' confirmation returned to mobile
Service role key used exclusively in the Deno Edge Function environment variable — never in Flutter code
RLS policies must prevent any user from reading another user's encrypted NIN field
Consent timestamp stored alongside the encrypted NIN for GDPR auditability
GDPR: NIN storage requires DPA with Vipps and documented lawful basis — must be verified before this code goes to production
All PII fields subject to encryption before storage per Supabase PostgreSQL 15 integration security rules
ui components
ConsentDialog (NIN storage consent)
LoadingOverlay (during upsert)
ErrorDialog (on failure)

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

The Edge Function should: (1) verify the Vipps access token by calling the Vipps userinfo endpoint, (2) look up existing Supabase user by Vipps sub claim using admin API, (3) create or update the user, (4) if consent was granted (passed as a boolean in the request body from mobile), encrypt and upsert the NIN via supabase.rpc('store_encrypted_nin', {...}), (5) create a Supabase session and return the JWT. Keep the NIN in a Deno string variable only as long as needed — delete immediately after the RPC call. On the Flutter side, pass the Vipps access token and the user's consent decision (boolean) to the Edge Function via a POST body. Store the returned JWT using flutter_secure_storage with the key supabase_session_v1.

Then call authSessionManager.setSession(jwt) to trigger the AuthState.authenticated emission. For existing user linking, use the Vipps 'sub' claim as the stable identifier to match against a vipps_sub column on the auth.users metadata — not email, which can change.

Testing Requirements

Unit tests (flutter_test): mock the Edge Function response and verify AuthState transitions for success, failure, and consent-denied paths. Verify flutter_secure_storage.write() is called with the Supabase JWT on success. Verify no write occurs on consent denial. Integration tests: deploy Edge Function to Supabase test project, perform end-to-end Vipps auth flow, verify user record created, NIN stored encrypted, session JWT returned.

Test idempotency: run the upsert twice with the same Vipps identity, verify only one user record exists. Test rollback: simulate database constraint failure mid-upsert, verify no partial records. Security tests: attempt to call the Edge Function with an invalid Vipps access token, verify 401 rejection. Verify NIN column is unreadable via direct Supabase client (RLS).

GDPR test: verify consent denial results in null NIN field.

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.