Skip to main content

Security Architecture

Security Architecture Document

Project: Drop — PSD2 Pass-Through Payment App Version: 1.0 Date: 2026-02-23 Author: Security Architect Status: Draft Reviewers: CTO, DPO, Engineering Lead Classification: Confidential

Document History

Version Date Author Changes
0.1 2026-02-23 Security Architect Initial draft from security audit + hardening implementation

1. Security Architecture Overview

Security Owner: CISO / Security Architect ([email protected]) Last Security Review: 2026-02-13 (post-hardening) Next Scheduled Review: 2026-08-23 Compliance Targets: GDPR / personopplysningsloven | PSD2 / betalingstjenesteloven | hvitvaskingsloven (AML) | IKT-forskriften / DORA | Finanstilsynet licensing

Architecture Model: Drop is a PSD2 pass-through payment app (AISP + PISP). It never holds customer money. AISP reads bank balances via Open Banking. PISP initiates payments from the user's bank account. Cards are a future feature gated behind feature flags (all default to false).

Defense-in-Depth Overview

flowchart TB
    subgraph Perimeter["Perimeter Security"]
        DNS[DNS / DDoS Protection\nCloudflare WAF]
        WAF[Web Application Firewall\nCloudflare WAF Rules]
        CDN[CDN — Edge TLS 1.3 Termination\nCloudflare]
    end

    subgraph Network["Network Security"]
        LB[AWS App Runner\nTLS 1.3]
        SG[Security Groups / NACLs]
        VPC[Private VPC\nNo public subnet for data layer]
    end

    subgraph Application["Application Security"]
        BANKID[BankID OIDC\nNorwegian eID — Level 4]
        AUTH[JWT Auth Service\nHS256, httpOnly cookies]
        AUTHZ[Authorization Layer\nRBAC user/merchant/admin + AND user_id=?]
        VALID[Input Validation\nZod schemas + sanitizeText()]
        RATE[Rate Limiting\nSQLite-backed, per-IP]
    end

    subgraph Data["Data Security"]
        ENCRYPT[Encryption at Rest\nAES-256]
        TRANSIT[Encryption in Transit\nTLS 1.3]
        MASK[PII Masking\nLast-4 only for cards/bank accounts]
        VAULT[Secrets Management\nAWS Secrets Manager + JWT_SECRET env]
    end

    subgraph Monitoring["Security Monitoring"]
        SENTRY[Error Monitoring\nSentry]
        BETTERSTACK[Uptime + Log Monitoring\nBetterStack]
        ALERTS[Security Alerts\nPagerDuty escalation]
        AUDIT[Audit Trail\nSession table + structured logs]
    end

    DNS --> WAF --> CDN --> LB
    LB --> SG --> VPC
    VPC --> BANKID --> AUTH --> AUTHZ --> VALID
    VALID --> RATE
    RATE --> Data
    Data --> Monitoring

2. Authentication Flows

2.1 BankID OIDC Authentication (Production Target)

sequenceDiagram
    autonumber
    actor User
    participant App as Drop App
    participant API as Drop API
    participant BankID as BankID OIDC Provider
    participant DB as User Store (PostgreSQL)

    User->>App: Tap "Login with BankID"
    App->>API: GET /auth/bankid/authorize
    API->>BankID: Redirect with PKCE code_challenge, scope=openid+profile+nnin
    User->>BankID: Authenticate with BankID (Level 4 — possession + knowledge + inherence)
    BankID->>API: Redirect with authorization_code
    API->>BankID: Exchange code for tokens (PKCE code_verifier)
    BankID-->>API: {id_token, access_token} — contains fødselsnummer, name, birthdate
    API->>API: Validate id_token signature + iss + aud + exp + nonce
    API->>API: Extract name, fødselsnummer; verify age >= 18 (from birthdate)
    API->>DB: Upsert user (name, phone, bankid_ref); check KYC status
    API->>API: Generate JWT (HS256, 24h), create session record
    API-->>App: Set httpOnly cookie (JWT) + 200 OK
    App->>App: Render dashboard

2.2 Current Email/Password Login (MVP — pre-BankID)

