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
-
On login/register: A
sessionsrecord is created with:token_hash: SHA-256 of the JWT stringexpires_at: Token expiry timerevoked: 0 (active)
-
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
-
On logout:
revokeAllSessions(userId)setsrevoked=1for 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
trueif allowed,falseif 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 IPSTRICT_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/meon mount (with deduplication viauseRef) - Redirects to
/loginif unauthenticated (unlessredirectIfUnauthenticated=false) logout()callsPOST /api/auth/logoutthen redirects to/login