BankID OIDC Integration
BankID OIDC Integration
Version: 1.0
Date: 2026-02-21
Author: Banking Architecture Team
Status: Approved
Applies to: Drop — Authentication via Norwegian BankID
1. Overview
Drop uses BankID OIDC (OpenID Connect) as its sole authentication method. BankID provides Strong Customer Authentication (SCA) out of the box — combining possession (mobile device / code generator) with knowledge (personal code) or inherence (biometrics).
Email/password login has been removed. All deprecated auth endpoints return 410 Gone .
Property
Value
Identity Provider
BankID (prod: auth.bankid.no )
Protocol
OpenID Connect 1.0 (Authorization Code Flow)
Auth endpoint
https://auth.bankid.no/auth/realms/prod/protocol/openid-connect/auth
Token endpoint
https://auth.bankid.no/auth/realms/prod/protocol/openid-connect/token
JWKS endpoint
https://auth.bankid.no/auth/realms/prod/protocol/openid-connect/certs
Issuer
https://auth.bankid.no/auth/realms/prod
Scopes
openid profile
Response type
code (authorization code flow)
Source code
src/drop-api/src/lib/bankid.ts
2. BankID OIDC Flow
2.1 Web Flow (Next.js BFF)
sequenceDiagram
participant B as Browser
participant N as Next.js BFF
/api/auth/bankid/*
participant BID as BankID OIDC
(auth.bankid.no)
B->>N: GET /api/auth/bankid
Note over N: Rate limit check (10/min per IP)
N->>N: Generate state (crypto.randomUUID)
Generate nonce (crypto.randomUUID)
N->>N: Set httpOnly cookie: bankid_state={state}
N-->>B: {redirectUrl: "https://auth.bankid.no/auth/...?
client_id=X&redirect_uri=Y&response_type=code
&scope=openid+profile&state=Z&nonce=N"}
B->>BID: Browser redirects to BankID authorize URL
Note over B,BID: User authenticates with BankID
(BankID app / code generator / biometrics)
BID-->>B: 302 redirect to /api/auth/bankid/callback
?code=AUTH_CODE&state=Z
B->>N: GET /api/auth/bankid/callback?code=AUTH_CODE&state=Z
N->>N: Verify state matches bankid_state cookie
N->>BID: POST /token
{grant_type: authorization_code,
code: AUTH_CODE, redirect_uri: Y,
client_id: X, client_secret: S}
BID-->>N: {id_token: "eyJ...", access_token: "...",
token_type: "Bearer", expires_in: 300}
N->>N: Verify id_token signature via JWKS
N->>N: Validate: issuer, audience, expiry, nonce
N->>N: Extract pid (fødselsnummer) from claims
N->>N: Parse birthdate from pid, verify age >= 18
N->>N: findOrCreateUser(pid, name)
N->>N: Create session (sessions table)
N->>N: Issue Drop JWT, set httpOnly cookie (drop_token)
N-->>B: 302 redirect to /dashboard
2.2 Mobile Flow (Hono API + Expo)
sequenceDiagram
participant M as Mobile App
(Expo)
participant H as Hono API
/v1/auth/bankid/*
participant BID as BankID OIDC
M->>H: GET /v1/auth/bankid/initiate?platform=mobile
Note over H: Rate limit check (10/min per IP)
H->>H: Generate state + nonce
H-->>M: {redirectUrl: "https://auth.bankid.no/...",
state: "Z"}
M->>M: Store state in memory
M->>BID: Open BankID in secure browser
(expo-web-browser)
Note over M,BID: User authenticates with BankID
BID-->>M: Deep link: drop://auth/callback
?code=AUTH_CODE&state=Z
M->>M: Verify state matches stored state
M->>H: POST /v1/auth/bankid/callback
{code: AUTH_CODE, state: Z, platform: "mobile"}
H->>BID: POST /token (exchange code)
BID-->>H: {id_token, access_token}
H->>H: Verify id_token via JWKS
H->>H: Extract pid, verify age >= 18
H->>H: findOrCreateUser(pid, name)
H->>H: Create session
H->>H: Issue Drop JWT (7-day expiry)
H-->>M: {token: "eyJ...", data: {id, name, role}}
M->>M: Store token in AsyncStorage
3. Token Lifecycle
3.1 Drop JWT (Issued After BankID Auth)
stateDiagram-v2
[*] --> Issued: BankID auth success
→ createSession() + signJWT()
Issued --> Active: Token in use
(cookie or Bearer header)
Active --> Active: API request
→ verifySession() passes
Active --> Expired: 7d expiry reached
(all clients)
Active --> Revoked: User logs out
→ revokeAllSessions()
Active --> Revoked: Session marked revoked
(admin action)
Expired --> Refreshed: POST /auth/refresh
→ new JWT + new session
Expired --> [*]: User must re-authenticate
Revoked --> [*]: User must re-authenticate
Refreshed --> Active
3.2 JWT Payload Structure
interface DropJwtPayload {
userId: string; // "usr_a1b2c3d4e5f6g7h8"
email: string; // "usr_xxx@bankid.drop.local" (placeholder)
role: string; // "user" | "merchant"
iat: number; // Issued at (Unix timestamp)
exp: number; // Expiry (iat + 7d, all clients)
iss?: string; // "drop-api" (Hono only)
aud?: string; // "drop" (Hono only)
}
3.3 Token Lifetimes
Platform
Token Location
Lifetime
Refresh
Web
httpOnly cookie ( drop_token )
24 hours
POST /api/auth/refresh
Mobile
Bearer header (AsyncStorage)
7 days
POST /v1/auth/refresh
3.4 Session Tracking
Every JWT has a corresponding record in the sessions table:
Column
Purpose
id
Session ID ( ses_ )
user_id
FK to users.id
token_hash
SHA-256 hash of the JWT string
expires_at
Token expiry timestamp
revoked
0 = active, 1 = revoked
On every authenticated request, the middleware:
Extracts JWT from cookie (web) or Authorization header (mobile)
Verifies JWT signature and expiry with jose.jwtVerify()
Hashes the JWT with SHA-256
Looks up the session by token_hash
Verifies revoked = 0 and expires_at > now
Rejects the request if any check fails
4. BankID Claims Mapping
4.1 ID Token Claims
BankID Claim
Type
Drop Usage
Stored In
sub
string
Subject identifier (BankID internal)
Fallback for pid
pid
string
Norwegian fødselsnummer (11 digits)
Hashed as users.national_id_hash (SHA-256)
name
string
Full name (e.g., "Ola Nordmann")
Split into users.first_name + users.last_name
birthdate
string
ISO date (not always present)
Parsed from pid instead (more reliable)
iss
string
Issuer URL
Validated against expected issuer
aud
string
Client ID
Validated against BANKID_CLIENT_ID
exp
number
Token expiry
Validated (must be in future)
iat
number
Issued at
Validated (must be reasonable)
nonce
string
Anti-replay
Matched against value sent in auth request
4.2 PID (Fødselsnummer) Processing
The pid (personal identification number) is an 11-digit Norwegian national ID that encodes the birthdate:
Format: DDMMYYNNNCC
DD = Day of birth (01-31)
MM = Month of birth (01-12)
YY = Year of birth (2 digits)
NNN = Individual number (000-999)
CC = Check digits (Luhn variant)
Century derivation (from individual number):
NNN 000-499 → born 1900-1999
NNN 500-999 → born 2000-2099
Source: bankid.ts:37-51 ( parseBirthdateFromPid )
Processing pipeline:
Parse birthdate from pid digits → ISO date string ( YYYY-MM-DD )
Verify age >= 18 by comparing birthdate to current date ( isOver18() )
Hash pid with SHA-256 for storage: crypto.createHash("sha256").update(pid).digest("hex")
Never store raw pid — only the hash exists in the database ( users.national_id_hash )
4.3 User Deduplication
Users are identified by national_id_hash (SHA-256 of their fødselsnummer). This ensures:
The same person logging in on different devices gets the same account
Re-authentication after token expiry reconnects to the existing account
Future Vipps Login integration can match users by the same pid hash
Source: bankid.ts:208-249 ( findOrCreateUser )
5. User Creation (Auto-Registration)
BankID login automatically creates user accounts. There is no separate registration step.
Field
Value
Source
id
usr_
randomId("usr")
email
usr_xxx@bankid.drop.local
Placeholder (BankID doesn't provide email)
password_hash
EIDONLY
Sentinel — no password authentication
auth_provider
bankid
Indicates BankID-only auth
first_name
First word of name claim
Split from BankID name
last_name
Remaining words of name claim
Split from BankID name
date_of_birth
Parsed from pid
parseBirthdateFromPid(pid)
kyc_status
approved
BankID = verified identity
kyc_method
bankid
KYC via BankID
kyc_verified_at
Current timestamp
Set on creation
national_id_hash
SHA-256(pid)
For user deduplication
role
user
Default; upgradable to merchant
6. SCA Compliance
6.1 PSD2 SCA Requirements Met by BankID
PSD2 RTS Requirement
BankID Implementation
Status
Two of three factors (knowledge, possession, inherence)
BankID app (possession) + personal code (knowledge) or biometrics (inherence)
Met
Independence of factors
Separate device (BankID app) + separate knowledge factor
Met
Dynamic linking (for PISP)
Amount and payee displayed in BankID app during payment approval
Met (ASPSP-side)
Authentication code specific to amount + payee
BankID generates unique code per transaction
Met (ASPSP-side)
Session timeout
BankID sessions expire (configurable by ASPSP)
Met
Max 5 failed attempts
BankID locks after failed attempts
Met (BankID-side)
6.2 Drop's SCA Scope
Drop performs SCA at two levels:
Drop authentication (login): BankID OIDC — satisfies SCA for account access
Payment SCA (PISP): Delegated to ASPSP — user re-authenticates with BankID at the bank for each payment (see open-banking-aisp-pisp.md )
7. Error Handling
7.1 Error Matrix
Error Scenario
HTTP Status
Error Code
User Message (Norwegian)
Recovery Action
BankID auth cancelled by user
400
bankid_cancelled
"Du avbrøt BankID-innlogging."
Retry login
BankID auth timeout
408
bankid_timeout
"BankID-sesjonen utløp. Prøv igjen."
Retry login
State mismatch (CSRF)
403
state_mismatch
"Sikkerhetssjekk feilet. Prøv igjen."
Restart flow
Token exchange failed
502
token_exchange_failed
"Kunne ikke koble til BankID. Prøv igjen."
Retry later
JWKS verification failed
502
jwks_verification_failed
"Teknisk feil. Prøv igjen senere."
Alert ops, retry
Invalid pid format
422
invalid_pid
"Ugyldig identifikasjon fra BankID."
Contact support
User under 18
403
underage
"Du må være minst 18 år for å bruke Drop."
No recovery — legal requirement
BANKID_CLIENT_ID missing
500
config_error
"Teknisk feil. Prøv igjen senere."
Fix server config
Session revoked
401
session_revoked
"Sesjonen din er utløpt. Logg inn på nytt."
Re-authenticate
JWT expired
401
token_expired
"Sesjonen din er utløpt. Logg inn på nytt."
Refresh or re-authenticate
Rate limited
429
rate_limited
"For mange forsøk. Vent litt og prøv igjen."
Wait and retry
7.2 Error Flow
flowchart TD
A[BankID auth attempt] --> B{Auth successful?}
B -->|Yes| C[Exchange code for tokens]
B -->|No: cancelled| D[Return bankid_cancelled]
B -->|No: timeout| E[Return bankid_timeout]
C --> F{Token exchange OK?}
F -->|Yes| G[Verify ID token via JWKS]
F -->|No| H[Return token_exchange_failed
Log error, alert ops]
G --> I{JWKS valid?}
I -->|Yes| J[Extract pid from claims]
I -->|No| K[Return jwks_verification_failed
Check JWKS URL, cert rotation]
J --> L{Valid pid format?}
L -->|Yes| M{Age >= 18?}
L -->|No| N[Return invalid_pid]
M -->|Yes| O[findOrCreateUser]
M -->|No| P[Return underage
403 Forbidden]
O --> Q[Create session + JWT]
Q --> R[Redirect to /dashboard]
8. Mock Mode (Development)
When BANKID_MOCK=true , the OIDC flow is simulated locally without contacting BankID:
Mock Code Prefix
Mock User
Birthdate
Age Check
underage*
"Ung Testbruker" (pid: 01011012345 )
2010-01-01
Fails (under 18)
(anything else)
"Test Bankersen" (pid: 01019012345 )
1990-01-01
Passes
Source: bankid.ts:188-195 ( getMockUserInfo )
Mock flow:
initiateOIDC() generates a redirect URL (same structure, but not called)
Frontend skips BankID redirect, posts a mock code to callback
exchangeAndVerify() detects isMock=true and returns mock user info
findOrCreateUser() runs normally (creates real DB records)
9. Production Migration Checklist
Step
Description
Status
1
Register BankID OIDC client at developer.bankid.no
Not started
2
Obtain BANKID_CLIENT_ID and BANKID_CLIENT_SECRET
Not started
3
Configure callback URLs (web + mobile deep link)
Not started
4
Set BANKID_MOCK=false (or remove env var)
Pending
5
Test OIDC flow in BankID preprod environment
Not started
6
Configure JWKS caching (jose handles this, verify TTL)
Pending
7
Set secure JWT_SECRET (min 32 chars, from vault)
Pending
8
Verify nonce validation in callback
Implemented in code
9
Test underage rejection with real BankID test user
Not started
10
Test session revocation and re-authentication
Implemented in code
11
Move to production BankID endpoints
Not started
12
Monitor JWKS key rotation (BankID rotates keys)
Pending
10. Phase 2: Vipps Login (Planned)
Vipps Login uses the same OIDC protocol. Integration plan:
Register Vipps Login client at portal.vipps.no
Add Vipps OIDC endpoints alongside BankID
Vipps also returns Norwegian pid (fødselsnummer)
User deduplication via national_id_hash — same hash regardless of whether user logs in with BankID or Vipps
UI: Login screen offers two buttons: "Logg inn med BankID" and "Logg inn med Vipps"
Optional: Use an OIDC aggregator like Idura to get a single integration point for BankID + Vipps + future providers.
11. Environment Variables
Required (Production)
Variable
Description
Example
BANKID_CLIENT_ID
OIDC client ID from BankID
abc123-def456
BANKID_CLIENT_SECRET
OIDC client secret
secret-from-bankid
BANKID_CALLBACK_URL
Web callback URL
https://getdrop.no/api/auth/bankid/callback
BANKID_CALLBACK_URL_MOBILE
Mobile deep link callback
drop://auth/callback
JWT_SECRET
Drop JWT signing secret (min 32 chars)
(from Vaultwarden)
Optional
Variable
Description
Default
BANKID_AUTHORIZE_URL
Override authorize endpoint
BankID prod URL
BANKID_TOKEN_URL
Override token endpoint
BankID prod URL
BANKID_JWKS_URL
Override JWKS endpoint
BankID prod URL
BANKID_ISSUER
Override expected issuer
BankID prod issuer
BANKID_MOCK
Enable mock mode ( true / false )
false
JWT_ALGORITHM
JWT signing algorithm
HS256
JWT_EXPIRY
Token lifetime
24h
12. Cross-References
Authentication System: ../../backend/AUTHENTICATION.md — Full auth system documentation
Open Banking AISP/PISP: open-banking-aisp-pisp.md — ASPSP SCA (separate from BankID login SCA)
Security Architecture: ../hld/security-architecture.md — JWT security, session management
Login Flow (LLD): ../lld/flow-login-authentication.md — Step-by-step login UX
Database Schema: ../../backend/DATABASE-SCHEMA.md — users , sessions tables
Source: src/drop-api/src/lib/bankid.ts — BankID OIDC implementation