sequenceDiagram
    autonumber
    actor User
    participant FE as Drop Frontend
    participant API as Drop API (src/drop-app)
    participant DB as SQLite / PostgreSQL

    User->>FE: Enter email + password
    FE->>API: POST /api/auth/login {email, password}
    API->>API: Rate limit check (10 req/60s per IP — middleware.ts)
    API->>API: CSRF Origin header validation
    API->>DB: SELECT user WHERE email = ?
    DB-->>API: User record
    API->>API: bcrypt.compare(password, hash) — cost factor 12
    alt Invalid credentials
        API-->>FE: 401 — Generic "Invalid credentials" (no user enumeration)
    end
    API->>DB: INSERT INTO sessions (id, user_id, token_hash, expires_at, revoked=0)
    API->>API: Generate JWT HS256 — setIssuedAt(), 24h expiry
    API-->>FE: Set httpOnly=true, secure=true, sameSite=strict cookie
    FE->>FE: Render dashboard (no token in localStorage)

2.3 Token Refresh & Session Revocation

sequenceDiagram
    autonumber
    actor FE as Drop Frontend
    participant API as Drop API
    participant DB as Session Store

    FE->>API: Any authenticated request (httpOnly cookie sent automatically)
    API->>API: Verify JWT signature + expiry (jose library)
    API->>DB: SELECT sessions WHERE token_hash = SHA256(jwt) AND revoked = 0
    alt Session revoked or expired
        API-->>FE: 401 — Force re-login
    end
    API->>API: Extract userId, proceed with request

    Note over FE,DB: On logout — all sessions revoked
    FE->>API: POST /api/auth/logout
    API->>DB: UPDATE sessions SET revoked=1 WHERE user_id = ?
    API-->>FE: Clear httpOnly cookie

2.4 MFA — Dynamic Linking for PSD2 Compliance (Phase 2)

BankID inherently provides possession + knowledge (two factors). For PSD2 Strong Customer Authentication (SCA):

Factor BankID Element Notes
Knowledge BankID PIN / password User-memorized secret
Possession BankID app / hardware token Registered device
Dynamic linking Amount + payee shown in BankID signing dialog Prevents transaction substitution

Current state (MVP): Email/password only — no SCA compliance. BankID integration is Phase 2.


3. Authorization Model

Model: RBAC (Role-Based Access Control) with mandatory user-scoped data isolation

3.1 Roles & Permissions Matrix

Permission user merchant admin
View own transactions
Initiate remittance
Initiate QR payment
Manage recipients (own)
View merchant dashboard
View merchant transactions ✓ (own) ✓ (all)
Manage merchant settings ✓ (own)
Admin functions
View all users

KYC Status Gates (required for financial operations):

KYC Status Remittance QR Payment Balance Read
pending Blocked Blocked Blocked
approved Allowed Allowed Allowed
rejected Blocked Blocked Blocked

3.2 Resource-Level Conditions (IDOR Protection)

All data access queries include mandatory user scoping. Source: src/drop-app/src/lib/:

// All recipient queries — IDOR prevention
db.prepare("SELECT * FROM recipients WHERE id = ? AND user_id = ?").get(id, userId);

// All transaction queries
db.prepare("SELECT * FROM transactions WHERE id = ? AND user_id = ?").get(id, userId);

// All bank account queries
db.prepare("SELECT * FROM bank_accounts WHERE user_id = ?").all(userId);

Merchant endpoints additionally verify:

  1. User has role = 'merchant'
  2. Merchant record belongs to authenticated user

3.3 Permission Hierarchy

admin (platform-level — full access)
  └── merchant (tenant-level — own data + merchant dashboard)
        └── user (standard — own data only)

4. Data Encryption

4.1 Encryption at Rest

Data Store Encryption Method Key Management
SQLite (current MVP) OS-level disk encryption Platform
PostgreSQL (Phase 2) AES-256-GCM (TDE via cloud provider) AWS KMS
PostgreSQL — fødselsnummer AES-256-GCM application-layer field encryption AWS KMS — separate key
PostgreSQL — KYC documents AES-256-GCM AWS KMS
AWS S3 backups SSE-KMS (AES-256) AWS KMS — backup key
BankID certificates AWS Certificate Manager AWS-managed
JWT_SECRET AWS Secrets Manager AWS-managed

