Skip to main content

Authentication

Drop Authentication System

Sources: src/drop-app/src/app/api/auth/bankid/, src/drop-api/src/lib/auth.bankid.ts, src/drop-app/api/src/lib/middleware.ts, src/drop-app/src/lib/middleware/auth-middleware.routes/auth.ts

Overview

Drop uses cookie-basedBankID JWTOIDC as the sole authentication method. Email/password login has been removed to comply with sessionPSD2/SCA revocation tracking.requirements.

  • Auth method: BankID OIDC (Norwegian eID)
  • JWT Algorithm: HS256 (HMAC-SHA256), RS256 opt-in
  • Library: jose (SignJWT / jwtVerify)
  • Token lifetime: 2424h hours(web cookie), 7d (mobile Bearer token)
  • CookieWeb name:cookie: drop_token (httpOnly, secure, sameSite=strict)
  • Cookie flags:Mobile: httpOnly,Bearer securetoken in Authorization header

Phase 2 (productionplanned)

only),
    sameSite=strict,
  • Vipps path=/Login — same OIDC pattern, user dedup by national_id_hash
  • Idura aggregator optional (single integration point for BankID + Vipps)

Authentication Flow

RegistrationBankID FlowLogin (Web)

ClientBrowser                     ServerNext.js BFF                   BankID OIDC
  |                            |                              |
  POST|  GET /api/auth/registerbankid      |                              |
  |--------------------------->|                              |
  |                            |  1. Rate limit check         |
  |                            |  2. Generate state + nonce   |
  |                            |  3. Set bankid_state cookie  |
  |  {email, password,redirectUrl ...}           |                              |
  |<---------------------------|                              |
  |                            |                              |
  |  Browser redirects to BankID authorize URL                |
  |---------------------------------------------------------->|
  |                            |                              1.|
  Rate|  limitUser checkauthenticates (10/minwith per IP)BankID                           |
  |                            2. Validate all fields|                              |
  |  3.BankID Checkredirects emailto uniqueness/api/auth/bankid/callback?code=&state=
  |<----------------------------------------------------------|
  |                            |                              |
  |  GET /callback?code&state  |                              |
  |--------------------------->|                              |
  |                            |  4. HashVerify passwordstate (bcrypt,vs 12cookie   rounds)|
  |                            |  5. INSERTExchange intocode usersfor tabletokens |
  |                            6. Sign JWT {userId, email, role}|----------------------------->|
  |                            |  7.{ Createid_token, sessionaccess_token record (SHA-256 hash of token)}  |                               |  8. Set httpOnly cookie
  |  Set-Cookie: drop_token=...
  |                            |<------------------------------|
  |                            {data:|  {id,6. email,Verify ...}}ID token (JWKS)   |
  |                            |  7. Parse pid, verify age    |
  |                            |  8. Find/create user         |
  |                            |  9. Create session + cookie  |
  |  302 /dashboard            |                              |
  |<---------------------------|                              |

BankID Login Flow(Mobile)

ClientMobile ServerApp                  Hono API                      BankID OIDC
  |                            |                              |
  POST|  GET /api/v1/auth/loginbankid/initiate?platform=mobile             |
  |--------------------------->|                              |
  |  {email, password}redirectUrl, state }    |                              |
  |<---------------------------|                              |
  |                            |                              |
  |  Open BankID in secure browser (expo-web-browser)         |
  |---------------------------------------------------------->|
  |                            |                              1.|
  Rate|  limitUser checkauthenticates (10/minwith per IP)BankID                           |
  |                            2. Look up user by email|                              |
  |  3.Redirect Verifyto password (bcrypt compare)
  |                               |  4. Sign JWT
  |                               |  5. Create session record
  |                               |  6. Set httpOnly cookie
  |  Set-Cookie: drop_token=...drop://auth/callback?code=&state=            |
  |<----------------------------------------------------------|
  |  {data: {id, email, ...}}     |

Request Authentication

