Security Architecture
Security Architecture — High-Level Design
Version: 1.0
Date: 2026-02-21
Author: Banking Architecture Team
Status: Approved
Applies to: Drop — Security Threat Model & Controls
1. Overview
Drop is a PSD2-regulated fintech application that processes financial transactions (remittance, QR payments) without holding customer funds. This document defines the security architecture: trust boundaries, threat model (STRIDE), SCA implementation, fraud detection, AML screening, data classification, and encryption strategy.
Security posture summary:
- All authentication via BankID OIDC (SCA by default)
- All payment SCA delegated to ASPSP (user's bank)
- JWT tokens in httpOnly cookies (web) or AsyncStorage (mobile)
- Parameterized SQL queries (no string concatenation)
- Input sanitization on all user-facing endpoints
- Compliance tables for audit, AML, STR, screening, consents, GDPR
2. Trust Boundaries
graph TB
subgraph Internet["Internet (Untrusted)"]
Browser["Web Browser"]
Mobile["Mobile App (Expo)"]
Attacker["Potential Attacker"]
end
subgraph CDN["CDN / Edge (Cloudflare)"]
WAF["WAF + DDoS Protection"]
TLS["TLS Termination"]
end
subgraph AppTier["Application Tier (AWS App Runner)"]
subgraph NextJS["Next.js BFF"]
WebRoutes["Web API Routes<br/>/api/auth/*, /api/transactions/*"]
Middleware["Auth Middleware<br/>Rate Limiter<br/>CSRF Validator<br/>Input Sanitizer"]
end
subgraph Hono["Hono API"]
MobileRoutes["Mobile API Routes<br/>/v1/auth/*, /v1/transactions/*"]
HonoMiddleware["Auth Middleware<br/>Rate Limiter"]
end
end
subgraph DataTier["Data Tier (Private Subnet)"]
SQLite["SQLite / PostgreSQL<br/>19 tables (12 core + 7 compliance)"]
end
subgraph ExternalServices["External Services (Trusted Partners)"]
BankID["BankID OIDC<br/>(auth.bankid.no)"]
ASPSP["ASPSPs<br/>(DNB, SpareBank 1, Nordea)"]
FX["FX Rate Provider"]
KYC["KYC Provider<br/>(Sumsub - future)"]
end
Browser -->|"HTTPS<br/>TB1: Internet→Edge"| WAF
Mobile -->|"HTTPS<br/>TB1: Internet→Edge"| WAF
Attacker -.->|"Blocked by WAF"| WAF
WAF -->|"TB2: Edge→App"| Middleware
WAF -->|"TB2: Edge→App"| HonoMiddleware
Middleware --> WebRoutes
HonoMiddleware --> MobileRoutes
WebRoutes -->|"TB3: App→Data"| SQLite
MobileRoutes -->|"TB3: App→Data"| SQLite
WebRoutes -->|"TB4: App→External<br/>mTLS"| BankID
WebRoutes -->|"TB4: App→External<br/>eIDAS cert"| ASPSP
MobileRoutes -->|"TB4: App→External"| BankID
style Internet fill:#ff6b6b,stroke:#333,color:#fff
style CDN fill:#ffd93d,stroke:#333
style AppTier fill:#6bcb77,stroke:#333
style DataTier fill:#4d96ff,stroke:#333,color:#fff
style ExternalServices fill:#845ec2,stroke:#333,color:#fff
Trust Boundary Definitions
| Boundary |
From |
To |
Protection |
| TB1: Internet to Edge |
Browser/Mobile |
Cloudflare |
TLS 1.3, WAF rules, DDoS mitigation |
| TB2: Edge to Application |
Cloudflare |
Next.js/Hono |
HTTPS, auth middleware, rate limiting |
| TB3: Application to Data |
API layer |
SQLite/PostgreSQL |
Parameterized queries, file permissions |
| TB4: Application to External |
API layer |
BankID/ASPSP |
mTLS (eIDAS QWAC), JWKS verification |
3. STRIDE Threat Model
3.1 Threat Matrix
| Component |
Spoofing |
Tampering |
Repudiation |
Info Disclosure |
DoS |
Elevation |
| BankID Auth |
L: BankID handles identity |
L: JWKS signature verification |
L: Audit log + session tracking |
M: pid hash exposure risk |
M: Rate limit 10/min |
L: Role check on every request |
| JWT Tokens |
M: Token theft via XSS |
L: HS256 signature |
L: Session table tracks all JWTs |
M: Payload contains userId |
L: 7d expiry |
M: Role claim in JWT |
| PISP Payments |
L: SCA required per payment |
M: Amount/payee tampering |
L: Audit log + idempotency_key |
L: Disclosure before payment |
M: Rate limit 10/min |
L: KYC check before remittance |
| AISP Balance |
L: Consent required |
L: Read-only from ASPSP |
L: balance_synced_at tracking |
M: Cached balance visible |
L: Max 4 reads/day |
N/A |
| Database |
L: No direct access |
M: SQL injection risk |
L: audit_log table |
H: PII in users table |
L: Rate limiting |
L: User-scoped queries |
| API Endpoints |
M: CSRF on web |
M: Input manipulation |
L: Audit logging |
M: Error message leakage |
H: Unthrottled endpoints |
M: IDOR if user_id not checked |
Risk levels: L = Low (mitigated), M = Medium (partial mitigation), H = High (needs attention), N/A = Not applicable
3.2 Detailed Threat Analysis
S — Spoofing
| Threat |
Attack Vector |
Mitigation |
Status |
| Identity spoofing |
Stolen credentials |
BankID OIDC (SCA: possession + knowledge) |
Implemented |
| Session hijacking |
Token theft |
httpOnly + secure + sameSite=Lax cookies |
Implemented |
| CSRF |
Forged cross-origin request |
State parameter (OIDC), Origin header validation |
Implemented |
| Replay attack |
Reuse old auth code |
Nonce in OIDC flow, one-time code exchange |
Implemented |
T — Tampering
| Threat |
Attack Vector |
Mitigation |
Status |
| SQL injection |
Malicious input in queries |
Parameterized queries (all 24 endpoints) |
Implemented |
| XSS |
Script injection in fields |
React auto-escaping, CSP headers, sanitizeText() |
Implemented |
| Payment amount tampering |
Modified request body |
Server-side validation, SCA dynamic linking |
Implemented |
| JWT modification |
Altered token claims |
HS256 signature verification |
Implemented |
R — Repudiation
| Threat |
Attack Vector |
Mitigation |
Status |
| Deny transaction |
User claims they didn't authorize |
BankID SCA log + audit_log table |
Partial (audit_log exists, SCA tracking needed) |
| Deny consent |
User claims no consent given |
consents table with IP address + timestamp |
Implemented |
| Admin action denial |
Unauthorized changes |
audit_log with user_agent and ip_address |
Implemented |
| Threat |
Attack Vector |
Mitigation |
Status |
| PII exposure |
Database breach |
Encryption at rest (planned), PID hashed with SHA-256 |
Partial |
| Card data exposure |
API response leakage |
Masked to last 4 digits, CVV hidden |
Implemented |
| Bank account exposure |
API response leakage |
Masked to last 4 digits in recipient list |
Implemented |
| Error message leakage |
Verbose error responses |
Centralized error handler, generic messages |
Implemented |
D — Denial of Service
| Threat |
Attack Vector |
Mitigation |
Status |
| API flooding |
High request volume |
Rate limiting (10-120/min per endpoint) |
Implemented |
| Auth brute force |
Repeated login attempts |
BankID handles (locks after failures) |
Implemented |
| Database exhaustion |
Large data queries |
Pagination (max 50/page), query limits |
Implemented |
| Resource exhaustion |
Large payloads |
Input length limits (sanitizeText) |
Implemented |
E — Elevation of Privilege
| Threat |
Attack Vector |
Mitigation |
Status |
| IDOR |
Access other user's data |
AND user_id = ? on all queries |
Implemented |
| Role escalation |
Modify role claim |
Server-side role check, role in DB not just JWT |
Implemented |
| Merchant impersonation |
Access merchant dashboard |
role = 'merchant' check on merchant routes. Note: merchant role currently grants admin access (audit, screening, STR) via isAdmin(role) === role === 'merchant' in admin.ts |
Implemented |
| KYC bypass |
Skip verification |
kyc_status = 'approved' check before remittance |
Implemented |
4. SCA Implementation
4.1 Two-Level SCA
Drop implements SCA at two levels:
| Level |
Purpose |
Provider |
Method |
| App Authentication |
Login to Drop |
BankID OIDC |
BankID app (possession) + code/biometrics (knowledge/inherence) |
| Payment Authorization |
Approve PISP payment |
ASPSP via BankID |
BankID at bank (dynamic linking: amount + payee) |
4.2 SCA Factors
| Factor Type |
BankID Implementation |
| Knowledge |
Personal code / PIN |
| Possession |
Mobile device with BankID app / code generator |
| Inherence |
Biometrics (fingerprint/face on mobile BankID) |
PSD2 RTS Art. 4: At least 2 of 3 factors required. BankID provides 2 by default (possession + knowledge or inherence).
4.3 Dynamic Linking (PISP)
For every PISP payment, PSD2 RTS Art. 97(2) requires:
- User sees exact amount and payee name during SCA
- Authentication code is cryptographically bound to amount + payee
- Any change to amount or payee invalidates the authentication
This is handled by the ASPSP's BankID integration — Drop passes instructedAmount and creditorName in the PISP API call, and the bank displays these during BankID authentication.
5. Fraud Detection Pipeline
flowchart TD
A[Transaction Request] --> B[Pre-Transaction Checks]
B --> C{User KYC Status}
C -->|pending/rejected| D[REJECT: kyc_required]
C -->|approved| E[Amount Validation]
E --> F{Amount in range?}
F -->|No| G[REJECT: validation_error]
F -->|Yes| H[Velocity Check]
H --> I{Exceeds daily/weekly limit?}
I -->|Yes| J[FLAG: velocity_alert<br/>Insert into aml_alerts<br/>severity: medium]
I -->|No| K[Pattern Analysis]
K --> L{Structuring detected?<br/>Multiple txns just below threshold}
L -->|Yes| M[FLAG: structuring_alert<br/>Insert into aml_alerts<br/>severity: high]
L -->|No| N[Corridor Risk Check]
N --> O{High-risk corridor?}
O -->|Yes| P[Enhanced due diligence<br/>FLAG if first-time corridor]
O -->|No| Q[Recipient Screening]
Q --> R{Recipient on sanctions list?}
R -->|Yes| S[BLOCK: sanctions_match<br/>Insert into screening_results<br/>result: match]
R -->|No| T[APPROVE: Proceed to PISP]
J --> T
M --> U[Escalate to compliance officer<br/>Insert into str_reports<br/>status: draft]
P --> T
style D fill:#ff6b6b,color:#fff
style G fill:#ff6b6b,color:#fff
style S fill:#ff6b6b,color:#fff
style T fill:#6bcb77,color:#fff
style J fill:#ffd93d
style M fill:#ffd93d
style U fill:#ff9f43
5.1 Detection Rules
| Rule |
Trigger |
Severity |
Action |
Velocity limit (checkVelocity) |
> 5 transactions in 1 hour |
Medium |
aml_alerts record, continue with flag |
Structuring detection (checkStructuring) |
3+ transactions in 24h totaling > 50,000 NOK |
High |
aml_alerts + str_reports draft |
High-value single (checkHighAmount) |
Single transaction > 100,000 NOK |
High |
Enhanced monitoring, aml_alerts record |
High-risk corridor (checkHighRiskCorridor) |
Country on FATF grey/black list |
High |
Enhanced due diligence required |
Unusual pattern (checkUnusualPattern) |
Transaction amount > 5x user's average |
Medium |
aml_alerts record |
| Sanctions match |
Recipient matches sanctions list |
Critical |
Block transaction, escalate |
| PEP match |
User matches PEP database |
High |
Enhanced due diligence |
These rules are implemented in transaction-monitor.ts and run on each remittance creation.
5.2 AML Screening Tables
| Table |
Purpose |
Key Columns |
aml_alerts |
Transaction monitoring flags |
alert_type, severity, status (open/investigating/resolved/escalated/filed) |
str_reports |
Suspicious Transaction Reports to authorities |
report_type, status (draft/submitted/acknowledged), reference_number |
screening_results |
PEP/sanctions/adverse media checks |
screening_type, result (clear/match/potential_match/error) |
6. Data Classification
6.1 Classification Levels
| Level |
Description |
Examples |
Storage |
Access |
| CRITICAL |
Financial credentials, encryption keys |
JWT_SECRET, BANKID_CLIENT_SECRET, eIDAS private keys |
Vaultwarden only |
Application runtime only |
| RESTRICTED |
PII subject to GDPR |
name, email, phone, date_of_birth, national_id_hash |
Encrypted at rest (planned), DB access layer |
Authenticated user (own data only) |
| CONFIDENTIAL |
Financial data |
transactions, bank balances, exchange rates, fees |
DB with user-scoped access |
Authenticated user (own data only) |
| INTERNAL |
Operational data |
audit_log, rate_limits, sessions |
DB |
System processes, compliance officers |
| PUBLIC |
Non-sensitive |
exchange rates (GET /api/rates), health check |
DB / API |
Unauthenticated |
6.2 Data Classification by Table
| Table |
Classification |
PII Fields |
Encryption at Rest |
Retention |
users |
RESTRICTED |
email, first_name, last_name, phone, date_of_birth, national_id_hash |
Planned |
5 years post-deletion (AML) |
bank_accounts |
RESTRICTED |
account_number, iban |
Planned |
Active + 5 years |
transactions |
CONFIDENTIAL |
amount, recipient details |
Planned |
5 years (AML/tax) |
recipients |
RESTRICTED |
name, bank_account |
Planned |
Active + 5 years |
sessions |
INTERNAL |
token_hash |
N/A (hash only) |
30 days |
audit_log |
INTERNAL |
ip_address, user_agent |
Planned |
5 years |
aml_alerts |
CONFIDENTIAL |
details |
Planned |
5 years |
str_reports |
CONFIDENTIAL |
details, reference_number |
Planned |
10 years |
screening_results |
CONFIDENTIAL |
match_details |
Planned |
5 years |
consents |
RESTRICTED |
ip_address |
Planned |
Until withdrawn + 5 years |
merchants |
CONFIDENTIAL |
None (business data) |
Planned |
Active + 5 years |
cards |
RESTRICTED |
last_four, token_ref |
Planned |
Active + 5 years |
data_access_requests |
INTERNAL |
None (metadata only) |
N/A |
5 years |
complaints |
INTERNAL |
None (user text) |
Planned |
5 years |
notifications |
INTERNAL |
None |
N/A |
90 days |
settings |
INTERNAL |
None (preferences) |
N/A |
Active |
spending_limits |
INTERNAL |
None |
N/A |
Active |
exchange_rates |
PUBLIC |
None |
N/A |
Indefinite |
rate_limits |
INTERNAL |
None |
N/A |
Transient |
7. Encryption
7.1 Encryption in Transit
| Connection |
Protocol |
Certificate |
| Browser to Drop |
TLS 1.3 (Cloudflare) |
Cloudflare managed |
| Mobile to Drop |
TLS 1.3 |
Cloudflare managed |
| Drop to BankID |
TLS 1.2+ |
BankID server cert |
| Drop to ASPSP |
mTLS (eIDAS QWAC) |
Qualified Website Authentication Certificate |
| Drop to Database |
N/A (SQLite local) / TLS (PostgreSQL) |
PostgreSQL server cert |
7.2 Encryption at Rest
| Data |
Current |
Target |
| PostgreSQL 16 (all environments) |
AWS RDS encryption (AES-256, TLS 1.3) |
Active |
| Secrets (JWT_SECRET, etc.) |
Vaultwarden |
Vaultwarden + AWS Secrets Manager |
| Backups |
Not encrypted |
AES-256 encrypted backups |
| Logs |
Plain text |
Encrypted log storage |
7.3 Key Management
| Key |
Purpose |
Storage |
Rotation |
JWT_SECRET |
Sign Drop JWTs |
Vaultwarden / env var |
Every 90 days |
BANKID_CLIENT_SECRET |
BankID OIDC client auth |
Vaultwarden / env var |
Per BankID policy |
| eIDAS QWAC private key |
mTLS to ASPSPs |
HSM (planned) |
Per certificate lifecycle |
| eIDAS QSeal private key |
Sign API requests |
HSM (planned) |
Per certificate lifecycle |
qr_hmac_key (merchants) |
HMAC for QR code verification |
DB (merchants table) |
Per merchant, on creation |
7.4 Hashing
| Data |
Algorithm |
Purpose |
Source |
| Passwords |
bcrypt (cost 12) |
Password verification |
utils-server.ts:8-16 |
| National ID (pid) |
SHA-256 |
User deduplication |
bankid.ts:211 |
| JWT tokens |
SHA-256 |
Session lookup |
auth.ts:59 |
| PIN codes |
bcrypt |
Card PIN verification |
cards/[id]/pin/route.ts |
8. Security Controls Summary
8.1 Application Security
| Control |
Implementation |
Source |
| Authentication |
BankID OIDC (SCA) |
bankid.ts, auth.ts |
| Authorization |
JWT + role check + user_id scoping |
middleware/auth.ts |
| Input validation |
sanitizeText, validateName, validateAmount, etc. |
middleware/validation.ts |
| SQL injection prevention |
Parameterized queries (all endpoints) |
db.ts |
| XSS prevention |
React auto-escaping + CSP + sanitization |
next.config.ts, validation.ts |
| CSRF prevention |
Origin validation + sameSite=Lax cookies |
app.ts:23-30 (CORS) |
| Rate limiting |
Per-IP, persistent (SQLite-backed) |
middleware/rate-limit.ts |
| Session management |
Server-side tracking with revocation |
sessions table, auth.ts |
8.2 Infrastructure Security
| Control |
Implementation |
Status |
| TLS 1.3 |
Cloudflare edge |
Active (landing page) |
| WAF |
Cloudflare WAF rules |
Active (landing page) |
| DDoS protection |
Cloudflare automatic |
Active |
| HSTS |
max-age=63072000; includeSubDomains; preload |
Configured (next.config.ts) |
| X-Frame-Options |
DENY |
Configured |
| X-Content-Type-Options |
nosniff |
Configured |
| Referrer-Policy |
strict-origin-when-cross-origin |
Configured |
| Permissions-Policy |
Camera (self), microphone (none), geolocation (self) |
Configured |
8.3 Compliance Controls
| Control |
Implementation |
Table |
| Audit trail |
All significant actions logged |
audit_log |
| AML monitoring |
Transaction pattern detection |
aml_alerts |
| STR filing |
Suspicious transaction reports |
str_reports |
| PEP/sanctions screening |
Automated list checking |
screening_results |
| GDPR consent tracking |
Consent grant/withdraw with IP |
consents |
| Data access requests |
GDPR Art. 15-17 |
data_access_requests |
| Complaint handling |
Finansavtaleloven compliance |
complaints |
9. Security Audit Results
9.1 Pre-Hardening (2026-02-12)
| Severity |
Count |
| CRITICAL |
4 |
| HIGH |
5 |
| MEDIUM |
6 |
| LOW |
4 |
9.2 Post-Hardening (2026-02-13)
| Severity |
Count |
Details |
| CRITICAL |
0 |
All resolved |
| HIGH |
0 |
All resolved |
| MEDIUM |
2 |
CSP tightening (nonce-based), proxy config |
| LOW |
4 |
Acknowledged, out of scope for MVP |
| Finding |
Fix |
Source |
| C1: Card data stored in plain |
Now stores only last_four + token_ref |
Schema change |
| C2: Demo credentials in production |
Gated behind NODE_ENV !== 'production' (note: SEED_DEMO=true can override this check) |
db.ts:241 |
| C4: SHA-256 password hashes |
Removed entirely, bcrypt only |
utils-server.ts |
| C6/H1: No session revocation |
Implemented in sessions table |
auth.ts:56-65 |
| H4: No input sanitization |
sanitizeText() on all text fields |
validation.ts |
| M5: Notification ID injection |
Validated format + max 100 per request |
notifications/route.ts |
| M6: Settings value injection |
Currency/language whitelists |
settings/route.ts |
10. Cross-References
No comments to display
No comments to display