Skip to main content

Middleware Design Document

DropMiddleware MiddlewareDesign Document

Sources:Project: src/drop-app/src/lib/middleware.ts,Drop src/drop-app/src/lib/middleware/Version: 0.1.0 Date: 2026-02-23 Author: Platform Architect (AI) Status: In Review Reviewers: Alem Bašić (CEO)

Document History

VersionDateAuthorChanges
0.12026-02-23Platform Architect (AI)Initial draft from source code analysis

1. Overview

Drop has two middleware layers:

  1. src/lib/middleware.ts — The active middleware used by all API routes. Provides requireAuth, requireMerchant, rateLimit, getClientIp, jsonError, CSRF,CSRF protection, and session revocation.

  2. src/lib/middleware/ directory — A modular middleware library with auth-middleware.ts (Bearer token for mobile), error-handler.ts (AppError class), and validation.ts. Exported(input viasanitization barrel file middleware/index.tsfunctions).

TheBoth APIlayers routesare used in production. Routes import from both: @/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-42–80

Authenticates the current request via cookie-based JWT.

Returns

Returns: { user,user: errorUser, error: null } | { user: null, error: NextResponse }.

Steps:

  1. CSRF origin checkIfif Origin header present, must match allowed origins
  2. Cookie extraction — Reads drop_token from cookies
  3. JWT verification — Validates signature and expiry
  4. User lookup — Loads user from users table
  5. Session revocation check — Verifies at least one non-revoked session exists

