Middleware Design Document
Middleware Design Document
Project: Drop Version: 0.1.0 Date: 2026-02-23 Author: Platform Architect (AI) Status: In Review Reviewers: Alem Bašić (CEO)
Document History
| Version | Date | Author | Changes |
|---|---|---|---|
| 0.1 | 2026-02-23 | Platform Architect (AI) | Initial draft from source code analysis |
1. Overview
Drop has two middleware layers:
-
src/lib/middleware.ts— The active middleware used by all API routes. ProvidesrequireAuth,requireMerchant,rateLimit,getClientIp,jsonError, CSRF protection, and session revocation. -
src/lib/middleware/— A modular middleware library withauth-middleware.ts(Bearer token for mobile),error-handler.ts(AppError class), andvalidation.ts(input sanitization functions).
Both layers are used in production. Routes import from @/lib/middleware (auth, rate limiting) and @/lib/middleware/validation (input validation).
2. Active Middleware (lib/middleware.ts)
2.1 requireAuth(request?)
Source: middleware.ts:42–80
Authenticates the current request via cookie-based JWT.
Returns: { user: User, error: null } | { user: null, error: NextResponse }
Steps:
- CSRF origin check — if
Originheader present, must match allowed origins (NEXT_PUBLIC_APP_URL,http://localhost:3000,http://localhost:3001) - Cookie extraction — reads
drop_tokenfrom request cookies - JWT verification — validates HS256 signature and expiry using
joselibrary - User lookup — loads user from
userstable byuserIdfrom JWT payload - Session revocation check — verifies at least one non-revoked session exists for this user
Usage:
const { user, error } = await requireAuth(request);
if (error) return error; // Returns NextResponse with JSON error
// user is guaranteed non-null here
Error responses:
2.2 requireMerchant(request?)
Source: middleware.ts:101–108
Extends requireAuth with a merchant role check.
const { user, error } = await requireMerchant(request);
if (error) return error; // 401 if not authenticated, 403 if not merchant
Returns 403 forbidden if user exists but role !== 'merchant'.
Applied to: GET /api/merchants/dashboard, GET /api/merchants/qr, GET /api/merchants/transactions
2.3 rateLimit(ip, limit, windowMs?)
Source: middleware.ts:7–31
Persistent IP-based rate limiter using the rate_limits database table.
| Parameter | Default | Description |
|---|---|---|
ip |
— | Client IP address |
limit |
— | Max requests per window |
windowMs |
60,000ms | Window size in milliseconds |
Returns: boolean — true if request is allowed, false if rate limited.
Implementation:
- Uses
runUpsertfor atomic counter creation/update - Cleans expired entries on each call (removes rows where
expires_at < now) - Counter stored in
rate_limitstable:(key, count, expires_at)
Rate limit table schema:
CREATE TABLE rate_limits (
key TEXT PRIMARY KEY, -- IP address
count INTEGER DEFAULT 1,
expires_at INTEGER -- Unix timestamp (ms)
);
Usage:
const ip = getClientIp(request);
if (!(await rateLimit(ip, 10))) { // 10 req/min
return jsonError("rate_limited", "Too many requests", 429);
}
Applied limits:
| Endpoint | Limit | Window |
|---|---|---|
/api/auth/bankid/initiate |
10/min | 60s |
/api/auth/bankid/callback |
10/min | 60s |
/api/auth/register (deprecated) |
10/min | 60s |
/api/auth/login (deprecated) |
10/min | 60s |
/api/transactions/remittance |
10/min | 60s |
/api/transactions/qr-payment |
10/min | 60s |
/api/rates |
120/min | 60s |
/api/rates/[currency] |
120/min | 60s |
2.4 getClientIp(request)
Source: middleware.ts:33–35
Extracts the client's real IP address from the x-forwarded-for header (first IP in the chain — the originating client). Falls back to '127.0.0.1' if header not present.
Note: When behind App Runner (AWS managed proxy), x-forwarded-for is set automatically with the real client IP.
2.5 jsonError(error, message, status, details?)
Source: middleware.ts:37–39
Creates a standardized JSON error NextResponse.
return jsonError("validation_error", "Validation failed", 422, ["Email required"]);
// Response body: { "error": "validation_error", "message": "Validation failed", "details": ["Email required"] }
2.6 revokeAllSessions(userId)
Source: middleware.ts:83–85
Sets revoked=1 on all sessions for a user. Called by POST /api/auth/logout.
UPDATE sessions SET revoked = 1 WHERE user_id = $1;
2.7 generateCsrfToken() / validateCsrf(request, token)
Source: middleware.ts:88–99
CSRF token generation (32 random bytes hex-encoded) and validation via x-csrf-token header.
Status: Implemented but not actively required on any route. CSRF protection is handled via:
- BankID OIDC state parameter (login flow)
- Origin header validation (in
requireAuth)
3. Middleware Library (lib/middleware/)
3.1 Error Handler (middleware/error-handler.ts)
AppError class:
class AppError extends Error {
constructor(
public code: string,
message: string,
public status: number = 500,
public details?: unknown
) {}
}
Predefined error constructors:
| Constructor | Code | HTTP Status |
|---|---|---|
Errors.unauthorized(msg?) |
UNAUTHORIZED |
401 |
Errors.forbidden(msg?) |
FORBIDDEN |
403 |
Errors.notFound(resource) |
NOT_FOUND |
404 |
Errors.badRequest(msg, details?) |
BAD_REQUEST |
400 |
Errors.conflict(msg) |
CONFLICT |
409 |
Errors.tooManyRequests(msg?) |
RATE_LIMIT_EXCEEDED |
429 |
Errors.internal(msg?) |
INTERNAL_ERROR |
500 |
Error response format:
{
"error": {
"code": "BAD_REQUEST",
"message": "Amount must be between 100 and 50000 NOK",
"details": "validation_error"
}
}
Production masking: createErrorResponse() masks internal error messages in production — only returns "An unexpected error occurred" for 500 errors.
3.2 Auth Middleware (middleware/auth-middleware.ts)
Alternative auth middleware for mobile clients using Bearer token pattern.
requireAuth(request):
- Extracts JWT from
Authorization: Bearer <token>header - Verifies JWT signature + expiry
- Returns
userIdfrom payload
In-memory rate limiter (for Bearer token routes):
DEFAULT_RATE_LIMIT: 100 req/minSTRICT_RATE_LIMIT: 10 req/min- Auto-cleanup every 5 minutes
- Rate limit headers:
X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset
getClientIP(request):
Checks X-Forwarded-For → X-Real-IP → falls back to 'unknown'.
3.3 Validation (middleware/validation.ts)
Input validation functions — no external dependencies, all custom implementations.
| Function | Description | Rules |
|---|---|---|
validatePhone(phone) |
International phone | Starts with +, 8–15 digits |
validateAmount(amount) |
Positive monetary amount | > 0, max 2 decimal places |
validateIBAN(iban) |
European IBAN | Country code + alphanumeric, mod-97 checksum |
validatePIN(pin) |
Card PIN | Exactly 4 digits |
validateEmail(email) |
Email address | Basic [email protected] pattern |
validateCurrency(currency) |
ISO 4217 code | Whitelist: EUR, USD, GBP, BAM, CHF, PLN, NOK, RSD, TRY, PKR |
validateDateISO(date) |
ISO 8601 date | Parseable by Date.parse() |
validateName(name) |
Name field | 1–100 chars, at least one letter, XSS-safe |
validateLanguage(lang) |
Language code | Whitelist: nb, en, bs, sq |
sanitizeText(text, maxLength?) |
Text sanitization | Strips HTML tags + control chars, trims, enforces max length (default 500) |
validate(condition, msg) |
Assert helper | Throws AppError (400) if false |
required(value, name) |
Required field check | Throws AppError (400) if null/undefined |
Security notes:
validateNamechecks for:<script,javascript:,onerror=,onclick=— blocks XSS injection in name fieldssanitizeTextremoves HTML tags via regex, strips control charactersvalidateIBANimplements full mod-97 checksum algorithmvalidateAmountrejectsNaN,Infinity, negative values
4. Security Headers (Next.js Config)
Applied to all responses via next.config.ts:
| Header | Production Value | Development Value | Purpose |
|---|---|---|---|
Content-Security-Policy |
default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self'; frame-ancestors 'none' |
Adds 'unsafe-eval' + 'unsafe-inline' for HMR |
XSS protection |
X-Frame-Options |
DENY |
DENY |
Clickjacking prevention |
X-Content-Type-Options |
nosniff |
nosniff |
MIME sniffing prevention |
Referrer-Policy |
strict-origin-when-cross-origin |
Same | Referrer leakage prevention |
Permissions-Policy |
camera=(self), microphone=(), geolocation=(self) |
Same | Feature restriction |
Strict-Transport-Security |
max-age=63072000; includeSubDomains; preload |
Same | Force HTTPS (2-year HSTS) |
5. Middleware Usage Matrix
| Route | Rate Limit | requireAuth |
requireMerchant |
Feature Flag | Validation Functions |
|---|---|---|---|---|---|
GET /api/auth/bankid |
10/min | No | No | No | — |
GET /api/auth/bankid/callback |
10/min | No | No | No | state cookie |
GET /api/auth/me |
No | Yes | No | No | — |
POST /api/auth/logout |
No | Yes | No | No | — |
POST /api/auth/refresh |
No | Yes | No | No | — |
GET /api/transactions |
No | Yes | No | No | — |
POST /api/transactions/remittance |
10/min | Yes | No | No | validateAmount |
POST /api/transactions/qr-payment |
10/min | Yes | No | No | validateAmount |
GET /api/rates |
120/min | No | No | No | — |
POST /api/recipients |
No | Yes | No | No | validateName, country whitelist |
POST /api/merchants/register |
No | Yes | No | No | validateName, orgNumber |
GET /api/merchants/dashboard |
No | Yes | Yes | No | period whitelist |
GET /api/notifications |
No | Yes | No | notifications |
— |
PATCH /api/notifications |
No | Yes | No | notifications |
ID format, max 100 |
PATCH /api/settings |
No | Yes | No | No | currency/language whitelist |
POST /api/cards/[id]/physical |
No | Yes | No | physicalCards |
address min 10 chars |
POST /api/cards/[id]/pin |
No | Yes | No | cardPin |
validatePIN |
GET/PUT /api/cards/[id]/limits |
No | Yes | No | spendingLimits |
limitType whitelist |
6. Error Spike Detection
Implemented in src/lib/alerts.ts as a middleware-adjacent concern:
- Every HTTP 5xx response triggers
trackError()(called injsonError()middleware for 500 errors) - Rolling 1-minute window of error timestamps maintained in-memory
- When count > 5 in 60 seconds → sends critical Slack alert to
#drop-ops - 10-minute cooldown per alert title prevents spam
Limitation: Error counter is in-memory only — resets on application restart. Redis-backed counter planned for v1.0.
Related Documents
Approval
| Role | Name | Date | Signature |
|---|---|---|---|
| Author | Platform Architect (AI) | 2026-02-23 | |
| Reviewer | |||
| Approver | Alem Bašić |
No comments to display
No comments to display