Authentication
Drop Authentication System
Sources:
src/drop-app/src/app/api/auth/bankid/,src/drop-api/src/lib/bankid.ts,src/drop-api/src/routes/auth.ts
Overview
Drop uses BankID OIDC as the sole authentication method. Email/password login has been removed to comply with PSD2/SCA requirements.
- Auth method: BankID OIDC (Norwegian eID)
- JWT Algorithm: HS256 (HMAC-SHA256), RS256 opt-in
- Library:
jose(SignJWT/jwtVerify) - Token lifetime: 24h (web cookie), 7d (mobile Bearer token)
⚠️ PSD2 SCA NON-COMPLIANCE (CRITICAL):Current 24h/7d login sessions are acceptable for browsing (AISP) butviolate PSD2 RTS Art. 4for payment initiation (PISP). PISP requires per-transaction SCA with a 5-minute session window, dynamically linked to amount and payee. SeeADR-017for the two-tier session redesign (login session + payment session). This MUST be implemented before Finanstilsynet PISP licence application.⚠️ TODO (token rotation):Migrate to 15-min access tokens + refresh token rotation. Current 7-day mobile tokens stored in AsyncStorage (unencrypted on Android) are a secondary concern. Seerefresh_tokenstable in schema.ts (exists but not yet used).- Web cookie:
drop_token(httpOnly, secure, sameSite=strict) - Mobile: Bearer token in Authorization header
Phase 2 (planned)
- Vipps Login — same OIDC pattern, user dedup by
national_id_hash - Idura aggregator optional (single integration point for BankID + Vipps)
Authentication Flow
BankID Login (Web)
Browser Next.js BFF BankID OIDC
| | |
| GET /api/auth/bankid | |
|--------------------------->| |
| | 1. Rate limit check |
| | 2. Generate state + nonce |
| | 3. Set bankid_state cookie |
| { redirectUrl } | |
|<---------------------------| |
| | |
| Browser redirects to BankID authorize URL |
|---------------------------------------------------------->|
| | |
| User authenticates with BankID |
| | |
| BankID redirects to /api/auth/bankid/callback?code=&state=
|<----------------------------------------------------------|
| | |
| GET /callback?code&state | |
|--------------------------->| |
| | 4. Verify state vs cookie |
| | 5. Exchange code for tokens |
| |----------------------------->|
| | { id_token, access_token } |
| |<-----------------------------|
| | 6. Verify ID token (JWKS) |
| | 7. Parse pid, verify age |
| | 8. Find/create user |
| | 9. Create session + cookie |
| 302 /dashboard | |
|<---------------------------| |
BankID Login (Mobile)
Mobile App Hono API BankID OIDC
| | |
| GET /v1/auth/bankid/initiate?platform=mobile |
|--------------------------->| |
| { redirectUrl, state } | |
|<---------------------------| |
| | |
| Open BankID in secure browser (expo-web-browser) |
|---------------------------------------------------------->|
| | |
| User authenticates with BankID |
| | |
| Redirect to drop://auth/callback?code=&state= |
|<----------------------------------------------------------|
| | |
| POST /v1/auth/bankid/callback |
| { code, state, platform } |
|--------------------------->| |
| | 1. Exchange code for tokens |
| |----------------------------->|
| | { id_token } |
| |<-----------------------------|
| | 2. Verify ID token (JWKS) |
| | 3. Parse pid, verify age |
| | 4. Find/create user |
| | 5. Create session |
| { token, data } | |
|<---------------------------| |
| | |
| Store token in AsyncStorage |
User Creation
BankID login automatically creates user accounts:
- Parse pid from BankID ID token (Norwegian national ID, 11 digits)
- Hash pid with SHA-256 for storage (
national_id_hashcolumn) - Check existing user by
national_id_hash - If new: Create user with:
kyc_status = 'approved'(BankID = verified identity)kyc_method = 'bankid'auth_provider = 'bankid'password_hash = 'EIDONLY'(sentinel — no password auth)
- 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]"
role: string; // "user" or "merchant"
}
Claims
| Claim | Value |
|---|---|
exp |
Current time + 24h (web) / 7d (mobile) |
iat |
Current time |
iss |
drop-api (Hono) / none (Next.js) |
aud |
drop (Hono) / none (Next.js) |
Session Revocation
- On login:
sessionsrecord created with SHA-256 hash of JWT - On each request: Verify session not revoked + not expired
- On logout: All user sessions marked
revoked = 1
CSRF Protection
- Web: State parameter in BankID OIDC flow (stored in httpOnly cookie)
- API: Origin header validation against allowed origins
- Mobile: N/A (Bearer token, no cookies)
Rate Limiting
| Endpoint | Limit |
|---|---|
| BankID initiate | 10/min per IP |
| BankID callback | 10/min per IP |
| Auth me/logout/refresh | No additional limit (auth required) |
Authorization
Role-Based Access
Two roles: user and merchant.
| Route | Auth | Role |
|---|---|---|
| GET /auth/bankid/initiate | None | - |
| POST /auth/bankid/callback | None | - |
| GET /auth/me | Required | Any |
| POST /auth/logout | Required | Any |
| POST /auth/refresh | Required | Any |
| POST /merchants/register | Required | Any (upgrades to merchant) |
| GET /merchants/dashboard | Required | Merchant |
Deprecated Endpoints
These endpoints return 410 Gone:
| Endpoint | Replacement |
|---|---|
POST /auth/login |
BankID OIDC flow |
POST /auth/register |
Automatic via BankID login |
POST /auth/verify-otp |
Not needed (BankID replaces OTP) |
Environment Variables
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"
Open Banking Consent Lifecycle (PSD2)
Drop uses the ob_consents table to track PSD2 Open Banking consents granted by users via BankID.
Consent Model
| ||
| ||
| ||
| ||
| ||
| | |
| | |
| ||
| ||
| ||
| ||
|
PSD2 Consent Rules
Maximum duration: 90 days— PSD2 RTS Article 10 requires re-authentication via SCA (BankID) every 90 days for AISP access.Daily access limit: 4/day— PSD2 RTS Article 36(5)(b) limits AISP to 4 data accesses per day without explicit user request.access_count_todaytracks this; resets whenlast_access_datechanges.User revocation— Users can revoke consent at any time via Profile → Bank Accounts → Revoke. Setsstatus = 'revoked'andrevoked_at.Automatic expiry— Background job checksexpires_atand marks expired consents. User is prompted to re-authenticate.
Consent Flow
User Drop API BaaS Partner (AISP)
| | |
| Link Bank Account | |
|--------------------------->| |
| | 1. Initiate consent request |
| |----------------------------->|
| | { consent_url } |
| |<-----------------------------|
| Redirect to bank auth | |
|<---------------------------| |
| User approves at bank | |
|---------------------------------------------------------->|
| Callback with consent_id | |
|<----------------------------------------------------------|
| | 2. Store ob_consent record |
| | 3. Link to bank_account |
| Success — account linked | |
|<---------------------------| |
Renewal Enforcement
When a consent is within 7 days of expiry, the app shows a banner prompting re-authentication. When expired, balance reads return stale data with a warning and the user must re-consent via BankID.
Merchant Flow
Merchants use the same BankID login as regular users. After logging in: