Skip to main content

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: 24h (web), 7d (mobile)
  • 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<br/>(Next.js Client)
    participant BFF as Next.js BFF<br/>(/api/auth/bankid)
    participant BankID as BankID OIDC<br/>Provider
    participant DB as SQLite/PostgreSQL

    User->>Browser: Navigate to /login
    Browser->>Browser: Render login page<br/>(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<br/>(client_id, redirect_uri, state, nonce, scope=openid)
    BankID->>User: BankID authentication UI<br/>(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 (24h, secure, sameSite=strict)
    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 (NODE_ENV !== "production" or BANKID_MOCK=true), an email/password form is available:

sequenceDiagram
    actor User
    participant Browser as Browser<br/>(/login page)
    participant API as Next.js API<br/>(/api/auth/login)
    participant DB as SQLite

    User->>Browser: Enter demo credentials<br/>([email protected] / demo1234)
    User->>Browser: Click "Logg inn"

    Browser->>Browser: Client-side validation<br/>(email regex, required fields)
    Browser->>API: POST /api/auth/login { email, password }

    API->>API: Rate limit check (10/min per IP)
    API->>DB: SELECT user WHERE email = ?
    API->>API: bcrypt.compare(password, hash)

    alt Valid credentials
        API->>DB: INSERT session
        API->>API: Sign JWT, set httpOnly cookie
        API-->>Browser: 200 { user }
        Browser->>Browser: router.push("/dashboard")
    else Invalid credentials
        API-->>Browser: 401 { error: "unauthorized" }
        Browser->>Browser: Show error message
    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<br/>(/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<br/>(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<br/>{ 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<br/>(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 (24h web / 7d mobile)
    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=strict
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 useAuth Hook (Web)

The useAuth() hook on the web client:

  1. Calls GET /api/auth/me on mount
  2. If 401 → redirects to /login
  3. Returns { user, loading } to the page component
  4. Every authenticated page wraps content with if (loading) return <Skeleton />

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 <TextInput> Email entry
Password input <TextInput secureTextEntry> Password entry
Login button <TouchableOpacity> 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 <label> elements
Error announcements Error messages use role="alert" for screen readers
Focus management After error, focus returns to the first invalid field
Color contrast Error red (#EF4444) on white background meets 4.5:1 ratio
Keyboard navigation Tab order follows visual order: email → password → submit → social buttons
Password visibility Toggle button has aria-label "Vis passord" / "Skjul passord"
Loading state Submit button shows loading spinner and is disabled during request

9. Cross-References