Client                          Server
  |                            |                              |
  GET|  POST /api/v1/auth/mebankid/callback                            |
  |  Cookie:{ drop_token=eyJ...code, state, platform }                                |
  |--------------------------->|                              |
  |                            |  1. Exchange code for tokens |
  |                            |----------------------------->|
  |                            |  1.{ Extractid_token token from cookie
  |                               |  2. Verify JWT signature + expiry
  |                               |  3. CSRF origin check (if Origin header present)
  |                               |  4. Check session not revoked
  |                               |  5. Load user from database
  |<------------------------------|
  |  {data: {user}}                |

Logout Flow

Client                          Server
  |                               |
  |  POST /api/auth/logout        |
  |  Cookie: drop_token=...       |
  |------------------------------>|
  |                               |  1. Verify current auth
  |                               |  2. Revoke ALL sessions for user
  |                               |  3. Delete cookie
  |  Set-Cookie: drop_token=;     |
  |  expires=Thu, 01 Jan 1970
  |                            |<-----------------------------|
  |                            |  2. Verify ID token (JWKS)   |
  |                            |  3. Parse pid, verify age    |
  |                            |  4. Find/create user         |
  |                            |  5. Create session           |
  |  { token, data }           |                              |
  |<---------------------------|                              |
  {message:|                            "Logged|                              out"}|
  |  Store token in AsyncStorage                              |

User Creation

BankID login automatically creates user accounts:

  1. Parse pid from BankID ID token (Norwegian national ID, 11 digits)
  2. Hash pid with SHA-256 for storage (national_id_hash column)
  3. Check existing user by national_id_hash
  4. If new: Create user with:
    • kyc_status = 'approved' (BankID = verified identity)
    • kyc_method = 'bankid'
    • auth_provider = 'bankid'
    • password_hash = 'EIDONLY' (sentinel — no password auth)
  5. Age check: Must be >= 18 (parsed from pid birthdate)

JWT Structure

Payload

interface JwtPayload {
  userId: string;   // e.g., "usr_a1b2c3d4e5f6g7h8"
  email: string;    // e.g., "[email protected]"[email protected]"
  role: string;     // "user" or "merchant"
}

Header

{ "alg": "HS256"
}

Claims

Claim Value
exp Current time + 24h (web) / 7d (mobile)
iat Current time
issdrop-api (Hono) / none (Next.js)
auddrop (Hono) / none (Next.js)

Source: auth.ts:28-34


JWT Secret Management

Source: auth.ts:5-16

Production:
  - JWT_SECRET env var MUST be set
  - Throws FATAL error if missing (unless during build phase — NEXT_PHASE)

Development:
  - Falls back to "drop-dev-only-" + process.cwd()
  - NEVER used in production runtime

Session Revocation (C5)

Source: middleware.ts:42-80, auth.ts:56-66

How It Works

  1. On login/register:login: A sessions record is created with:

    • token_hash:with SHA-256 hash of the JWT string
    • expires_at: Token expiry time
    • revoked: 0 (active)
  2. On each authenticatedrequest: request (requireAuth):

    • Check if user has ANY non-revoked, non-expiredVerify session not revoked + not expired
    • If sessions exist but all are revoked/expired → reject with 401
  3. On logout:

    • revokeAllSessions(userId) sets revoked=1 for allAll user sessions
    marked revoked = 1

Backwards Compatibility

If a user has zero session records (pre-migration), authentication still works — revocation check is skipped (middleware.ts:72-76).


CSRF Protection

  • Web: State parameter in BankID OIDC flow (C6)stored

    Source:in middleware.ts:43-56

    httpOnly

    cookie)

  • API: Origin Validation

    On every authenticated request, if an Origin header isvalidation present:

    const allowedOrigins = [
      process.env.NEXT_PUBLIC_APP_URL,
      "http://localhost:3000",
      "http://localhost:3001",
    ];
    

    If the origin is not in theagainst allowed listorigins

  • Mobile: 403 Forbidden.

    CSRF TokenN/A (availableBearer buttoken, notno activelycookies)

  • enforced on routes)
    generateCsrfToken(): string    // crypto.randomBytes(32).toString("hex")
    validateCsrf(request, token): boolean  // Checks x-csrf-token header
    

    Source: middleware.ts:88-99


Rate Limiting

PersistentRateLimiter(Active)

Source:

middleware.ts:7-31

Uses

therate_limitsdatabasefor persistent, cross-restart rate limiting.

