Login & Authentication Flow Flow: Login & Authentication Document: LLD-001 Version: 1.0 Date: 2026-02-21 Author: Frontend Architect (AI Agent) Status: Draft Scope: End-to-end login flow for web and mobile, including BankID OIDC, session management, demo mode, and error handling 1. Overview Drop uses BankID OIDC as the sole production authentication method. Email/password login exists only in demo/dev mode. Authentication produces a JWT stored as an httpOnly cookie (web) or Bearer token (mobile). The login flow includes BankID redirect, loading states, token receipt, session persistence, and comprehensive error handling. Key facts: BankID is mandatory for production (PSD2/SCA compliance) Demo mode provides email/password fallback for development JWT lifetime: 7d (all clients) Session tracking via SHA-256 token hash in sessions table Age verification (>= 18) enforced during BankID callback 2. Web Login Flow (BankID OIDC) 2.1 Sequence Diagram — Web BankID Login sequenceDiagram actor User participant Browser as Browser
(Next.js Client) participant BFF as Next.js BFF
(/api/auth/bankid) participant BankID as BankID OIDC
Provider participant DB as SQLite/PostgreSQL User->>Browser: Navigate to /login Browser->>Browser: Render login page
(BankID + Vipps buttons) User->>Browser: Click "BankID" button Browser->>BFF: GET /api/auth/bankid BFF->>BFF: Rate limit check (10/min per IP) BFF->>BFF: Generate state + nonce BFF->>BFF: Set bankid_state httpOnly cookie BFF-->>Browser: { redirectUrl } Browser->>BankID: Redirect to BankID authorize URL
(client_id, redirect_uri, state, nonce, scope=openid) BankID->>User: BankID authentication UI
(code device, app, or biometric) User->>BankID: Authenticate with BankID BankID-->>Browser: 302 → /api/auth/bankid/callback?code=XXX&state=YYY Browser->>BFF: GET /api/auth/bankid/callback?code&state BFF->>BFF: Verify state matches bankid_state cookie BFF->>BankID: POST /token (exchange code for tokens) BankID-->>BFF: { id_token, access_token } BFF->>BFF: Verify id_token signature (JWKS) BFF->>BFF: Parse pid (national ID, 11 digits) BFF->>BFF: Verify age >= 18 from pid birthdate BFF->>DB: SELECT user WHERE national_id_hash = SHA-256(pid) alt New user BFF->>DB: INSERT user (kyc_status=approved, auth_provider=bankid) BFF->>DB: INSERT default settings (NOK, nb) end BFF->>DB: INSERT session (token_hash, expires_at) BFF->>BFF: Sign JWT (userId, email, role) BFF->>BFF: Set drop_token httpOnly cookie (7d, secure, sameSite=Lax) BFF-->>Browser: 302 → /dashboard Browser->>Browser: Router navigates to /dashboard Browser->>BFF: GET /api/auth/me (with cookie) BFF->>DB: Verify session not revoked BFF-->>Browser: { user, bankAccounts, totalBalance } Browser->>Browser: Render dashboard with user data 2.2 Demo Mode Login (Development Only) In development ( isDemoMode() returns true), a demo login endpoint is available. It loads a fixed demo user ( usr_demo1 , email: demo@example.test ) without requiring credentials: sequenceDiagram actor User participant Browser as Browser
(/login page) participant API as Next.js API
(/api/auth/login) participant DB as SQLite User->>Browser: Click "Demo Login" Browser->>API: POST /v1/auth/demo-login (no credentials) API->>API: Check isDemoMode() API->>DB: SELECT user WHERE id = 'usr_demo1' Note over API: Fixed demo user (demo@example.test) alt Demo mode active API->>DB: INSERT session API->>API: Sign JWT, set httpOnly cookie API-->>Browser: 200 { user, token } Browser->>Browser: router.push("/dashboard") else Demo mode disabled API-->>Browser: 404 { error: "not_found" } end 3. Mobile Login Flow (BankID OIDC) 3.1 Sequence Diagram — Mobile BankID Login sequenceDiagram actor User participant App as Expo App participant WebBrowser as expo-web-browser participant API as Hono API
(/v1/auth) participant BankID as BankID OIDC participant DB as Database User->>App: Open app, tap "Logg inn" App->>API: GET /v1/auth/bankid/initiate?platform=mobile API->>API: Rate limit check API->>API: Generate state + nonce API-->>App: { redirectUrl, state } App->>WebBrowser: Open BankID URL
(expo-web-browser) WebBrowser->>BankID: BankID authorize URL BankID->>User: BankID authentication User->>BankID: Authenticate BankID-->>WebBrowser: Redirect to drop://auth/callback?code&state WebBrowser-->>App: Deep link intercept App->>API: POST /v1/auth/bankid/callback
{ code, state, platform: "mobile" } API->>BankID: Exchange code for tokens BankID-->>API: { id_token } API->>API: Verify id_token (JWKS) API->>API: Parse pid, verify age >= 18 API->>DB: Find or create user alt New user API->>DB: INSERT user (kyc_status=approved) end API->>DB: INSERT session API->>API: Sign JWT (7d expiry) API-->>App: { token, data: { user } } App->>App: Store token in AsyncStorage App->>App: Navigate to (tabs) dashboard Note over App: Future: biometric unlock
(Face ID / Touch ID) 4. Authentication State Diagram stateDiagram-v2 [*] --> Unauthenticated: App launch Unauthenticated --> BankIDRedirect: Click "BankID" Unauthenticated --> DemoLogin: Enter credentials (dev mode) BankIDRedirect --> BankIDAuthenticating: Browser opens BankID BankIDAuthenticating --> CallbackProcessing: BankID returns code BankIDAuthenticating --> BankIDError: Auth failed/cancelled/timeout CallbackProcessing --> Authenticated: Valid token + session created CallbackProcessing --> AgeRejected: User under 18 CallbackProcessing --> BankIDError: Token verification failed DemoLogin --> Authenticated: Valid credentials DemoLogin --> LoginError: Invalid credentials Authenticated --> SessionActive: JWT valid + session not revoked SessionActive --> TokenExpired: JWT expired (7d all platforms) SessionActive --> SessionRevoked: Logout or admin revoke SessionActive --> Authenticated: Token refresh TokenExpired --> Unauthenticated: Redirect to /login SessionRevoked --> Unauthenticated: Clear cookie/token AgeRejected --> Unauthenticated: Show age error BankIDError --> Unauthenticated: Show error + retry LoginError --> Unauthenticated: Show error message 5. Error States 5.1 Error State Table Error Cause User-Facing Message (Norwegian) Recovery Action BankID Unavailable BankID service down "BankID er midlertidig utilgjengelig. Prøv igjen senere." Retry button, show status page link BankID Timeout User took too long (>5min) "BankID-sesjonen utløp. Vennligst prøv igjen." Auto-redirect back to login page BankID Cancelled User cancelled authentication "Innlogging avbrutt. Trykk 'BankID' for å prøve igjen." Show login page with BankID button State Mismatch CSRF attack or stale session "Noe gikk galt. Vennligst prøv å logge inn på nytt." Clear state cookie, redirect to /login Token Verification Failed Invalid/tampered id_token "Autentisering mislyktes. Prøv igjen." Redirect to /login Age Under 18 User is younger than 18 "Du må være minst 18 år for å bruke Drop." No retry — age requirement is firm Rate Limited Too many login attempts "For mange forsøk. Vent litt og prøv igjen." Wait and retry (10/min limit) Invalid Credentials (Demo) Wrong email or password "Feil e-post eller passord." Re-enter credentials Session Expired JWT expired "Sesjonen din har utløpt. Logg inn igjen." Redirect to /login Session Revoked Logout from another device "Du har blitt logget ut." Re-login via BankID Network Error No connectivity "Ingen nettverkstilkobling. Sjekk internett." Retry when connectivity restored 5.2 Error Handling by Platform Platform Error Display Navigation Web Inline error message on login page, red text below form router.push("/login") on session errors Mobile Alert dialog or inline error text router.replace("/") (welcome screen) on session errors 6. Session Management 6.1 Token Storage Platform Storage Token Name Flags Web httpOnly cookie drop_token httpOnly, secure, sameSite=Lax Mobile AsyncStorage Bearer token In-memory variable + persistent storage 6.2 Session Lifecycle Event Action Database Login success Create session record INSERT INTO sessions (id, user_id, token_hash, expires_at) Each request Verify session valid SELECT * FROM sessions WHERE token_hash = ? AND revoked = 0 AND expires_at > now() Token refresh New session, revoke old INSERT new session , UPDATE old session SET revoked = 1 Logout Revoke all sessions UPDATE sessions SET revoked = 1 WHERE user_id = ? Admin action Revoke specific session UPDATE sessions SET revoked = 1 WHERE id = ? 6.3 Token Refresh POST /v1/auth/refresh refreshes the user's session: Reads drop_token cookie / Bearer token Verifies current JWT and session validity Revokes old session ( UPDATE sessions SET revoked = 1 ) Creates new session + JWT Sets new drop_token cookie (Max-Age=604800, HttpOnly, SameSite=Lax) Returns new user data 6.4 Deprecated Endpoints The following endpoints return 410 Gone and exist only for backward compatibility: Endpoint Status Reason POST /v1/auth/login 410 Gone Replaced by BankID OIDC flow POST /v1/auth/register 410 Gone User creation is automatic on first BankID login POST /v1/auth/verify-otp 410 Gone OTP flow removed with BankID migration 6.5 useAuth Hook (Web) The useAuth() hook on the web client: Calls GET /api/auth/me on mount If 401 → redirects to /login Returns { user, loading } to the page component Every authenticated page wraps content with if (loading) return 7. UI Components Involved 7.1 Web Login Page ( /login ) Component Source Purpose Image next/image Drop logo display Link next/link Navigation to /register, /dashboard Button shadcn/ui Submit button, social login buttons Mail lucide-react Email input icon Lock lucide-react Password input icon Eye / EyeOff lucide-react Password visibility toggle ArrowRight lucide-react Submit button icon 7.2 Mobile Login Screen ( login.js ) Element Implementation Purpose Email input Email entry Password input Password entry Login button Trigger api.login() Register link router.push("/register") Navigate to registration 8. Accessibility Considerations (WCAG 2.1 AA) Requirement Implementation Form labels All inputs have associated