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:
- User has
role = 'merchant' - 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 |