Allowed origins: (NEXT_PUBLIC_APP_URL, http://localhost:3000, http://localhost:3001)

  • Cookie extraction — reads drop_token from request cookies
  • JWT verification — validates HS256 signature and expiry using jose library
  • User lookup — loads user from users table by userId from 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
    JSON// user is guaranteed non-null here
    

    Error responses:

    • 401 unauthorized — missing cookie, invalid JWT, expired token, user not found, all sessions revoked

    2.2 requireMerchant(request?)

    Source: middleware.ts:101-101–108

    Extends requireAuth with a merchant role check: user must have role === 'merchant'. Returns 403 if not.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-7–31

    Persistent IP-based rate limiter using the rate_limits database table.

    ParameterDefaultDescription
    ipClient IP address
    limitMax requests per window
    windowMs60,000msWindow size in milliseconds

    Returns: boolean — true if request is allowed, false if rate limited.

    Implementation:

    • Uses runUpsert for atomic counter creation/update
    • Cleans expired entries on each call (removes rows where expires_at < now)
    • Counter stored in rate_limits table: (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 requests per 60s windowreq/min
      return jsonError("rate_limited", "Too many requests", 429);
    }
    

      Applied

    • Defaultlimits:

      window:60,000ms minute)
    • Cleans
    • expiredentrieson each call
    • Uses
    • atomiccountercreation/update
      EndpointLimitWindow
      /api/auth/bankid/initiate10/min60s
      /api/auth/bankid/callback10/min60s
      /api/auth/register (1deprecated) 10/min 60s
      runUpsert/api/auth/login for(deprecated) 10/min 60s
      /api/transactions/remittance10/min60s
      /api/transactions/qr-payment10/min60s
      /api/rates120/min60s
      /api/rates/[currency]120/min60s

      2.4 getClientIp(request)

      Source: middleware.ts:33-33–35

      Extracts clientthe client's real IP address from the x-forwarded-for header (first IP in chain),the fallschain — the originating client). Falls back to '127.0.0.11'. 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-37–39

      Creates a standardized JSON error response.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-83–85

      Sets revoked=1 on all sessions for a user. Called duringby logout.POST /api/auth/logout.

      UPDATE sessions SET revoked = 1 WHERE user_id = $1;
      

      2.7 generateCsrfToken() / validateCsrf(request, token)

      Source: middleware.ts:88-88–99

      CSRF token generation (32 random bytes hex-encoded) and validation via x-csrf-token header.

      Available

      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

      Source: (middleware/error-handler.ts

      )

      AppError class:

      class AppError extends Error {
        constructor(
          public code: string,
          message: string,
          public status: number = 500,
          public details?: unknown)unknown
        ) {}
      }
      

      Predefined error constructors (Errors.*):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(error)) handles AppError, standard Error, and unknown errors. In development, includes original error messages; in production, masks internal error messages in production — only returns "An unexpected error occurred" for 500 errors.


      3.2 Auth Middleware

      Source: (middleware/auth-middleware.ts

      )

      Alternative auth middleware for mobile clients using Bearer token pattern (vs. cookie pattern in middleware.ts).pattern.

      requireAuth(request):

      • Extracts JWT from Authorization: Bearer <token> header,header
      • verifies,
      • Verifies returnsJWT userId.

        signature + expiry
      • Returns userId from payload

      In-memory rate limiter with:(for Bearer token routes):

      • DEFAULT_RATE_LIMIT: 100 req/min
      • STRICT_RATE_LIMIT: 10 req/min
      • Auto-cleanup every 5 minutes
      • Rate limit responseheaders: headers (X-RateLimit-*Limit), X-RateLimit-Remaining, X-RateLimit-Reset

      getClientIP(request): — Checks X-Forwarded-For, then X-Real-IP, then falls back to 'unknown'.


      3.3 Validation

      Source: (middleware/validation.ts

      )

      Input validation functions (no external dependencies):dependencies, all custom implementations.

      Function Description Rules
      validatePhone(phone) International phone format Starts with +, 8-8–15 digits
      validateAmount(amount) Positive numbermonetary amount > 0,0, max 2 decimal places
      validateIBAN(iban) European IBAN format Country code + digits + 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-1–100 chars, at least one letter, no script/HTML injectionXSS-safe
      validateLanguage(lang) Language code Whitelist: nb, en, bs, sq
      sanitizeText(text, maxLength?) Text sanitization Strips HTML tags,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:

      • validateName checks for dangerous patterns:for: <script, javascript:, onerror=, onclick= — blocks XSS injection in name fields
      • sanitizeText removes HTML tags via regex, strips control characters
      • IBAN validationvalidateIBAN implements the full mod-97 checksum algorithm
      • validateAmount rejects NaN, Infinity, negative values

      4. Security Headers (Next.js Config)

      Applied to all responses via next.config.ts:

      HeaderProduction ValueDevelopment ValuePurpose
      Content-Security-Policydefault-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 HMRXSS protection
      X-Frame-OptionsDENYDENYClickjacking prevention
      X-Content-Type-OptionsnosniffnosniffMIME sniffing prevention
      Referrer-Policystrict-origin-when-cross-originSameReferrer leakage prevention
      Permissions-Policycamera=(self), microphone=(), geolocation=(self)SameFeature restriction
      Strict-Transport-Securitymax-age=63072000; includeSubDomains; preloadSameForce HTTPS (2-year HSTS)

      5. Middleware Usage by RouteMatrix

      name,phone,age
      Route Rate Limit AuthrequireAuth MerchantrequireMerchant Feature Flag Validation Functions
      POSTGET /api/auth/registerbankid 10/min -No -No -No email,
      GET /api/auth/bankid/callback10/minNoNoNostate cookie
      GET /api/auth/meNoYesNoNo
      POST /api/auth/loginlogout 10/minNo -Yes -No -No -
      POST /api/auth/refreshNoYesNoNo
      GET /auth/meapi/transactions -No Yes -No -No -
      POST /auth/logout-Yes---
      POST /auth/refresh-Yes---
      GET /transactions-Yes---
      POST /api/transactions/remittance 10/min Yes -No -No amount range, decimalvalidateAmount
      POST /api/transactions/qr-payment 10/min Yes -No -No amount range, decimalvalidateAmount
      GET /api/rates 120/min -No -No -No -
      POST /api/recipientsNoYesNoNovalidateName, country whitelist
      POST /api/merchants/registerNoYesNoNovalidateName, orgNumber
      GET /rates/[currency]api/merchants/dashboard 120/minNo -Yes -Yes -No -period whitelist
      GET /api/notificationsNoYesNonotifications
      PATCH /api/notificationsNoYesNonotificationsID format, max 100
      PATCH /api/settingsNoYesNoNocurrency/language whitelist
      POST /api/cards/[id]/physical -No Yes -No physicalCards address min 10 chars
      POST /api/cards/[id]/pin -No Yes -No cardPin 4-digit PINvalidatePIN
      GETGET/PUT /api/cards/[id]/limits -No Yes -No spendingLimits-
      PUT /cards/[id]/limits-Yes-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 in jsonError() 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.



      Approval

      RoleNameDateSignature
      GET /notificationsAuthor -Platform Architect (AI) Yes2026-02-23 -notifications-
      PATCH /notificationsReviewer - Yes -notificationsID format, max 100
      PATCH /settingsApprover -Alem Bašić Yes --currency/language whitelist
      POST /recipients-Yes--name, country whitelist
      POST /merchants/register-Yes--orgNumber 9 digits
      GET /merchants/dashboard-YesMerchant-period whitelist
      GET /merchants/qr-YesMerchant--
      GET /merchants/transactions-YesMerchant--