critical priority high complexity backend pending backend specialist Tier 5

Acceptance Criteria

Given a validated BankID assertion containing a verified personnummer, the system must look up the Supabase user table by personnummer and return the matched user ID within 500ms.
Given no existing Supabase user matches the personnummer, a new user account is created with the personnummer stored in the user_identities table and the BankID identity level (e.g., 'high') persisted in the User Identity Repository.
Given a successful identity link or creation, a Supabase JWT session is issued containing custom claims: { bankid_verified: true, identity_level: 'high', pid: '<personnummer_hash>' }.
The Supabase session access token and refresh token are stored in Secure Storage immediately after session creation, not in memory or shared preferences.
Given an already-authenticated user re-authenticating with BankID, the existing account is linked without duplication; duplicate identity creation is explicitly rejected with a meaningful error.
Given a Supabase edge function or RPC failure during account creation, the error is surfaced to the BLoC as a BankIdLinkingFailure state with a retry opportunity — no partial state is left in the database.
The User Identity Repository's `identityLevel` field is set to 'bankid_high' and persisted atomically in the same transaction as the user creation/link.
All personnummer values are hashed (SHA-256 minimum) before storage; raw personnummer must never appear in logs, error messages, or non-encrypted storage.
The auth state stream emits Authenticated(user, trustLevel: high) upon successful session creation.
If the organization associated with the BankID login is not found or the user has no active role in that organization, the session is terminated and a NoOrganizationAccess error is emitted.

Technical Requirements

frameworks
Flutter
Riverpod
BLoC
Dart
apis
Supabase Auth API (signInWithIdToken or custom JWT minting via edge function)
Supabase Database REST API (upsert user_identities row)
Supabase RPC for atomic identity-link transaction
BankID Assertion Validation endpoint (upstream, completed in task-008)
data models
UserIdentity (userId, personnummerHash, identityLevel, provider, createdAt, updatedAt)
SupabaseSession (accessToken, refreshToken, expiresAt, userId)
AuthState (Authenticated | Unauthenticated | Loading | Error)
BankIdClaims (bankid_verified, identity_level, pid_hash)
performance requirements
Identity lookup + session creation round-trip must complete within 1500ms on a 4G connection
Supabase RPC for upsert must be wrapped in a database transaction to prevent partial writes
Session token storage to Secure Storage must complete before emitting auth state change
security requirements
Personnummer must be hashed (SHA-256) before storage in any database field
JWT custom claims must be set server-side (Supabase edge function), never client-side
Secure Storage (flutter_secure_storage) used exclusively for token persistence — no SharedPreferences
Access token must not be logged at any verbosity level
Supabase RLS policies must restrict user_identities rows to own userId only
PKCE code verifier from task-008 must be invalidated after assertion exchange

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Implement identity linking via a Supabase Edge Function (TypeScript/Deno) to keep JWT claim minting server-side and prevent client-side privilege escalation. The Flutter service calls the edge function with the validated BankID assertion token; the edge function performs the upsert and returns a signed Supabase JWT. Use `supabase.rpc('link_bankid_identity', { pid_hash, identity_level })` wrapped in a Dart try/catch with typed failure classes. Define a sealed class hierarchy for BankIdLinkResult: Success(session), UserNotInOrg, DuplicateIdentity, LinkingFailure(error).

Store refresh token in flutter_secure_storage under key 'supabase_refresh_token_{userId}'. The BLoC should transition: BankIdValidated → IdentityLinking → Authenticated | LinkingError. Ensure atomicity: the Supabase edge function must use a single `BEGIN/COMMIT` block for user creation + identity row insertion.

Testing Requirements

Write unit tests for the identity-link service covering: (1) happy path — existing user matched by personnummer hash, session created, claims verified; (2) new user creation path — no match found, user created, identity row inserted, session issued; (3) duplicate identity rejection — same personnummer linked twice returns error without creating duplicate; (4) RPC failure — Supabase call throws, BLoC emits BankIdLinkingFailure, no partial state remains; (5) invalid assertion input — null or malformed BankID assertion rejected before any Supabase call. Mock Supabase client using a test double. Verify JWT custom claims structure using a JWT decode utility in tests. Verify Secure Storage write is called with correct token keys.

Coverage target: 90% for the identity-link service class.

Component
BankID 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.