4.2 Encryption in Transit

TLS configuration (Cloudflare + AWS App Runner):

ssl_protocols TLSv1.3;
ssl_ciphers TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256;
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

All API traffic: HTTPS only (HTTP → 301 redirect). httpOnly cookies with secure: true in production.

4.3 Field-Level Encryption for PII

Fødselsnummer → AES-256-GCM, encrypted immediately on receipt from BankID
  Key: NATIONAL_ID_KEY (AWS KMS — separate from database master key)
  Stored: Only encrypted ciphertext in database
  Read: Decrypted only for KYC checks, by compliance role only

Bank account numbers → masked in API responses (last 4 digits only)
  Source: utils-server.ts:23-26 maskAccountNumber()

Card numbers → last_four + token_ref only (full PAN never stored)
  Note: Cards feature gated behind feature flags, all default false

5. Network Security

5.1 Network Architecture

Internet
  → Cloudflare DDoS Protection + WAF
  → Cloudflare CDN (TLS 1.3 termination at edge)
  → AWS App Runner (re-encrypts, TLS 1.3)
  → Application containers (Next.js)
  → Database (SQLite → PostgreSQL in private subnet)

5.2 Firewall Rules

Source Destination Port Protocol Action
Internet Cloudflare edge 443 HTTPS ALLOW
Internet Any 80 HTTP REDIRECT → 443
Cloudflare AWS App Runner 443 HTTPS ALLOW
App Runner PostgreSQL (Phase 2) 5432 TCP ALLOW
Any PostgreSQL direct Any Any DENY
Any (unauthenticated) /api/auth/* 443 HTTPS Rate limited (10/60s)
Any (unauthenticated) /api/transactions/* 443 HTTPS Rate limited (10/60s)

5.3 WAF Rules (Cloudflare)

Rule Action Notes
OWASP Core Rule Set Block Managed Cloudflare ruleset
SQL injection Block Including encoded variants
XSS Block Including DOM-based
Bot traffic Challenge Suspicious patterns
Rate limiting Block >120 req/min from single IP to /api/rates
Geo-blocking Log (review) Sanctioned countries per OFAC/EU list

6. API Security

6.1 Rate Limiting Strategy

Source: src/drop-app/src/lib/middleware.ts:6-31

Endpoint Type Limit Window Implementation
Auth routes (login, register) 10 requests 60 seconds SQLite-backed, per-IP
Transaction routes (remittance, qr-payment) 10 requests 60 seconds SQLite-backed, per-IP
Rates endpoint (/api/rates) 120 requests 60 seconds SQLite-backed, per-IP

Note (Medium finding M3): X-Forwarded-For header is trusted from Cloudflare. Must validate only from trusted proxy IPs in production.

6.2 Input Validation

Source: src/drop-app/src/lib/middleware/validation.ts

// All text inputs sanitized
sanitizeText(input) → removes HTML tags, control chars, trims, enforces maxLength

// Financial validation
validateAmount(amount) → positive, finite, max 2 decimal places
  Remittance: 100 NOK ≤ amount ≤ 50,000 NOK
  QR Payment: 1 NOK ≤ amount ≤ 100,000 NOK
  Number.isFinite() prevents NaN/Infinity injection

// Identity validation
validateIBAN(iban) → format + checksum
validatePIN(pin) → exactly 4 digits
validateCurrency(currency) → whitelist: EUR, USD, GBP, BAM, CHF, PLN, NOK, RSD, TRY, PKR
validateLanguage(lang) → whitelist: nb, en, bs, sq

// SQL: ALL 24 endpoints use parameterized queries — zero string concatenation

6.3 CORS Policy

// Production CORS config
{
  origin: ['https://getdrop.no', 'https://app.getdrop.no'],
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,   // Required for httpOnly cookies
  maxAge: 86400
}

6.4 Content Security Policy

Source: src/drop-app/next.config.ts:6-46

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'unsafe-inline' 'unsafe-eval';
  frame-ancestors 'none';
  [remaining headers...]

Known issue (Medium M1): unsafe-inline and unsafe-eval required for Next.js. Production target: nonce-based CSP.


7. OWASP Top 10 Mitigation Matrix

OWASP Risk Mitigation Implementation Status
A01: Broken Access Control RBAC + user_id scoping on every query AND user_id = ? in all data queries; middleware.ts
A02: Cryptographic Failures TLS 1.3 + AES-256 + bcrypt(12) auth.ts, next.config.ts, IKT-sikkerhetspolicy
A03: Injection Parameterized queries exclusively All 24 API endpoints — zero string concatenation
A04: Insecure Design PSD2 pass-through model — no stored funds Architecture decision — Drop never holds money
A05: Security Misconfiguration Hardened headers + feature flags next.config.ts headers, feature-flags.ts ✓ (CSP partial)
A06: Vulnerable Components Recent dependency versions jose ^6.1.3, bcryptjs ^3.0.3, next 16.1.6
A07: Auth Failures Rate limiting + session revocation + bcrypt middleware.ts, sessions table, auth.ts
A08: Software Integrity Signed commits + CI/CD GitHub Actions pipeline Partial
A09: Logging Failures Sentry + BetterStack structured logs Error tracking + uptime monitoring Partial (no audit_log table yet)
A10: SSRF Input validation + allowlist for outbound validateIBAN, external API allowlist Partial

8. Security Headers Checklist

Source: src/drop-app/next.config.ts

Header Value Status
Strict-Transport-Security max-age=63072000; includeSubDomains; preload
Content-Security-Policy default-src 'self'; frame-ancestors 'none'; ... ✓ (unsafe-inline/eval TODO)
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy strict-origin-when-cross-origin
Permissions-Policy camera=(self), microphone=(), geolocation=(self)
Cache-Control (auth responses) no-store TODO
X-XSS-Protection 0 (rely on CSP) TODO

9. Dependency Vulnerability Management

Current dependency status (audit date: 2026-02-12):

Package Version Risk Notes
jose ^6.1.3 Low JWT library — well-maintained
bcryptjs ^3.0.3 Low Pure JS bcrypt — no native compilation
better-sqlite3 ^12.6.2 Low Parameterized queries
next 16.1.6 Low Recent version
react 19.2.3 Low Latest major
radix-ui ^1.4.3 Low UI components only

Scanning tools (planned):

  • SAST: Semgrep / CodeQL (runs on every PR)
  • SCA: Dependabot + npm audit (runs on every PR + daily on main)
  • Container: Trivy (runs on Docker image build)

Remediation SLAs:

Severity SLA Owner
Critical (CVSS ≥ 9.0) 24 hours Security + affected team
High (CVSS 7.0-8.9) 7 days Affected team
Medium (CVSS 4.0-6.9) 30 days Affected team
Low (CVSS < 4.0) 90 days Next sprint

10. Security Logging & Audit Trail

Current monitoring stack:

  • Sentry: Error tracking, exception monitoring, performance
  • BetterStack: Uptime monitoring, log aggregation, alerting
  • Sessions table: Token revocation, session lifecycle

Planned (pre-production):

Event Data Logged Retention Alert?
Login success user_id, ip, user_agent, timestamp 1 year No
Login failure ip, email_hash, attempt_count, timestamp 1 year Yes (> 5 failures)
BankID authentication user_id, bankid_ref, success/failure 1 year Yes (failure)
Password change user_id, ip, timestamp 1 year Yes
Session revocation user_id, session_ids, timestamp 1 year No
Transaction created user_id, amount, corridor, timestamp 5 years Yes (>50,000 NOK)
KYC status change user_id, old_status, new_status, operator 5 years Yes
AML flag triggered user_id, rule, transaction_id 5 years Yes (always)
Admin action actor_id, action, target, changes 2 years Yes

Log retention (hvitvaskingsloven § 30): Minimum 5 years for KYC and transaction data.

SIEM integration (Phase 2): BetterStack → Sentry → PagerDuty escalation chain.


Approval

Role Name Date Signature
Author Security Architect 2026-02-23
CISO / Security Lead
DPO
CTO