async function rateLimit(ip: string, limit: number, windowMs: number = 60000): Promise<boolean>
  • Cleans expired entries on each call
  • Returns true if allowed, false if limit exceeded
  • Used on: login (
  • STRICT_RATE_LIMIT:
  • 10req/

    Returns

    rate X-RateLimit-Remaining,X-RateLimit-Reset

    Endpoint Limit
    BankID tableinitiate 10/min), register (10/min), remittance (10/min), qr-payment (10/min), rates (120/min)

    In-Memory Rate Limiter (Available)

    Source: middleware/auth-middleware.ts:44-115

    Alternative implementation using Map<string, RateLimitEntry> with automatic cleanup every 5 minutes.

    Predefined configs:

    • DEFAULT_RATE_LIMIT: 100 req/min per IP
    BankID callback10/min per IP
    Auth me/logout/refreshNo additional limit headers:(auth X-RateLimit-Limit,required)

    Authorization

    Role-Based Access

    Two roles in the system:roles: user and merchant.

    FunctionRoute SourceAuth ChecksRole
    requireAuth(request?)middleware.ts:42Cookie auth + session revocation + CSRF
    requireMerchant(request?)middleware.ts:101All of requireAuth + role === 'merchant'

    Route Protection Summary

    RouteAuthRoleAdditional
    POSTGET /api/auth/registerbankid/initiate None-Rate limited
    POST /api/auth/loginNone-Rate limited
    GET /api/auth/meRequiredAny -
    POST /api/auth/logoutbankid/callback RequiredAny-
    POST /api/auth/refreshRequiredAnyNone -
    GET /api/transactionsauth/me Required Any
    -POST /auth/logoutRequiredAny
    POST /api/transactions/remittanceauth/refresh Required Any KYC approved, rate limited
    POST /api/transactions/qr-paymentmerchants/register Required Any Rate(upgrades limitedto merchant)
    GET /api/merchants/dashboard Required Merchant

    Deprecated Endpoints

    These endpoints return 410 Gone:

    AutomaticviaNot BankIDreplaces
    EndpointReplacement
    -POST /auth/loginBankID OIDC flow
    GETPOST /api/merchants/qrauth/register Required Merchant -BankID login
    GETPOST /api/merchants/transactionsauth/verify-otp Required Merchant-
    GET /api/ratesNone-Rate limitedneeded (120/min)
    GET /api/healthNone--OTP)

    PasswordEnvironment SecurityVariables

    Required (Production)

    BANKID_CLIENT_ID          # BankID OIDC client ID
    BANKID_CLIENT_SECRET      # BankID OIDC client secret
    BANKID_CALLBACK_URL       # Web callback URL (e.g., https://getdrop.no/api/auth/bankid/callback)
    BANKID_CALLBACK_URL_MOBILE # Mobile deep link (e.g., drop://auth/callback)
    JWT_SECRET                # JWT signing secret (min 32 chars)
    

    Optional

    BANKID_AUTHORIZE_URL      # Default: BankID prod authorize endpoint
    BANKID_TOKEN_URL          # Default: BankID prod token endpoint
    BANKID_JWKS_URL           # Default: BankID prod JWKS endpoint
    BANKID_ISSUER             # Default: BankID prod issuer
    BANKID_MOCK=true          # Dev mode: mock OIDC flow (no real BankID needed)
    JWT_ALGORITHM             # "HS256" (default) or "RS256"
    JWT_EXPIRY                # Default: "24h"
    

    Merchant Flow

    Source:Merchants utils-server.ts:8-15use the same BankID login as regular users. After logging in:

      1. Hashing:Navigate bcryptto merchant registration
      2. Fill in business details (business name, org number, bank account)
      3. POST /merchants/register with 12auth rounds (bcryptjs)token
      4. Verification:User role upgraded from bcrypt.compareSync(password,user hash)to merchant
      5. PINMerchant hashing:dashboard Samebecomes bcrypt function used for card PINsaccessible

    Client-Side Auth Hook

    Source: use-auth.ts

    function useAuth(redirectIfUnauthenticated = true): {
      user: User | null;
      loading: boolean;
      logout: () => Promise<void>;
      refreshUser: () => Promise<void>;
    }
    
    • Fetches /api/auth/me on mount (with deduplication via useRef)
    • Redirects to /login if unauthenticated (unless redirectIfUnauthenticated=false)
    • logout() calls POST /api/auth/logout then redirects to /login

    Phone/SMS Verification [PLANNED]

    Status: NOT IMPLEMENTED

    The onboarding flow includes a 6-digit OTP verification step (step 2 of 4 in /onboarding), but SMS sending is not implemented. In the current MVP:

    • The OTP input field accepts any 6-digit code — no actual verification
    • No SMS provider (Twilio, Vonage, etc.) is integrated
    • No phone number verification occurs
    • BankID (planned) will replace OTP-based verification for production

    For production: Phone verification will be handled via BankID fødselsnummer validation, not SMS OTP.