Authentication
Drop Authentication System
Sources:
src/drop-app/src/app/api/auth/bankid/,src/drop-api/src/lib/,auth.bankid.tssrc/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:
2424hhours(web cookie), 7d (mobile Bearer token) CookieWebname:cookie:drop_token(httpOnly, secure, sameSite=strict)Cookie flags:Mobile:httpOnly,Bearersecuretoken in Authorization header
Phase 2 (productionplanned)
- Vipps
path=/Login — same OIDC pattern, user dedup bynational_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:
- 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]"[email protected]"
role: string; // "user" or "merchant"
}
Header
{ "alg": "HS256"
}
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) |
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
On
login/register:login:Asessionsrecordiscreatedwith:token_hash:with SHA-256 hash oftheJWTstringexpires_at: Token expiry timerevoked: 0 (active)
On each
authenticatedrequest:request (requireAuth):Check if user has ANY non-revoked, non-expiredVerify session not revoked + not expiredIf sessions exist but all are revoked/expired → reject with 401
On logout:
revokeAllSessions(userId)setsrevoked=1for allAll user sessions
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
httpOnlySource:inmiddleware.ts:43-56cookie)
- API: Origin
ValidationOn every authenticated request, if anheaderOriginisvalidationpresent:const allowedOrigins = [ process.env.NEXT_PUBLIC_APP_URL, "http://localhost:3000", "http://localhost:3001", ];If the origin is not in theagainst allowedlistorigins - Mobile:
403 Forbidden.CSRF TokenN/A (availableBearerbuttoken,notnoactivelycookies)
generateCsrfToken(): string // crypto.randomBytes(32).toString("hex")
validateCsrf(request, token): boolean // Checks x-csrf-token header
Source: middleware.ts:88-99
Rate Limiting
Persistent
| Endpoint | Limit |
|---|---|
| BankID |
10/
|
| BankID callback | 10/min per IP |
| Auth me/logout/refresh | No additional limit |
Authorization
Role-Based Access
Two roles in the system:roles: user and merchant.
| | |
| | |
Route Protection Summary
| None | |||
| - | |||
| POST / |
|||
| - | |||
| GET / |
Required | Any | |
| Required | Any | ||
| POST / |
Required | Any | |
| POST / |
Required | Any | |
| GET / |
Required | Merchant |
Deprecated Endpoints
These endpoints return 410 Gone:
| Endpoint | Replacement | ||
|---|---|---|---|
POST /auth/login |
BankID OIDC flow | ||
POST / |
|||
POST / |
|||
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 use the same BankID login as regular users. After logging in:utils-server.ts:8-15
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/meon mount (with deduplication viauseRef)Redirects to/loginif unauthenticated (unlessredirectIfUnauthenticated=false)logout()callsPOST /api/auth/logoutthen 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 acceptsany 6-digit code— no actual verificationNo SMS provider (Twilio, Vonage, etc.) is integratedNo phone number verification occursBankID (planned) will replace OTP-based verification for production
For production: Phone verification will be handled via BankID fødselsnummer validation, not SMS OTP.