Skip to main content

Authentication

Drop Authentication System

Sources: src/drop-app/src/lib/auth.ts, src/drop-app/src/lib/middleware.ts, src/drop-app/src/lib/middleware/auth-middleware.ts

Overview

  • Algorithm: HS256 (HMAC-SHA256)
  • Library: jose (SignJWT / jwtVerify)
  • Token lifetime: 24 hours
  • Cookie name: drop_token
  • Cookie flags: httpOnly, secure (production only), sameSite=strict, path=/

Authentication Flow

Registration Flow

Client                          Server
  |                               |
  |  POST /api/auth/register      |
  |  {email, password, ...}       |
  |------------------------------>|
  |                               |  1. Rate limit check (10/min per IP)
  |                               |  2. Validate all fields
  |                               |  3. Check email uniqueness
  |                               |  4. Hash password (bcrypt, 12 rounds)
  |                               |  5. INSERT into users table
  |                               |  6. Sign JWT {userId, email, role}
  |                               |  7. Create session record (SHA-256 hash of token)
  |                               |  8. Set httpOnly cookie
  |  Set-Cookie: drop_token=...   |
  |<------------------------------|
  |  {data: {id, email, ...}}     |

Login Flow

Client                          Server
  |                               |
  |  POST /api/auth/login         |
  |  {email, password}            |
  |------------------------------>|
  |                               |  1. Rate limit check (10/min per IP)
  |                               |  2. Look up user by email
  |                               |  3. Verify password (bcrypt compare)
  |                               |  4. Sign JWT
  |                               |  5. Create session record
  |                               |  6. Set httpOnly cookie
  |  Set-Cookie: drop_token=...   |
  |<------------------------------|
  |  {data: {id, email, ...}}     |

Request Authentication

Client                          Server
  |                               |
  |  GET /api/auth/me             |
  |  Cookie: drop_token=eyJ...    |
  |------------------------------>|
  |                               |  1. Extract token from cookie
  |                               |  2. Verify JWT signature + expiry
  |                               |  3. CSRF origin check (if Origin header present)
  |                               |  4. Check session not revoked
  |                               |  5. Load user from database
  |<------------------------------|
  |  {data: {user}}               |

Logout Flow

Client                          Server
  |                               |
  |  POST /api/auth/logout        |
  |  Cookie: drop_token=...       |
  |------------------------------>|
  |                               |  1. Verify current auth
  |                               |  2. Revoke ALL sessions for user
  |                               |  3. Delete cookie
  |  Set-Cookie: drop_token=;     |
  |  expires=Thu, 01 Jan 1970     |
  |<------------------------------|
  |  {message: "Logged out"}      |

JWT Structure

Payload

interface JwtPayload {
  userId: string;   // e.g., "usr_a1b2c3d4e5f6g7h8"
  email: string;    // e.g., "[email protected]"
  role: string;     // "user" or "merchant"
}

Header

{ "alg": "HS256" }

Claims

Claim Value
exp Current time + 24h
iat Current time

Source: auth.ts:28-34


JWT Secret Management

Source: auth.ts:5-16

Production:
  - JWT_SECRET env var MUST be set
  - Throws FATAL error if missing (unless during build phase — NEXT_PHASE)

Development:
  - Falls back to "drop-dev-only-" + process.cwd()
  - NEVER used in production runtime

Session Revocation (C5)

Source: middleware.ts:42-80, auth.ts:56-66

How It Works

  1. On login/register: A sessions record is created with:

    • token_hash: SHA-256 of the JWT string
    • expires_at: Token expiry time
    • revoked: 0 (active)
  2. On each authenticated request (requireAuth):

    • Check if user has ANY non-revoked, non-expired session
    • If sessions exist but all are revoked/expired → reject with 401
  3. On logout:

    • revokeAllSessions(userId) sets revoked=1 for all user sessions

Backwards Compatibility

If a user has zero session records (pre-migration), authentication still works — revocation check is skipped (middleware.ts:72-76).


CSRF Protection (C6)

Source: middleware.ts:43-56

Origin Validation

On every authenticated request, if an Origin header is present:

const allowedOrigins = [
  process.env.NEXT_PUBLIC_APP_URL,
  "http://localhost:3000",
  "http://localhost:3001",
];

If the origin is not in the allowed list → 403 Forbidden.

CSRF Token (available but not actively enforced on routes)

generateCsrfToken(): string    // crypto.randomBytes(32).toString("hex")
validateCsrf(request, token): boolean  // Checks x-csrf-token header

Source: middleware.ts:88-99


Rate Limiting

Persistent Rate Limiter (Active)

Source: middleware.ts:7-31

Uses the rate_limits database table for persistent, cross-restart rate limiting.

async function rateLimit(ip: string, limit: number, windowMs: number = 60000): Promise<boolean>
  • Cleans expired entries on each call
  • Returns true if allowed, false if limit exceeded
  • Used on: login (10/min), register (10/min), remittance (10/min), qr-payment (10/min), rates (120/min)

In-Memory Rate Limiter (Available)

Source: middleware/auth-middleware.ts:44-115

Alternative implementation using Map<string, RateLimitEntry> with automatic cleanup every 5 minutes.

Predefined configs:

  • DEFAULT_RATE_LIMIT: 100 req/min per IP
  • STRICT_RATE_LIMIT: 10 req/min per IP

Returns rate limit headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset


Authorization

Role-Based Access

Two roles in the system: user and merchant.

Function Source Checks
requireAuth(request?) middleware.ts:42 Cookie auth + session revocation + CSRF
requireMerchant(request?) middleware.ts:101 All of requireAuth + role === 'merchant'

Route Protection Summary

Route Auth Role Additional
POST /api/auth/register None - Rate limited
POST /api/auth/login None - Rate limited
GET /api/auth/me Required Any -
POST /api/auth/logout Required Any -
POST /api/auth/refresh Required Any -
GET /api/transactions Required Any -
POST /api/transactions/remittance Required Any KYC approved, rate limited
POST /api/transactions/qr-payment Required Any Rate limited
GET /api/merchants/dashboard Required Merchant -
GET /api/merchants/qr Required Merchant -
GET /api/merchants/transactions Required Merchant -
GET /api/rates None - Rate limited (120/min)
GET /api/health None - -

Password Security

Source: utils-server.ts:8-15

  • Hashing: bcrypt with 12 rounds (bcryptjs)
  • Verification: bcrypt.compareSync(password, hash)
  • PIN hashing: Same bcrypt function used for card PINs

Client-Side Auth Hook

Source: use-auth.ts

function useAuth(redirectIfUnauthenticated = true): {
  user: User | null;
  loading: boolean;
  logout: () => Promise<void>;
  refreshUser: () => Promise<void>;
}
  • Fetches /api/auth/me on mount (with deduplication via useRef)
  • Redirects to /login if unauthenticated (unless redirectIfUnauthenticated=false)
  • logout() calls POST /api/auth/logout then redirects to /login

Phone/SMS Verification [PLANNED]

Status: NOT IMPLEMENTED

The onboarding flow includes a 6-digit OTP verification step (step 2 of 4 in /onboarding), but SMS sending is not implemented. In the current MVP:

  • The OTP input field accepts any 6-digit code — no actual verification
  • No SMS provider (Twilio, Vonage, etc.) is integrated
  • No phone number verification occurs
  • BankID (planned) will replace OTP-based verification for production

For production: Phone verification will be handled via BankID fødselsnummer validation, not SMS OTP.