Latest GoodTag Auth
This commit is contained in:
parent
90f6abdabe
commit
e38204c4e6
|
|
@ -45,3 +45,4 @@ build/
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,68 +0,0 @@
|
|||
# 🚀 Admin Dashboard - Quick Start Guide
|
||||
|
||||
## ⚡ 5-Minute Setup
|
||||
|
||||
### 1. Enable Dashboard
|
||||
```bash
|
||||
# Add to .env
|
||||
ENABLE_ADMIN_DASHBOARD=true
|
||||
```
|
||||
|
||||
### 2. Create Admin User
|
||||
```sql
|
||||
UPDATE users SET role = 'security_admin' WHERE phone_number = '+YOUR_ADMIN_PHONE';
|
||||
```
|
||||
|
||||
### 3. Get Access Token
|
||||
```bash
|
||||
# Step 1: Request OTP
|
||||
curl -X POST http://localhost:3000/auth/request-otp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"phone_number": "+YOUR_ADMIN_PHONE"}'
|
||||
|
||||
# Step 2: Verify OTP (use code from SMS)
|
||||
curl -X POST http://localhost:3000/auth/verify-otp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"phone_number": "+YOUR_ADMIN_PHONE", "code": "123456"}'
|
||||
|
||||
# Response contains: {"access_token": "..."}
|
||||
```
|
||||
|
||||
### 4. Set Token in Browser
|
||||
1. Open: `http://localhost:3000/admin/security-dashboard`
|
||||
2. Open browser console (F12)
|
||||
3. Run: `localStorage.setItem('admin_token', 'YOUR_ACCESS_TOKEN')`
|
||||
4. Refresh page
|
||||
|
||||
### 5. Configure Alerts (Optional)
|
||||
```bash
|
||||
# Add to .env
|
||||
SECURITY_ALERT_WEBHOOK_URL=https://hooks.slack.com/services/YOUR/WEBHOOK
|
||||
SECURITY_ALERT_MIN_LEVEL=HIGH_RISK
|
||||
```
|
||||
|
||||
## ✅ Done!
|
||||
|
||||
Dashboard is now accessible at: `/admin/security-dashboard`
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Security Checklist
|
||||
|
||||
- [ ] `ENABLE_ADMIN_DASHBOARD=true` set
|
||||
- [ ] Admin user has `role = 'security_admin'`
|
||||
- [ ] `CORS_ALLOWED_ORIGINS` configured (production)
|
||||
- [ ] HTTPS enabled (production)
|
||||
- [ ] Admin token stored securely
|
||||
- [ ] `SECURITY_ALERT_WEBHOOK_URL` configured (optional)
|
||||
|
||||
---
|
||||
|
||||
## 📚 Full Documentation
|
||||
|
||||
See `ADMIN_DASHBOARD_SECURITY.md` for complete details.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,387 +0,0 @@
|
|||
# 🔒 Admin Security Dashboard - Implementation Summary
|
||||
|
||||
## ✅ Implementation Status: **COMPLETE**
|
||||
|
||||
All components of the secure Authentication Admin Dashboard have been implemented and are ready for use.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Components Delivered
|
||||
|
||||
### 1️⃣ **Admin API Endpoint** ✅
|
||||
**File:** `src/routes/adminRoutes.js`
|
||||
|
||||
- **Route:** `GET /admin/security-events`
|
||||
- **Features:**
|
||||
- ✅ Filtering by `risk_level` (INFO, SUSPICIOUS, HIGH_RISK)
|
||||
- ✅ Search by user_id, phone, or IP address
|
||||
- ✅ Pagination with `limit` (default: 200, max: 1000) and `offset`
|
||||
- ✅ Statistics for last 24 hours
|
||||
- ✅ Complete input validation and sanitization
|
||||
- ✅ SQL injection prevention (parameterized queries)
|
||||
- ✅ Output sanitization before JSON response
|
||||
- ✅ Admin access logging
|
||||
|
||||
### 2️⃣ **Admin Authentication Middleware** ✅
|
||||
**File:** `src/middleware/adminAuth.js`
|
||||
|
||||
- ✅ Role-based access control (RBAC)
|
||||
- ✅ Checks `user.role === 'security_admin'`
|
||||
- ✅ Returns 403 for unauthorized users
|
||||
- ✅ Logs unauthorized access attempts
|
||||
|
||||
### 3️⃣ **Admin Dashboard UI** ✅
|
||||
**File:** `public/security-dashboard.html`
|
||||
|
||||
- ✅ Vanilla HTML/CSS/JS (no frameworks)
|
||||
- ✅ Dark theme with modern UI
|
||||
- ✅ **XSS Prevention:** Uses `textContent` only (NO `innerHTML`)
|
||||
- ✅ Table view of security events
|
||||
- ✅ Filter by risk level
|
||||
- ✅ Search functionality
|
||||
- ✅ Statistics counters (total, high risk, suspicious, info)
|
||||
- ✅ Manual refresh button
|
||||
- ✅ Auto-refresh every 15 seconds
|
||||
- ✅ Local time formatting
|
||||
- ✅ Responsive design
|
||||
|
||||
### 4️⃣ **Security Middleware** ✅
|
||||
|
||||
**Rate Limiting:** `src/middleware/adminRateLimit.js`
|
||||
- ✅ 100 requests per 15 minutes per user
|
||||
- ✅ Redis-backed with memory fallback
|
||||
- ✅ Configurable via env vars
|
||||
|
||||
**Security Headers:** `src/middleware/securityHeaders.js`
|
||||
- ✅ `X-Frame-Options: DENY` (clickjacking protection)
|
||||
- ✅ `X-Content-Type-Options: nosniff`
|
||||
- ✅ `X-XSS-Protection: 1; mode=block`
|
||||
- ✅ `Strict-Transport-Security` (production)
|
||||
|
||||
### 5️⃣ **Active Alerting** ✅
|
||||
**File:** `src/services/auditLogger.js`
|
||||
|
||||
- ✅ `triggerSecurityAlert()` function implemented
|
||||
- ✅ Fires for HIGH_RISK events
|
||||
- ✅ Fires for anomalies detected by `checkAnomalies()`
|
||||
- ✅ Webhook integration (Slack-compatible)
|
||||
- ✅ Resilient (doesn't crash on webhook failure)
|
||||
- ✅ Configurable via `SECURITY_ALERT_WEBHOOK_URL`
|
||||
|
||||
### 6️⃣ **Server Integration** ✅
|
||||
**File:** `src/index.js`
|
||||
|
||||
- ✅ Admin routes mounted at `/admin`
|
||||
- ✅ Protected by: `securityHeaders` → `authMiddleware` → `adminAuth` → `adminRateLimit`
|
||||
- ✅ Dashboard served at `/admin/security-dashboard`
|
||||
- ✅ Feature flag: `ENABLE_ADMIN_DASHBOARD=true`
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Security Protections Applied
|
||||
|
||||
| Security Measure | Status | Implementation |
|
||||
|-----------------|:------:|----------------|
|
||||
| **RBAC (Role-Based Access)** | ✅ | `adminAuth` middleware checks `role === 'security_admin'` |
|
||||
| **JWT Authentication** | ✅ | All routes protected by `authMiddleware` |
|
||||
| **HTTPS Enforcement** | ✅ | `Strict-Transport-Security` header in production |
|
||||
| **XSS Prevention** | ✅ | Dashboard uses `textContent` only, NO `innerHTML` |
|
||||
| **Clickjacking Protection** | ✅ | `X-Frame-Options: DENY` header |
|
||||
| **SQL Injection Prevention** | ✅ | Parameterized queries only |
|
||||
| **Input Validation** | ✅ | All query parameters validated and sanitized |
|
||||
| **Output Sanitization** | ✅ | All DB fields sanitized before JSON response |
|
||||
| **Rate Limiting** | ✅ | 100 requests/15min per admin user |
|
||||
| **CORS Protection** | ✅ | No public origins, whitelist only |
|
||||
| **Audit Logging** | ✅ | All admin access logged to `auth_audit` |
|
||||
| **Feature Flag** | ✅ | Dashboard only enabled when `ENABLE_ADMIN_DASHBOARD=true` |
|
||||
| **No Secrets in Code** | ✅ | All config via environment variables |
|
||||
| **Error Handling** | ✅ | Graceful degradation, no sensitive info leaked |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Configuration & Setup
|
||||
|
||||
### Step 1: Environment Variables
|
||||
|
||||
Add to your `.env` file:
|
||||
|
||||
```bash
|
||||
# Enable admin dashboard
|
||||
ENABLE_ADMIN_DASHBOARD=true
|
||||
|
||||
# Security alerting webhook (optional)
|
||||
SECURITY_ALERT_WEBHOOK_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
|
||||
SECURITY_ALERT_MIN_LEVEL=HIGH_RISK # Options: INFO, SUSPICIOUS, HIGH_RISK
|
||||
|
||||
# Admin rate limiting (optional, defaults shown)
|
||||
ADMIN_RATE_LIMIT_MAX=100
|
||||
ADMIN_RATE_LIMIT_WINDOW=900 # 15 minutes in seconds
|
||||
|
||||
# CORS (REQUIRED in production - no wildcards!)
|
||||
CORS_ALLOWED_ORIGINS=https://your-admin-domain.com,https://api.yourdomain.com
|
||||
```
|
||||
|
||||
### Step 2: Create Admin User
|
||||
|
||||
Ensure at least one user has `role = 'security_admin'` in the database:
|
||||
|
||||
```sql
|
||||
UPDATE users
|
||||
SET role = 'security_admin'
|
||||
WHERE phone_number = '+1234567890'; -- Replace with admin phone
|
||||
```
|
||||
|
||||
### Step 3: Get Admin Access Token
|
||||
|
||||
1. **Authenticate as admin user:**
|
||||
```bash
|
||||
# Request OTP
|
||||
POST /auth/request-otp
|
||||
{
|
||||
"phone_number": "+1234567890"
|
||||
}
|
||||
|
||||
# Verify OTP
|
||||
POST /auth/verify-otp
|
||||
{
|
||||
"phone_number": "+1234567890",
|
||||
"code": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
2. **Save the access token:**
|
||||
- Copy the `access_token` from the response
|
||||
- Open browser console on `/admin/security-dashboard`
|
||||
- Run: `localStorage.setItem('admin_token', 'YOUR_ACCESS_TOKEN')`
|
||||
- Refresh the page
|
||||
|
||||
### Step 4: Access Dashboard
|
||||
|
||||
Navigate to: `https://your-domain.com/admin/security-dashboard`
|
||||
|
||||
The dashboard will:
|
||||
- ✅ Load security events automatically
|
||||
- ✅ Auto-refresh every 15 seconds
|
||||
- ✅ Allow filtering and searching
|
||||
- ✅ Display statistics
|
||||
|
||||
---
|
||||
|
||||
## 📋 API Usage
|
||||
|
||||
### Get Security Events
|
||||
|
||||
```bash
|
||||
GET /admin/security-events?risk_level=HIGH_RISK&limit=100&search=192.168.1.1
|
||||
|
||||
Authorization: Bearer YOUR_ADMIN_ACCESS_TOKEN
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
- `risk_level` (optional): `INFO`, `SUSPICIOUS`, or `HIGH_RISK`
|
||||
- `limit` (optional): Number of results (1-1000, default: 200)
|
||||
- `offset` (optional): Pagination offset (default: 0)
|
||||
- `search` (optional): Search in user_id, phone, or IP address
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"user_id": "uuid",
|
||||
"action": "login",
|
||||
"status": "blocked",
|
||||
"risk_level": "HIGH_RISK",
|
||||
"ip_address": "192.168.1.1",
|
||||
"phone": "****5678",
|
||||
"created_at": "2024-01-01T12:00:00Z",
|
||||
...
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"total": 150,
|
||||
"limit": 100,
|
||||
"offset": 0,
|
||||
"has_more": true
|
||||
},
|
||||
"stats": {
|
||||
"last_24h": {
|
||||
"total": 500,
|
||||
"high_risk": 10,
|
||||
"suspicious": 50,
|
||||
"info": 440
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔔 Alerting Configuration
|
||||
|
||||
### Slack Webhook Setup
|
||||
|
||||
1. Go to https://api.slack.com/apps
|
||||
2. Create a new app or select existing
|
||||
3. Navigate to "Incoming Webhooks"
|
||||
4. Enable and create webhook URL
|
||||
5. Add to `.env`:
|
||||
```bash
|
||||
SECURITY_ALERT_WEBHOOK_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
|
||||
```
|
||||
|
||||
### Alert Triggers
|
||||
|
||||
Alerts are sent for:
|
||||
- ✅ All `HIGH_RISK` events (by default)
|
||||
- ✅ Events flagged by anomaly detection:
|
||||
- 5+ failed OTP attempts in 1 hour
|
||||
- 3+ HIGH_RISK events from same IP in 15 minutes
|
||||
|
||||
### Customize Alert Level
|
||||
|
||||
Set `SECURITY_ALERT_MIN_LEVEL` in `.env`:
|
||||
- `HIGH_RISK` (default) - Only HIGH_RISK events
|
||||
- `SUSPICIOUS` - SUSPICIOUS and HIGH_RISK events
|
||||
- `INFO` - All events (not recommended)
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ Security Best Practices
|
||||
|
||||
### ✅ DO:
|
||||
- Always use HTTPS in production
|
||||
- Set `CORS_ALLOWED_ORIGINS` to specific domains (never `*`)
|
||||
- Rotate admin access tokens regularly
|
||||
- Monitor admin access logs
|
||||
- Keep `ENABLE_ADMIN_DASHBOARD=false` when not in use
|
||||
- Use strong JWT secrets
|
||||
- Limit admin user accounts
|
||||
|
||||
### ❌ DON'T:
|
||||
- Never expose admin endpoints to public CORS origins
|
||||
- Never use `innerHTML` in dashboard code
|
||||
- Never commit `.env` files
|
||||
- Never use wildcard CORS (`*`) in production
|
||||
- Never disable rate limiting
|
||||
- Never share admin tokens
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Test Admin Access
|
||||
|
||||
```bash
|
||||
# 1. Get admin token (as shown in Step 3)
|
||||
# 2. Test API endpoint
|
||||
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||
https://your-domain.com/admin/security-events?limit=10
|
||||
|
||||
# 3. Access dashboard
|
||||
open https://your-domain.com/admin/security-dashboard
|
||||
```
|
||||
|
||||
### Verify Security Headers
|
||||
|
||||
```bash
|
||||
curl -I https://your-domain.com/admin/security-dashboard
|
||||
|
||||
# Should see:
|
||||
# X-Frame-Options: DENY
|
||||
# X-Content-Type-Options: nosniff
|
||||
# X-XSS-Protection: 1; mode=block
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Monitoring
|
||||
|
||||
### Admin Access Logs
|
||||
|
||||
All admin actions are logged to `auth_audit` table:
|
||||
- `action: 'admin_view_security_events'`
|
||||
- `status: 'success'` or `'failed'`
|
||||
- Includes IP, user agent, and filters used
|
||||
|
||||
### Query Admin Activity
|
||||
|
||||
```sql
|
||||
SELECT * FROM auth_audit
|
||||
WHERE action = 'admin_view_security_events'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 100;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Dashboard shows "Authentication required"
|
||||
- ✅ Ensure you've set `localStorage.setItem('admin_token', 'YOUR_TOKEN')`
|
||||
- ✅ Verify token is valid and not expired
|
||||
- ✅ Check that user has `role = 'security_admin'`
|
||||
|
||||
### 403 Forbidden on admin routes
|
||||
- ✅ Verify user role is `security_admin` in database
|
||||
- ✅ Check JWT token includes `role` claim
|
||||
- ✅ Ensure token is not expired
|
||||
|
||||
### Alerts not firing
|
||||
- ✅ Check `SECURITY_ALERT_WEBHOOK_URL` is set
|
||||
- ✅ Verify webhook URL is valid
|
||||
- ✅ Check server logs for webhook errors
|
||||
- ✅ Ensure events have `risk_level >= SECURITY_ALERT_MIN_LEVEL`
|
||||
|
||||
### Rate limit errors
|
||||
- ✅ Default: 100 requests per 15 minutes
|
||||
- ✅ Adjust via `ADMIN_RATE_LIMIT_MAX` env var
|
||||
- ✅ Check Redis connection if using Redis
|
||||
|
||||
---
|
||||
|
||||
## 📝 Files Modified/Created
|
||||
|
||||
### Created:
|
||||
- ✅ `src/routes/adminRoutes.js` - Admin API endpoints
|
||||
- ✅ `src/middleware/adminAuth.js` - RBAC middleware
|
||||
- ✅ `src/middleware/adminRateLimit.js` - Rate limiting
|
||||
- ✅ `src/middleware/securityHeaders.js` - Security headers
|
||||
- ✅ `public/security-dashboard.html` - Admin dashboard UI
|
||||
|
||||
### Modified:
|
||||
- ✅ `src/index.js` - Admin routes mounting
|
||||
- ✅ `src/services/auditLogger.js` - Alerting integration (already done)
|
||||
|
||||
---
|
||||
|
||||
## ✨ Summary
|
||||
|
||||
Your secure Admin Security Dashboard is **fully implemented** and ready for production use. All security requirements have been met:
|
||||
|
||||
✅ **Authentication & Authorization** - JWT + RBAC
|
||||
✅ **XSS Prevention** - textContent only
|
||||
✅ **Clickjacking Protection** - X-Frame-Options
|
||||
✅ **Input/Output Sanitization** - All data sanitized
|
||||
✅ **Rate Limiting** - Prevents abuse
|
||||
✅ **Audit Logging** - All access logged
|
||||
✅ **Feature Flag** - Can be disabled
|
||||
✅ **Active Alerting** - Webhook integration
|
||||
|
||||
**Next Steps:**
|
||||
1. Set `ENABLE_ADMIN_DASHBOARD=true` in `.env`
|
||||
2. Create admin user with `role = 'security_admin'`
|
||||
3. Configure `SECURITY_ALERT_WEBHOOK_URL` (optional)
|
||||
4. Set `CORS_ALLOWED_ORIGINS` for production
|
||||
5. Test dashboard access
|
||||
6. Monitor admin activity logs
|
||||
|
||||
---
|
||||
|
||||
**🔒 Security Status: PRODUCTION READY**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,362 +0,0 @@
|
|||
# Admin Security Dashboard - Setup Guide
|
||||
|
||||
## === ADMIN SECURITY VISUALIZER ===
|
||||
|
||||
This document explains how to configure and use the secure Authentication Admin Dashboard for monitoring security events.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Prerequisites
|
||||
|
||||
1. **Admin User Account**: A user account with `role = 'security_admin'` in the database
|
||||
2. **JWT Access Token**: Admin must authenticate and obtain a JWT token with admin role
|
||||
3. **Environment Variables**: Required configuration (see below)
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### 1. Environment Variables
|
||||
|
||||
Add the following to your `.env` file:
|
||||
|
||||
```bash
|
||||
# Enable admin dashboard
|
||||
ENABLE_ADMIN_DASHBOARD=true
|
||||
|
||||
# Security alerting webhook (optional but recommended)
|
||||
SECURITY_ALERT_WEBHOOK_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
|
||||
SECURITY_ALERT_MIN_LEVEL=HIGH_RISK
|
||||
|
||||
# Admin rate limiting (optional, defaults provided)
|
||||
ADMIN_RATE_LIMIT_MAX=100
|
||||
ADMIN_RATE_LIMIT_WINDOW=900
|
||||
```
|
||||
|
||||
### 2. Create Admin User
|
||||
|
||||
Update a user's role in the database:
|
||||
|
||||
```sql
|
||||
UPDATE users
|
||||
SET role = 'security_admin'
|
||||
WHERE phone_number = '+919876543210';
|
||||
```
|
||||
|
||||
**Note**: The `role` column must support the value `'security_admin'`. If your enum doesn't include this, you may need to update the enum type:
|
||||
|
||||
```sql
|
||||
-- Check current enum values
|
||||
SELECT unnest(enum_range(NULL::listing_role_enum));
|
||||
|
||||
-- If needed, add security_admin to the enum
|
||||
ALTER TYPE listing_role_enum ADD VALUE 'security_admin';
|
||||
```
|
||||
|
||||
### 3. CORS Configuration
|
||||
|
||||
**IMPORTANT**: Admin dashboard should NOT be accessible from public origins.
|
||||
|
||||
In production, ensure `CORS_ALLOWED_ORIGINS` only includes trusted domains:
|
||||
|
||||
```bash
|
||||
# .env
|
||||
CORS_ALLOWED_ORIGINS=https://admin.yourdomain.com,https://internal.yourdomain.com
|
||||
```
|
||||
|
||||
**Never use `*` for CORS when admin endpoints are enabled.**
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Usage
|
||||
|
||||
### 1. Obtain Admin Access Token
|
||||
|
||||
Admin must authenticate via the normal auth flow:
|
||||
|
||||
```bash
|
||||
# Step 1: Request OTP
|
||||
POST /auth/request-otp
|
||||
{
|
||||
"phone_number": "+919876543210"
|
||||
}
|
||||
|
||||
# Step 2: Verify OTP (returns access token)
|
||||
POST /auth/verify-otp
|
||||
{
|
||||
"phone_number": "+919876543210",
|
||||
"code": "123456"
|
||||
}
|
||||
|
||||
# Response includes:
|
||||
{
|
||||
"access_token": "eyJhbGc...",
|
||||
"refresh_token": "..."
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Access Dashboard
|
||||
|
||||
**Option A: Browser (with token in localStorage)**
|
||||
|
||||
1. Open browser console
|
||||
2. Set token: `localStorage.setItem('admin_token', 'YOUR_ACCESS_TOKEN')`
|
||||
3. Navigate to: `https://yourdomain.com/admin/security-dashboard`
|
||||
|
||||
**Option B: API Direct Access**
|
||||
|
||||
```bash
|
||||
GET /admin/security-events?risk_level=HIGH_RISK&limit=50
|
||||
Authorization: Bearer YOUR_ACCESS_TOKEN
|
||||
```
|
||||
|
||||
### 3. Dashboard Features
|
||||
|
||||
- **Real-time Event Table**: View all authentication events
|
||||
- **Risk Level Filtering**: Filter by INFO, SUSPICIOUS, HIGH_RISK
|
||||
- **Search**: Search by User ID, Phone, or IP Address
|
||||
- **Statistics**: 24-hour event counts by risk level
|
||||
- **Auto-refresh**: Automatically updates every 15 seconds
|
||||
- **Manual Refresh**: Click "Refresh" button anytime
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Security Protections
|
||||
|
||||
### ✅ Implemented Security Measures
|
||||
|
||||
| Security Measure | Implementation |
|
||||
|-----------------|----------------|
|
||||
| **RBAC (Role-Based Access)** | `adminAuth` middleware checks `role === 'security_admin'` |
|
||||
| **JWT Authentication** | All admin routes require valid Bearer token |
|
||||
| **Rate Limiting** | 100 requests per 15 minutes per admin user |
|
||||
| **Input Validation** | All query parameters sanitized and validated |
|
||||
| **SQL Injection Prevention** | Parameterized queries only |
|
||||
| **Output Sanitization** | All DB fields sanitized before JSON response |
|
||||
| **XSS Prevention** | Dashboard uses `textContent` only (NO `innerHTML`) |
|
||||
| **Clickjacking Protection** | `X-Frame-Options: DENY` header |
|
||||
| **CORS Restrictions** | No public origins allowed |
|
||||
| **Audit Logging** | All admin access logged to `auth_audit` |
|
||||
| **HTTPS Enforcement** | HSTS header in production |
|
||||
| **Feature Flag** | Dashboard only enabled when `ENABLE_ADMIN_DASHBOARD=true` |
|
||||
|
||||
### Security Headers
|
||||
|
||||
The dashboard and admin API endpoints include:
|
||||
|
||||
- `X-Frame-Options: DENY` - Prevents clickjacking
|
||||
- `X-Content-Type-Options: nosniff` - Prevents MIME sniffing
|
||||
- `X-XSS-Protection: 1; mode=block` - XSS protection
|
||||
- `Strict-Transport-Security` - HTTPS enforcement (production)
|
||||
|
||||
---
|
||||
|
||||
## 📊 API Endpoints
|
||||
|
||||
### GET /admin/security-events
|
||||
|
||||
Retrieve security audit events with filtering and pagination.
|
||||
|
||||
**Query Parameters:**
|
||||
- `risk_level` (optional): `INFO`, `SUSPICIOUS`, or `HIGH_RISK`
|
||||
- `search` (optional): Search in user_id, phone, or ip_address
|
||||
- `limit` (optional): Number of results (1-1000, default: 200)
|
||||
- `offset` (optional): Pagination offset (default: 0)
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"user_id": "uuid",
|
||||
"action": "otp_verify",
|
||||
"status": "failed",
|
||||
"risk_level": "HIGH_RISK",
|
||||
"ip_address": "192.168.1.1",
|
||||
"user_agent": "Mozilla/5.0...",
|
||||
"device_id": "device-123",
|
||||
"phone": "***543210",
|
||||
"meta": {},
|
||||
"created_at": "2024-01-01T12:00:00Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"total": 1000,
|
||||
"limit": 200,
|
||||
"offset": 0,
|
||||
"has_more": true
|
||||
},
|
||||
"stats": {
|
||||
"last_24h": {
|
||||
"total": 500,
|
||||
"high_risk": 10,
|
||||
"suspicious": 50,
|
||||
"info": 440
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Authentication:** Required (Bearer token with `security_admin` role)
|
||||
|
||||
---
|
||||
|
||||
## 🚨 Alerting Integration
|
||||
|
||||
The dashboard integrates with the existing `triggerSecurityAlert()` function in `auditLogger.js`.
|
||||
|
||||
### When Alerts Fire
|
||||
|
||||
Alerts are sent to `SECURITY_ALERT_WEBHOOK_URL` for:
|
||||
|
||||
1. **HIGH_RISK Events**: All events with `risk_level = 'HIGH_RISK'`
|
||||
2. **Anomaly Detection**: Events flagged by `checkAnomalies()`:
|
||||
- 5+ failed OTP attempts in 1 hour
|
||||
- 3+ HIGH_RISK events from same IP in 15 minutes
|
||||
|
||||
### Webhook Payload Format
|
||||
|
||||
The webhook receives a Slack-compatible JSON payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"text": "🚨 Security Alert: HIGH_RISK",
|
||||
"attachments": [{
|
||||
"color": "danger",
|
||||
"fields": [
|
||||
{"title": "Event Type", "value": "login", "short": true},
|
||||
{"title": "Risk Level", "value": "HIGH_RISK (high)", "short": true},
|
||||
{"title": "IP Address", "value": "192.168.1.1", "short": true}
|
||||
]
|
||||
}],
|
||||
"metadata": {
|
||||
"event_id": "uuid",
|
||||
"risk_level": "HIGH_RISK",
|
||||
"severity": "high",
|
||||
"event_type": "login",
|
||||
"ip_address": "192.168.1.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Setting Up Slack Webhook
|
||||
|
||||
1. Go to https://api.slack.com/apps
|
||||
2. Create a new app or select existing
|
||||
3. Enable "Incoming Webhooks"
|
||||
4. Create webhook URL
|
||||
5. Add to `.env`: `SECURITY_ALERT_WEBHOOK_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL`
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ Security Best Practices
|
||||
|
||||
### 1. Access Control
|
||||
|
||||
- **Limit Admin Users**: Only grant `security_admin` role to trusted personnel
|
||||
- **Rotate Tokens**: Admin tokens should be rotated regularly
|
||||
- **Monitor Admin Access**: Review `auth_audit` logs for admin actions
|
||||
|
||||
### 2. Network Security
|
||||
|
||||
- **HTTPS Only**: Always use HTTPS in production
|
||||
- **VPN/Private Network**: Consider restricting admin dashboard to internal networks
|
||||
- **IP Whitelisting**: Optionally restrict admin endpoints by IP at reverse proxy level
|
||||
|
||||
### 3. Token Management
|
||||
|
||||
- **Short Token TTL**: Use shorter access token TTL for admin users (e.g., 5 minutes)
|
||||
- **Token Storage**: Store admin tokens securely (never in localStorage for production)
|
||||
- **Logout**: Implement proper logout to revoke tokens
|
||||
|
||||
### 4. Monitoring
|
||||
|
||||
- **Alert on Admin Access**: Set up alerts for admin dashboard access
|
||||
- **Review Logs**: Regularly review `auth_audit` for admin actions
|
||||
- **Anomaly Detection**: Monitor for unusual admin access patterns
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Dashboard Not Loading
|
||||
|
||||
1. **Check Feature Flag**: Ensure `ENABLE_ADMIN_DASHBOARD=true` in `.env`
|
||||
2. **Check Token**: Verify admin token is valid and has `security_admin` role
|
||||
3. **Check CORS**: Ensure your origin is in `CORS_ALLOWED_ORIGINS`
|
||||
4. **Check Console**: Open browser console for error messages
|
||||
|
||||
### 403 Forbidden
|
||||
|
||||
- User role is not `security_admin`
|
||||
- Token is invalid or expired
|
||||
- CORS origin not whitelisted
|
||||
|
||||
### 401 Unauthorized
|
||||
|
||||
- Missing `Authorization` header
|
||||
- Invalid JWT token
|
||||
- Token expired
|
||||
|
||||
### No Events Showing
|
||||
|
||||
- Check database connection
|
||||
- Verify `auth_audit` table exists
|
||||
- Check filters (risk_level, search) aren't too restrictive
|
||||
|
||||
---
|
||||
|
||||
## 📝 Files Created/Modified
|
||||
|
||||
### New Files
|
||||
|
||||
- `src/middleware/adminAuth.js` - Admin role check middleware
|
||||
- `src/middleware/adminRateLimit.js` - Rate limiting for admin routes
|
||||
- `src/middleware/securityHeaders.js` - Security headers middleware
|
||||
- `src/routes/adminRoutes.js` - Admin API endpoints
|
||||
- `public/security-dashboard.html` - Admin dashboard UI
|
||||
|
||||
### Modified Files
|
||||
|
||||
- `src/index.js` - Added admin route mounting
|
||||
- `src/services/auditLogger.js` - Already has `triggerSecurityAlert()` (from previous task)
|
||||
|
||||
---
|
||||
|
||||
## ✅ Verification Checklist
|
||||
|
||||
- [ ] `ENABLE_ADMIN_DASHBOARD=true` in `.env`
|
||||
- [ ] Admin user has `role = 'security_admin'` in database
|
||||
- [ ] Admin can obtain JWT token via normal auth flow
|
||||
- [ ] Dashboard accessible at `/admin/security-dashboard`
|
||||
- [ ] API endpoint `/admin/security-events` returns data
|
||||
- [ ] Security headers present (check browser DevTools)
|
||||
- [ ] Rate limiting works (test with >100 requests)
|
||||
- [ ] Webhook alerts fire for HIGH_RISK events (if configured)
|
||||
- [ ] CORS properly configured (no public origins)
|
||||
- [ ] All admin access logged to `auth_audit` table
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Related Documentation
|
||||
|
||||
- `SECURITY_SCENARIOS.md` - Security threat scenarios
|
||||
- `DEVICE_MANAGEMENT.md` - Device management features
|
||||
- `src/services/auditLogger.js` - Audit logging implementation
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
For issues or questions:
|
||||
1. Check browser console for errors
|
||||
2. Review server logs
|
||||
3. Verify database connectivity
|
||||
4. Check environment variables
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2024-01-01
|
||||
**Version**: 1.0.0
|
||||
|
||||
1231
API_INTEGRATION.md
1231
API_INTEGRATION.md
File diff suppressed because it is too large
Load Diff
|
|
@ -1,270 +0,0 @@
|
|||
# Device Management Implementation - Changelog
|
||||
|
||||
## Summary
|
||||
|
||||
Enhanced the auth service to properly support multi-device login with device management capabilities. **One phone number = One account**, but that account can be logged in from **multiple devices simultaneously**.
|
||||
|
||||
---
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Enhanced Verify OTP Endpoint (`/auth/verify-otp`)
|
||||
|
||||
**Added:**
|
||||
- New device detection logic
|
||||
- Audit logging for all login attempts
|
||||
- Response fields: `is_new_device`, `is_new_account`, `active_devices_count`
|
||||
|
||||
**Response now includes:**
|
||||
```json
|
||||
{
|
||||
"user": { ... },
|
||||
"access_token": "...",
|
||||
"refresh_token": "...",
|
||||
"needs_profile": true,
|
||||
"is_new_device": false, // NEW
|
||||
"is_new_account": false, // NEW
|
||||
"active_devices_count": 2 // NEW
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Mobile app can show security notifications for new devices
|
||||
- Track account creation vs. existing account login
|
||||
- Display active device count in UI
|
||||
|
||||
---
|
||||
|
||||
### 2. Enhanced Refresh Token Endpoint (`/auth/refresh`)
|
||||
|
||||
**Added:**
|
||||
- Updates `user_devices.last_seen_at` on token refresh
|
||||
- Tracks device activity for monitoring
|
||||
|
||||
**Benefits:**
|
||||
- Identify active vs. inactive devices
|
||||
- Better device management insights
|
||||
|
||||
---
|
||||
|
||||
### 3. New User Endpoints (`/users/me/*`)
|
||||
|
||||
#### GET `/users/me`
|
||||
- Returns user info with `active_devices_count`
|
||||
- Includes account creation date and last login time
|
||||
|
||||
#### GET `/users/me/devices`
|
||||
- Lists all active devices for the user
|
||||
- Shows device metadata (platform, model, OS, etc.)
|
||||
- Includes first_seen_at and last_seen_at timestamps
|
||||
|
||||
#### DELETE `/users/me/devices/:device_id`
|
||||
- Revokes/logs out a specific device
|
||||
- Revokes all refresh tokens for that device
|
||||
- Logs the action in audit table
|
||||
|
||||
#### POST `/users/me/logout-all-other-devices`
|
||||
- Logs out all devices except the current one
|
||||
- Requires `current_device_id` in header or body
|
||||
- Returns count of revoked devices
|
||||
|
||||
---
|
||||
|
||||
### 4. Audit Logging
|
||||
|
||||
**Added comprehensive logging:**
|
||||
- Login attempts (success/failure)
|
||||
- Device revocations
|
||||
- Logout actions
|
||||
- All events logged to `auth_audit` table with metadata
|
||||
|
||||
**Audit fields:**
|
||||
- `action`: 'login', 'device_revoked', 'logout_all_other_devices'
|
||||
- `status`: 'success', 'failed'
|
||||
- `device_id`: Device identifier
|
||||
- `ip_address`: Client IP
|
||||
- `user_agent`: Client user agent
|
||||
- `meta`: JSONB with additional context (is_new_device, platform, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Database Changes
|
||||
|
||||
### No Schema Changes Required
|
||||
- All features use existing tables:
|
||||
- `users` (already has UNIQUE constraint on phone_number)
|
||||
- `user_devices` (already tracks devices per user)
|
||||
- `auth_audit` (already exists for logging)
|
||||
- `refresh_tokens` (already tracks tokens per device)
|
||||
|
||||
### Existing Constraints Work Perfectly
|
||||
- `phone_number UNIQUE` → One account per phone number ✅
|
||||
- `(user_id, device_identifier) UNIQUE` → One device record per user-device combo ✅
|
||||
|
||||
---
|
||||
|
||||
## Security Improvements
|
||||
|
||||
### ✅ New Device Detection
|
||||
- Automatically flags new device logins
|
||||
- Mobile app can show security alerts
|
||||
- Logged in audit table
|
||||
|
||||
### ✅ Device Activity Tracking
|
||||
- `last_seen_at` updated on token refresh
|
||||
- Helps identify abandoned devices
|
||||
- Better security monitoring
|
||||
|
||||
### ✅ Device Management
|
||||
- Users can see all active devices
|
||||
- Users can revoke specific devices
|
||||
- Users can logout all other devices (security feature)
|
||||
|
||||
### ✅ Audit Trail
|
||||
- All authentication events logged
|
||||
- Can track suspicious activity
|
||||
- Compliance and security auditing
|
||||
|
||||
---
|
||||
|
||||
## API Usage Examples
|
||||
|
||||
### Login from New Device
|
||||
```bash
|
||||
POST /auth/verify-otp
|
||||
{
|
||||
"phone_number": "+919876543210",
|
||||
"code": "123456",
|
||||
"device_id": "new-device-123",
|
||||
"device_info": { "platform": "android" }
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"is_new_device": true,
|
||||
"is_new_account": false,
|
||||
"active_devices_count": 2
|
||||
}
|
||||
```
|
||||
|
||||
### List Active Devices
|
||||
```bash
|
||||
GET /users/me/devices
|
||||
Authorization: Bearer <access_token>
|
||||
|
||||
Response:
|
||||
{
|
||||
"devices": [
|
||||
{
|
||||
"device_identifier": "device-1",
|
||||
"device_platform": "android",
|
||||
"last_seen_at": "2024-01-15T10:30:00Z",
|
||||
"is_active": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Revoke Device
|
||||
```bash
|
||||
DELETE /users/me/devices/device-1
|
||||
Authorization: Bearer <access_token>
|
||||
|
||||
Response:
|
||||
{
|
||||
"ok": true,
|
||||
"message": "Device logged out successfully"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mobile App Integration
|
||||
|
||||
### New Response Fields to Handle
|
||||
|
||||
1. **`is_new_device`** → Show security notification
|
||||
2. **`is_new_account`** → Show welcome flow
|
||||
3. **`active_devices_count`** → Display in settings
|
||||
|
||||
### New Endpoints to Implement
|
||||
|
||||
1. **Device List Screen** → Show all active devices
|
||||
2. **Revoke Device** → Allow users to logout specific devices
|
||||
3. **Security Settings** → Show active device count
|
||||
|
||||
### Example Flow
|
||||
|
||||
```kotlin
|
||||
// After login
|
||||
if (response.is_new_device && !response.is_new_account) {
|
||||
showSecurityAlert("New device logged in: ${deviceModel}")
|
||||
}
|
||||
|
||||
// In settings screen
|
||||
val devices = apiClient.getDevices(accessToken)
|
||||
devices.forEach { device ->
|
||||
showDeviceCard(
|
||||
model = device.device_model,
|
||||
lastSeen = device.last_seen_at,
|
||||
onRevoke = { revokeDevice(device.device_identifier) }
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [x] Same phone number can log in from multiple devices
|
||||
- [x] Each device gets its own refresh token
|
||||
- [x] Devices can be active simultaneously
|
||||
- [x] Revoking one device doesn't affect others
|
||||
- [x] New device detection works correctly
|
||||
- [x] Audit logging captures all events
|
||||
- [x] Device activity tracking works
|
||||
- [x] Logout all other devices works
|
||||
|
||||
---
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
✅ **Fully backward compatible**
|
||||
- All existing endpoints work as before
|
||||
- New response fields are additions (optional to use)
|
||||
- New endpoints are additions (don't break existing code)
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Update Mobile App**
|
||||
- Handle new response fields
|
||||
- Implement device management UI
|
||||
- Show security notifications
|
||||
|
||||
2. **Monitoring**
|
||||
- Query `auth_audit` for suspicious activity
|
||||
- Monitor device counts per user
|
||||
- Alert on unusual patterns
|
||||
|
||||
3. **Future Enhancements**
|
||||
- Device name/labeling (let users name devices)
|
||||
- Push notifications on new device login
|
||||
- Device location tracking (optional)
|
||||
- Session limits (max devices per user)
|
||||
|
||||
---
|
||||
|
||||
## Files Modified
|
||||
|
||||
1. `src/routes/authRoutes.js` - Enhanced verify-otp and refresh endpoints
|
||||
2. `src/routes/userRoutes.js` - Added device management endpoints
|
||||
3. `DEVICE_MANAGEMENT.md` - New documentation
|
||||
4. `CHANGELOG_DEVICE_MANAGEMENT.md` - This file
|
||||
|
||||
---
|
||||
|
||||
## Questions?
|
||||
|
||||
See `DEVICE_MANAGEMENT.md` for detailed API documentation and examples.
|
||||
|
|
@ -1,229 +0,0 @@
|
|||
# CORS + XSS Security Implementation Summary
|
||||
|
||||
## Overview
|
||||
|
||||
This document summarizes the security enhancements implemented to address CORS and XSS vulnerabilities (Issue #14 from the Security Audit Report).
|
||||
|
||||
## Implementation Date
|
||||
|
||||
2024
|
||||
|
||||
## Status
|
||||
|
||||
✅ **FULLY IMPLEMENTED**
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. CORS Startup Validation
|
||||
|
||||
**File:** `src/utils/corsValidator.js`
|
||||
|
||||
- **Startup Validation:** Validates CORS configuration when the application starts
|
||||
- **Production Enforcement:** Fails fast if CORS origins are not configured in production
|
||||
- **Origin Format Validation:** Checks for valid URL format and warns about suspicious patterns
|
||||
- **Wildcard Detection:** Prevents wildcard (`*`) usage in production
|
||||
|
||||
**Features:**
|
||||
- Validates that `CORS_ALLOWED_ORIGINS` is set in production
|
||||
- Checks origin format (must be valid URLs)
|
||||
- Warns about HTTP origins in production (should be HTTPS)
|
||||
- Prevents wildcard origins in production
|
||||
- Provides clear error messages for misconfiguration
|
||||
|
||||
### 2. Runtime CORS Checks
|
||||
|
||||
**File:** `src/utils/corsValidator.js`
|
||||
|
||||
- **Runtime Monitoring:** Checks CORS requests at runtime
|
||||
- **Suspicious Pattern Detection:** Identifies potentially misconfigured origins
|
||||
- **Logging:** Logs warnings for suspicious CORS patterns
|
||||
|
||||
**Features:**
|
||||
- Detects HTTP origins in production
|
||||
- Identifies localhost/127.0.0.1 usage in production
|
||||
- Logs blocked origins for monitoring
|
||||
- Provides runtime feedback without blocking legitimate requests
|
||||
|
||||
### 3. Content Security Policy (CSP)
|
||||
|
||||
**File:** `src/middleware/securityHeaders.js`
|
||||
|
||||
- **CSP Headers:** Implements comprehensive Content Security Policy
|
||||
- **Nonce Support:** Generates unique nonces for each request to allow safe inline scripts/styles
|
||||
- **Strict Directives:** Restricts resource loading to prevent XSS attacks
|
||||
|
||||
**CSP Directives:**
|
||||
- `default-src 'self'` - Only allow resources from same origin
|
||||
- `script-src 'self' 'nonce-...' 'unsafe-eval'` - Scripts from self or with nonce
|
||||
- `style-src 'self' 'nonce-...'` - Styles from self or with nonce
|
||||
- `img-src 'self' data: https:` - Images from self, data URIs, or HTTPS
|
||||
- `font-src 'self' data: https:` - Fonts from self, data URIs, or HTTPS
|
||||
- `connect-src 'self' [CORS origins]` - API calls to self or allowed CORS origins
|
||||
- `frame-ancestors 'none'` - Prevent embedding (clickjacking protection)
|
||||
- `base-uri 'self'` - Restrict base tag
|
||||
- `form-action 'self'` - Forms can only submit to same origin
|
||||
- `upgrade-insecure-requests` - Upgrade HTTP to HTTPS
|
||||
|
||||
**Nonce Usage:**
|
||||
- Nonce is generated per request and stored in `res.locals.cspNonce`
|
||||
- Can be used in templates/views for inline scripts/styles
|
||||
- Example: `<script nonce="<%= res.locals.cspNonce %>">...</script>`
|
||||
|
||||
### 4. Additional Security Headers
|
||||
|
||||
**File:** `src/middleware/securityHeaders.js`
|
||||
|
||||
Added headers:
|
||||
- **Referrer-Policy:** `strict-origin-when-cross-origin` - Controls referrer information
|
||||
- **Permissions-Policy:** Restricts browser features (geolocation, camera, microphone, etc.)
|
||||
|
||||
Existing headers maintained:
|
||||
- `X-Frame-Options: DENY` - Clickjacking protection
|
||||
- `X-Content-Type-Options: nosniff` - MIME type sniffing protection
|
||||
- `X-XSS-Protection: 1; mode=block` - Legacy XSS filter
|
||||
- `Strict-Transport-Security` - HSTS for HTTPS enforcement
|
||||
|
||||
### 5. Global Security Headers Application
|
||||
|
||||
**File:** `src/index.js`
|
||||
|
||||
- **Before:** Security headers only applied to admin routes
|
||||
- **After:** Security headers applied to ALL routes globally
|
||||
- **Placement:** Applied early in middleware chain for maximum protection
|
||||
|
||||
### 6. XSS Prevention Documentation
|
||||
|
||||
**File:** `XSS_PREVENTION_GUIDE.md`
|
||||
|
||||
Comprehensive guide covering:
|
||||
- Server-side protections
|
||||
- Frontend best practices
|
||||
- Output encoding techniques
|
||||
- Framework-specific guidance (React, Vue, Angular)
|
||||
- Common attack vectors
|
||||
- Testing methodologies
|
||||
- Security checklist
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
No new environment variables required. Uses existing:
|
||||
- `CORS_ALLOWED_ORIGINS` - Comma-separated list of allowed origins (required in production)
|
||||
- `NODE_ENV` - Set to `production` for strict validation
|
||||
|
||||
### CSP Configuration
|
||||
|
||||
CSP is automatically configured. To customize:
|
||||
|
||||
1. Edit `src/middleware/securityHeaders.js`
|
||||
2. Modify the `buildCSP()` function
|
||||
3. Adjust directives as needed
|
||||
|
||||
**Note:** Current CSP allows `'unsafe-inline'` and `'unsafe-eval'` for compatibility. Consider tightening in production by:
|
||||
- Removing `'unsafe-inline'` and using nonces exclusively
|
||||
- Removing `'unsafe-eval'` if not needed
|
||||
|
||||
## Testing
|
||||
|
||||
### CORS Validation Testing
|
||||
|
||||
1. **Test startup validation:**
|
||||
```bash
|
||||
# Should fail in production without CORS_ALLOWED_ORIGINS
|
||||
NODE_ENV=production node src/index.js
|
||||
```
|
||||
|
||||
2. **Test runtime checks:**
|
||||
- Make request from non-whitelisted origin
|
||||
- Check logs for warnings
|
||||
|
||||
### CSP Testing
|
||||
|
||||
1. **Test CSP headers:**
|
||||
```bash
|
||||
curl -I http://localhost:3000/health
|
||||
# Check for Content-Security-Policy header
|
||||
```
|
||||
|
||||
2. **Test nonce generation:**
|
||||
- Each request should have unique nonce
|
||||
- Check `res.locals.cspNonce` in route handlers
|
||||
|
||||
### Browser Testing
|
||||
|
||||
1. Open browser DevTools → Network tab
|
||||
2. Check response headers for:
|
||||
- `Content-Security-Policy`
|
||||
- `X-Frame-Options`
|
||||
- `Referrer-Policy`
|
||||
- `Permissions-Policy`
|
||||
|
||||
## Security Impact
|
||||
|
||||
### Before
|
||||
- ⚠️ No CORS validation at startup
|
||||
- ⚠️ No runtime CORS monitoring
|
||||
- ⚠️ No CSP headers
|
||||
- ⚠️ Security headers only on admin routes
|
||||
- ⚠️ No XSS prevention guidance
|
||||
|
||||
### After
|
||||
- ✅ CORS validated at startup (fails fast if misconfigured)
|
||||
- ✅ Runtime CORS monitoring and logging
|
||||
- ✅ Comprehensive CSP with nonce support
|
||||
- ✅ Security headers on all routes
|
||||
- ✅ Complete XSS prevention documentation
|
||||
|
||||
## Risk Level
|
||||
|
||||
**Before:** 🟡 MEDIUM
|
||||
**After:** 🟢 LOW
|
||||
|
||||
## Migration Notes
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
None. All changes are backward compatible.
|
||||
|
||||
### Recommendations
|
||||
|
||||
1. **Tighten CSP in production:**
|
||||
- Remove `'unsafe-inline'` and use nonces
|
||||
- Remove `'unsafe-eval'` if not needed
|
||||
|
||||
2. **Monitor CORS logs:**
|
||||
- Watch for blocked origins
|
||||
- Review suspicious pattern warnings
|
||||
|
||||
3. **Update frontend code:**
|
||||
- Follow XSS prevention guide
|
||||
- Use nonces for inline scripts/styles
|
||||
- Sanitize user input
|
||||
|
||||
## Files Modified
|
||||
|
||||
1. `src/utils/corsValidator.js` - **NEW FILE**
|
||||
2. `src/middleware/securityHeaders.js` - **ENHANCED**
|
||||
3. `src/index.js` - **UPDATED**
|
||||
4. `XSS_PREVENTION_GUIDE.md` - **NEW FILE**
|
||||
5. `SECURITY_AUDIT_REPORT.md` - **UPDATED**
|
||||
|
||||
## References
|
||||
|
||||
- [OWASP XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html)
|
||||
- [MDN Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)
|
||||
- [CORS Specification](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
|
||||
|
||||
## Support
|
||||
|
||||
For questions or issues:
|
||||
1. Review `XSS_PREVENTION_GUIDE.md` for frontend guidance
|
||||
2. Check `SECURITY_AUDIT_REPORT.md` for security context
|
||||
3. Review code comments in implementation files
|
||||
|
||||
---
|
||||
|
||||
**Implementation Status:** ✅ Complete
|
||||
**Testing Status:** ✅ Syntax validated
|
||||
**Documentation Status:** ✅ Complete
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
# CSRF Protection Notes
|
||||
|
||||
## Current Implementation
|
||||
|
||||
Currently, this authentication service uses **Bearer tokens** in the `Authorization` header. This approach is **CSRF-safe** because:
|
||||
|
||||
1. **Same-Origin Policy**: Browsers enforce same-origin policy for JavaScript requests
|
||||
2. **Custom Headers**: Bearer tokens in custom headers cannot be set by malicious sites
|
||||
3. **No Cookies**: We don't store tokens in cookies, so there's no automatic cookie sending
|
||||
|
||||
## Future Considerations
|
||||
|
||||
### If Moving to HTTP-Only Cookies
|
||||
|
||||
If you decide to move tokens to HTTP-only cookies in the future (for XSS protection), **CSRF protection becomes mandatory**. Here's what you should implement:
|
||||
|
||||
### Recommended CSRF Protection Strategy
|
||||
|
||||
1. **SameSite Cookie Attribute**
|
||||
```javascript
|
||||
// Set cookies with SameSite=Strict or SameSite=Lax
|
||||
res.cookie('access_token', token, {
|
||||
httpOnly: true,
|
||||
secure: true, // HTTPS only
|
||||
sameSite: 'strict', // or 'lax'
|
||||
maxAge: 15 * 60 * 1000 // 15 minutes
|
||||
});
|
||||
```
|
||||
|
||||
2. **CSRF Token Validation**
|
||||
- Issue a CSRF token on login
|
||||
- Store CSRF token in a separate cookie (not httpOnly)
|
||||
- Require CSRF token in a custom header (e.g., `X-CSRF-Token`) for state-changing requests
|
||||
- Validate CSRF token on each request
|
||||
|
||||
3. **Double Submit Cookie Pattern**
|
||||
- Store CSRF token in both:
|
||||
- Cookie (httpOnly: false, so JavaScript can read it)
|
||||
- Request header (sent by JavaScript)
|
||||
- Validate that both values match
|
||||
|
||||
### Implementation Example (if needed)
|
||||
|
||||
```javascript
|
||||
// Middleware to validate CSRF token
|
||||
function csrfProtection(req, res, next) {
|
||||
// Skip for GET, HEAD, OPTIONS (safe methods)
|
||||
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const csrfToken = req.headers['x-csrf-token'];
|
||||
const cookieToken = req.cookies.csrf_token;
|
||||
|
||||
if (!csrfToken || !cookieToken || csrfToken !== cookieToken) {
|
||||
return res.status(403).json({ error: 'Invalid CSRF token' });
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
```
|
||||
|
||||
### Additional Recommendations
|
||||
|
||||
1. **Origin Header Validation**: Validate the `Origin` header matches your allowed origins
|
||||
2. **Referer Header Check**: As a fallback, check `Referer` header (though it can be spoofed)
|
||||
3. **State Parameter**: For OAuth flows, use state parameters to prevent CSRF
|
||||
|
||||
## Current Status
|
||||
|
||||
✅ **No CSRF protection needed** - Using Bearer tokens in headers is CSRF-safe
|
||||
|
||||
⚠️ **If you move to cookies** - Implement CSRF protection immediately
|
||||
|
||||
## References
|
||||
|
||||
- [OWASP CSRF Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html)
|
||||
- [MDN: SameSite Cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,369 +0,0 @@
|
|||
# Database Encryption Setup Guide
|
||||
**Security Hardening: Database Compromise Mitigation**
|
||||
|
||||
This guide covers the implementation of database encryption at multiple levels to protect sensitive data (PII) like phone numbers.
|
||||
|
||||
---
|
||||
|
||||
## ✅ **IMPLEMENTED: Field-Level Encryption**
|
||||
|
||||
### **What's Implemented**
|
||||
|
||||
1. **Field-Level Encryption for Phone Numbers**
|
||||
- Phone numbers are encrypted using AES-256-GCM before storing in database
|
||||
- Automatic decryption when reading from database
|
||||
- Backward compatibility with existing plaintext data
|
||||
|
||||
2. **Database Access Logging**
|
||||
- All database queries are logged (configurable)
|
||||
- Logs include: query type, tables accessed, user context, IP address, timestamp
|
||||
- Sensitive parameters are redacted in logs
|
||||
|
||||
### **Configuration**
|
||||
|
||||
#### **1. Enable Field-Level Encryption**
|
||||
|
||||
Add to your `.env` file:
|
||||
|
||||
```bash
|
||||
# Enable field-level encryption
|
||||
ENCRYPTION_ENABLED=true
|
||||
|
||||
# Generate encryption key (32 bytes, base64 encoded)
|
||||
# Run: node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
|
||||
ENCRYPTION_KEY=<your-32-byte-base64-key>
|
||||
```
|
||||
|
||||
**Generate Encryption Key:**
|
||||
```bash
|
||||
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
|
||||
```
|
||||
|
||||
**Example output:**
|
||||
```
|
||||
K8mN3pQ9rT5vW7xY1zA2bC4dE6fG8hI0jK2lM4nO6pQ8rS0tU2vW4xY6zA=
|
||||
```
|
||||
|
||||
⚠️ **IMPORTANT:**
|
||||
- Store encryption key in secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.)
|
||||
- Never commit encryption keys to version control
|
||||
- Rotate keys periodically (requires data re-encryption)
|
||||
|
||||
#### **2. Enable Database Access Logging**
|
||||
|
||||
Add to your `.env` file:
|
||||
|
||||
```bash
|
||||
# Enable database access logging
|
||||
DB_ACCESS_LOGGING_ENABLED=true
|
||||
|
||||
# Log level: 'all' (all queries) or 'sensitive' (only sensitive tables)
|
||||
DB_ACCESS_LOG_LEVEL=sensitive
|
||||
```
|
||||
|
||||
**Sensitive Tables (always logged if enabled):**
|
||||
- `users`
|
||||
- `otp_codes`
|
||||
- `otp_requests`
|
||||
- `refresh_tokens`
|
||||
- `auth_audit`
|
||||
- `user_devices`
|
||||
|
||||
### **How It Works**
|
||||
|
||||
#### **Encryption Flow:**
|
||||
1. Application receives plaintext phone number
|
||||
2. Phone number is encrypted using AES-256-GCM
|
||||
3. Encrypted value is stored in database
|
||||
4. When reading, encrypted value is automatically decrypted
|
||||
|
||||
#### **Backward Compatibility:**
|
||||
- Existing plaintext phone numbers continue to work
|
||||
- System searches for both encrypted and plaintext values
|
||||
- New records are stored encrypted
|
||||
- Old records can be migrated gradually
|
||||
|
||||
#### **Database Access Logging:**
|
||||
- All queries to sensitive tables are logged
|
||||
- Logs include sanitized parameters (no sensitive data)
|
||||
- User context (user ID, IP, user agent) is captured
|
||||
- Query duration is tracked
|
||||
|
||||
### **Code Usage**
|
||||
|
||||
#### **Encrypt Phone Number:**
|
||||
```javascript
|
||||
const { encryptPhoneNumber } = require('./utils/fieldEncryption');
|
||||
|
||||
const encryptedPhone = encryptPhoneNumber('+919876543210');
|
||||
// Store encryptedPhone in database
|
||||
```
|
||||
|
||||
#### **Decrypt Phone Number:**
|
||||
```javascript
|
||||
const { decryptPhoneNumber } = require('./utils/fieldEncryption');
|
||||
|
||||
const plaintextPhone = decryptPhoneNumber(encryptedPhoneFromDb);
|
||||
// Use plaintextPhone in application
|
||||
```
|
||||
|
||||
#### **Database Query with Context:**
|
||||
```javascript
|
||||
const db = require('./db');
|
||||
|
||||
// Create context from request
|
||||
const context = db.createContextFromRequest(req);
|
||||
|
||||
// Query with context (for logging)
|
||||
const result = await db.query(
|
||||
'SELECT * FROM users WHERE id = $1',
|
||||
[userId],
|
||||
context
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ **REQUIRED: Database-Level Encryption (TDE)**
|
||||
|
||||
### **What's Needed**
|
||||
|
||||
**Transparent Data Encryption (TDE)** must be configured at the database server level. This is infrastructure-level encryption that protects data at rest.
|
||||
|
||||
### **PostgreSQL TDE Setup**
|
||||
|
||||
PostgreSQL doesn't have built-in TDE, but you can use:
|
||||
|
||||
#### **Option 1: PostgreSQL with pgcrypto Extension (Application-Level)**
|
||||
|
||||
Already implemented via field-level encryption (see above).
|
||||
|
||||
#### **Option 2: Database-Level Encryption (Infrastructure)**
|
||||
|
||||
**For PostgreSQL on Cloud Providers:**
|
||||
|
||||
1. **AWS RDS PostgreSQL:**
|
||||
- Enable encryption at rest when creating RDS instance
|
||||
- Uses AWS KMS for key management
|
||||
- Encryption is transparent to application
|
||||
|
||||
2. **Google Cloud SQL:**
|
||||
- Enable encryption at rest in instance settings
|
||||
- Uses Google Cloud KMS
|
||||
|
||||
3. **Azure Database for PostgreSQL:**
|
||||
- Enable Transparent Data Encryption (TDE)
|
||||
- Uses Azure Key Vault
|
||||
|
||||
4. **Self-Hosted PostgreSQL:**
|
||||
- Use encrypted filesystem (LUKS, BitLocker)
|
||||
- Use PostgreSQL with encryption at filesystem level
|
||||
|
||||
### **Setup Instructions**
|
||||
|
||||
#### **AWS RDS PostgreSQL:**
|
||||
|
||||
1. **Create Encrypted RDS Instance:**
|
||||
```bash
|
||||
aws rds create-db-instance \
|
||||
--db-instance-identifier farm-auth-db \
|
||||
--db-instance-class db.t3.micro \
|
||||
--engine postgres \
|
||||
--master-username postgres \
|
||||
--master-user-password <password> \
|
||||
--allocated-storage 20 \
|
||||
--storage-encrypted \
|
||||
--kms-key-id <kms-key-id>
|
||||
```
|
||||
|
||||
2. **Or Enable Encryption on Existing Instance:**
|
||||
- Create snapshot
|
||||
- Restore snapshot with encryption enabled
|
||||
- Update application connection string
|
||||
|
||||
#### **Google Cloud SQL:**
|
||||
|
||||
1. **Enable Encryption:**
|
||||
```bash
|
||||
gcloud sql instances create farm-auth-db \
|
||||
--database-version=POSTGRES_14 \
|
||||
--tier=db-f1-micro \
|
||||
--storage-type=SSD \
|
||||
--disk-size=20GB \
|
||||
--disk-encryption-key=<kms-key>
|
||||
```
|
||||
|
||||
#### **Azure Database for PostgreSQL:**
|
||||
|
||||
1. **Enable TDE:**
|
||||
- Navigate to Azure Portal
|
||||
- Select your PostgreSQL server
|
||||
- Go to "Data encryption"
|
||||
- Enable "Data encryption"
|
||||
- Select Key Vault key
|
||||
|
||||
---
|
||||
|
||||
## 📋 **Migration Plan**
|
||||
|
||||
### **Phase 1: Enable Field-Level Encryption (Current)**
|
||||
|
||||
✅ **Status: Implemented**
|
||||
|
||||
1. Set `ENCRYPTION_ENABLED=true`
|
||||
2. Set `ENCRYPTION_KEY` (from secrets manager)
|
||||
3. New records are automatically encrypted
|
||||
4. Existing plaintext records continue to work (backward compatibility)
|
||||
|
||||
### **Phase 2: Migrate Existing Data**
|
||||
|
||||
**Script to migrate existing plaintext phone numbers:**
|
||||
|
||||
```sql
|
||||
-- WARNING: This requires application-level decryption/encryption
|
||||
-- Run migration script that:
|
||||
-- 1. Reads plaintext phone numbers
|
||||
-- 2. Encrypts them using application encryption
|
||||
-- 3. Updates database with encrypted values
|
||||
|
||||
-- Example (run via Node.js script, not direct SQL):
|
||||
-- const { encryptPhoneNumber } = require('./utils/fieldEncryption');
|
||||
-- const users = await db.query('SELECT id, phone_number FROM users WHERE phone_number NOT LIKE \'%:%\'');
|
||||
-- for (const user of users.rows) {
|
||||
-- const encrypted = encryptPhoneNumber(user.phone_number);
|
||||
-- await db.query('UPDATE users SET phone_number = $1 WHERE id = $2', [encrypted, user.id]);
|
||||
-- }
|
||||
```
|
||||
|
||||
### **Phase 3: Enable Database-Level Encryption (TDE)**
|
||||
|
||||
1. **For Cloud Providers:**
|
||||
- Enable encryption at rest in database settings
|
||||
- No application changes needed (transparent)
|
||||
|
||||
2. **For Self-Hosted:**
|
||||
- Set up encrypted filesystem
|
||||
- Migrate database to encrypted volume
|
||||
- Update backup procedures
|
||||
|
||||
---
|
||||
|
||||
## 🔒 **Security Best Practices**
|
||||
|
||||
### **1. Key Management**
|
||||
|
||||
- ✅ Store encryption keys in secrets manager (AWS Secrets Manager, HashiCorp Vault)
|
||||
- ✅ Rotate keys periodically (every 90 days recommended)
|
||||
- ✅ Use separate keys for different environments (dev, staging, prod)
|
||||
- ✅ Never commit keys to version control
|
||||
|
||||
### **2. Access Control**
|
||||
|
||||
- ✅ Use least privilege for database users
|
||||
- ✅ Enable database access logging (`DB_ACCESS_LOGGING_ENABLED=true`)
|
||||
- ✅ Review access logs regularly
|
||||
- ✅ Set up alerts for suspicious access patterns
|
||||
|
||||
### **3. Backup Encryption**
|
||||
|
||||
- ✅ Encrypt database backups
|
||||
- ✅ Store backup encryption keys separately
|
||||
- ✅ Test backup restoration procedures
|
||||
|
||||
### **4. Monitoring**
|
||||
|
||||
- ✅ Monitor database access logs
|
||||
- ✅ Set up alerts for:
|
||||
- Unusual access patterns
|
||||
- Failed authentication attempts
|
||||
- Large data exports
|
||||
- Access from unexpected IPs
|
||||
|
||||
---
|
||||
|
||||
## 📊 **Compliance**
|
||||
|
||||
### **GDPR Compliance**
|
||||
|
||||
- ✅ Personal data (phone numbers) is encrypted at rest
|
||||
- ✅ Access to personal data is logged
|
||||
- ✅ Encryption keys are managed securely
|
||||
|
||||
### **PCI DSS Compliance**
|
||||
|
||||
- ✅ Sensitive data is encrypted
|
||||
- ✅ Access controls are in place
|
||||
- ✅ Audit logging is enabled
|
||||
|
||||
---
|
||||
|
||||
## 🚨 **Troubleshooting**
|
||||
|
||||
### **Issue: Encryption Not Working**
|
||||
|
||||
**Symptoms:**
|
||||
- Phone numbers stored as plaintext
|
||||
- Decryption errors
|
||||
|
||||
**Solutions:**
|
||||
1. Check `ENCRYPTION_ENABLED=true` in environment
|
||||
2. Verify `ENCRYPTION_KEY` is set and valid (32 bytes, base64)
|
||||
3. Check application logs for encryption errors
|
||||
|
||||
### **Issue: Backward Compatibility Broken**
|
||||
|
||||
**Symptoms:**
|
||||
- Existing users can't log in
|
||||
- Phone number lookups fail
|
||||
|
||||
**Solutions:**
|
||||
1. Ensure queries search for both encrypted and plaintext
|
||||
2. Check that `decryptPhoneNumber` handles plaintext gracefully
|
||||
3. Verify migration script completed successfully
|
||||
|
||||
### **Issue: Database Access Logging Not Working**
|
||||
|
||||
**Symptoms:**
|
||||
- No entries in `db_access_log` table
|
||||
|
||||
**Solutions:**
|
||||
1. Check `DB_ACCESS_LOGGING_ENABLED=true`
|
||||
2. Verify `db_access_log` table exists (auto-created on first log)
|
||||
3. Check application logs for logging errors
|
||||
|
||||
---
|
||||
|
||||
## 📝 **Checklist**
|
||||
|
||||
### **Before Production:**
|
||||
|
||||
- [ ] Generate encryption key and store in secrets manager
|
||||
- [ ] Set `ENCRYPTION_ENABLED=true` in production environment
|
||||
- [ ] Set `DB_ACCESS_LOGGING_ENABLED=true` in production
|
||||
- [ ] Enable database-level encryption (TDE) at infrastructure level
|
||||
- [ ] Test encryption/decryption with sample data
|
||||
- [ ] Verify backward compatibility with existing data
|
||||
- [ ] Set up monitoring for database access logs
|
||||
- [ ] Document key rotation procedure
|
||||
- [ ] Test backup and restore procedures
|
||||
- [ ] Review and update access control policies
|
||||
|
||||
---
|
||||
|
||||
## 🔗 **Related Documentation**
|
||||
|
||||
- `SECURITY_AUDIT_REPORT.md` - Security audit findings
|
||||
- `src/utils/fieldEncryption.js` - Encryption implementation
|
||||
- `src/middleware/dbAccessLogger.js` - Database access logging
|
||||
- `src/db.js` - Database wrapper with logging support
|
||||
|
||||
---
|
||||
|
||||
## 📞 **Support**
|
||||
|
||||
For questions or issues:
|
||||
1. Check application logs for encryption/decryption errors
|
||||
2. Review database access logs for suspicious activity
|
||||
3. Verify environment variables are set correctly
|
||||
4. Test with sample data before production deployment
|
||||
|
||||
|
|
@ -1,341 +0,0 @@
|
|||
# Database Overview - What Your Database is Doing
|
||||
|
||||
## Database Purpose
|
||||
|
||||
Your PostgreSQL database is the **central data store** for a **Farm Market Authentication Service** that handles:
|
||||
1. **User Authentication** (Phone-based OTP login)
|
||||
2. **Farm/Marketplace Data** (Animals, Listings, Locations)
|
||||
3. **Security & Audit Logging**
|
||||
4. **Session Management** (Multi-device support)
|
||||
|
||||
---
|
||||
|
||||
## Table Categories
|
||||
|
||||
### 🔐 **AUTHENTICATION TABLES**
|
||||
|
||||
#### 1. `users` - User Accounts
|
||||
**Purpose:** Store user account information
|
||||
**Key Fields:**
|
||||
- `id` (UUID) - Primary key
|
||||
- `phone_number` (UNIQUE) - Phone is the login identifier
|
||||
- `name` - User's name (can be NULL until profile completed)
|
||||
- `user_type` - Enum: 'seller', 'buyer', 'service_provider'
|
||||
- `role` - System role: 'user', 'admin', 'moderator'
|
||||
- `token_version` - Incremented on logout-all to invalidate all tokens
|
||||
- `last_login_at` - Tracks last login time
|
||||
|
||||
**Current Usage:**
|
||||
- Created **AFTER** OTP verification (find-or-create pattern)
|
||||
- Phone number is encrypted before storage
|
||||
- One phone number = One account (UNIQUE constraint)
|
||||
|
||||
#### 2. `otp_codes` - OTP Storage (ACTIVELY USED)
|
||||
**Purpose:** Store OTP codes for phone verification
|
||||
**Key Fields:**
|
||||
- `id` (UUID)
|
||||
- `phone_number` - Phone requesting OTP (NO user_id - user doesn't exist yet)
|
||||
- `otp_hash` - Hashed OTP code (bcrypt)
|
||||
- `expires_at` - OTP expiry (2 minutes default)
|
||||
- `attempt_count` - Failed verification attempts
|
||||
- `created_at`
|
||||
|
||||
**Current Usage:**
|
||||
- Created when `/auth/request-otp` is called
|
||||
- Deleted after successful verification or expiry
|
||||
- Used for OTP verification in `/auth/verify-otp`
|
||||
|
||||
#### 3. `otp_requests` - Legacy OTP Table (NOT ACTIVELY USED)
|
||||
**Purpose:** Alternative OTP storage (currently unused)
|
||||
**Note:** Your code uses `otp_codes` table, not `otp_requests`
|
||||
|
||||
#### 4. `refresh_tokens` - Session Management
|
||||
**Purpose:** Store refresh tokens for JWT authentication
|
||||
**Key Fields:**
|
||||
- `id` (UUID)
|
||||
- `user_id` - Links to users table
|
||||
- `token_id` (UNIQUE) - UUID identifier for token
|
||||
- `token_hash` - Hashed refresh token (bcrypt)
|
||||
- `device_id` - Device identifier from client
|
||||
- `expires_at` - Token expiry
|
||||
- `revoked_at` - NULL = active, timestamp = revoked
|
||||
- `rotated_from_id` - Links to previous token (rotation tracking)
|
||||
- `reuse_detected_at` - Detects token theft/reuse
|
||||
|
||||
**Current Usage:**
|
||||
- Created during OTP verification
|
||||
- Rotated on each refresh (old token revoked, new one created)
|
||||
- Tracks which device each token belongs to
|
||||
- Supports multi-device logins (one token per device)
|
||||
|
||||
#### 5. `user_devices` - Device Tracking
|
||||
**Purpose:** Track user's logged-in devices
|
||||
**Key Fields:**
|
||||
- `id` (UUID)
|
||||
- `user_id` - Links to users
|
||||
- `device_identifier` - Unique device ID (e.g., Firebase Installation ID)
|
||||
- `device_platform` - 'android', 'ios'
|
||||
- `device_model`, `os_version`, `app_version`
|
||||
- `first_seen_at`, `last_seen_at`
|
||||
- `is_active` - Whether device session is active
|
||||
- UNIQUE(`user_id`, `device_identifier`) - One record per user-device pair
|
||||
|
||||
**Current Usage:**
|
||||
- Created/updated during OTP verification
|
||||
- Tracks all devices a user is logged in from
|
||||
- Used for device management (view/revoke devices)
|
||||
|
||||
#### 6. `auth_audit` - Security Audit Log
|
||||
**Purpose:** Log all authentication events for security monitoring
|
||||
**Key Fields:**
|
||||
- `id` (UUID)
|
||||
- `user_id` - NULL if user doesn't exist yet (e.g., failed login)
|
||||
- `action` - 'otp_request', 'otp_verify', 'token_refresh', 'logout', etc.
|
||||
- `status` - 'success', 'failed', 'error'
|
||||
- `ip_address`, `user_agent`, `device_id`
|
||||
- `meta` (JSONB) - Additional metadata (errors, risk scores, etc.)
|
||||
- `created_at`
|
||||
|
||||
**Current Usage:**
|
||||
- Logs every authentication event
|
||||
- Used by admin dashboard for security monitoring
|
||||
- Used for risk scoring and anomaly detection
|
||||
- Tracks suspicious activities (enumeration, brute force, etc.)
|
||||
|
||||
---
|
||||
|
||||
### 🏪 **MARKETPLACE TABLES**
|
||||
|
||||
#### 7. `species` - Animal Species
|
||||
**Purpose:** Master data for animal species (e.g., "Cow", "Goat", "Sheep")
|
||||
**Key Fields:**
|
||||
- `id` (UUID)
|
||||
- `name` (UNIQUE)
|
||||
|
||||
#### 8. `breeds` - Animal Breeds
|
||||
**Purpose:** Breeds within each species (e.g., "Holstein", "Jersey" for Cow)
|
||||
**Key Fields:**
|
||||
- `id` (UUID)
|
||||
- `species_id` - Links to species
|
||||
- `name` - Breed name
|
||||
- UNIQUE(`species_id`, `name`)
|
||||
|
||||
#### 9. `locations` - Geographic Locations
|
||||
**Purpose:** Store farm/home/office locations
|
||||
**Key Fields:**
|
||||
- `id` (UUID)
|
||||
- `user_id` - Links to users (NULL = temporary/captured location)
|
||||
- `is_saved_address` - Whether user saved this location
|
||||
- `location_type` - Enum: 'farm', 'home', 'office', etc.
|
||||
- `country`, `state`, `district`, `city_village`, `pincode`
|
||||
- `lat`, `lng` - GPS coordinates
|
||||
- `source_type` - 'device_gps', 'manual', 'unknown'
|
||||
- `source_confidence` - 'high', 'medium', 'low'
|
||||
|
||||
**Current Usage:**
|
||||
- Stores where animals are kept
|
||||
- Stores listing locations
|
||||
- Can be temporary (no user_id) or saved (with user_id)
|
||||
|
||||
#### 10. `animals` - Animal Records
|
||||
**Purpose:** Store individual animal information
|
||||
**Key Fields:**
|
||||
- `id` (UUID)
|
||||
- `species_id`, `breed_id` - Animal classification
|
||||
- `location_id` - Where animal is kept
|
||||
- `sex` - Enum: 'M', 'F', 'Neutered'
|
||||
- `age_months`, `weight_kg`
|
||||
- `purpose` - Enum: 'dairy', 'meat', 'breeding', 'pet', 'work', 'other'
|
||||
- `health_status` - Enum: 'healthy', 'minor_issues', 'serious_issues'
|
||||
- `vaccinated`, `dewormed`
|
||||
- `milk_yield_litre_per_day` - For dairy animals
|
||||
- `ear_tag_no` - Ear tag identification
|
||||
- `quantity` - Number of animals (default: 1)
|
||||
|
||||
**Current Usage:**
|
||||
- Created by sellers when they want to list animals
|
||||
- Linked to listings (one animal per listing)
|
||||
|
||||
#### 11. `listings` - Marketplace Listings
|
||||
**Purpose:** Animals for sale/stud service/adoption
|
||||
**Key Fields:**
|
||||
- `id` (UUID)
|
||||
- `seller_id` - Links to users (who is selling)
|
||||
- `animal_id` - Links to animals (what is being sold)
|
||||
- `title`, `price`, `currency` (default: 'INR')
|
||||
- `listing_type` - Enum: 'sale', 'stud_service', 'adoption'
|
||||
- `status` - Enum: 'active', 'sold', 'expired', 'hidden'
|
||||
- `is_negotiable` - Price negotiation allowed
|
||||
- Engagement metrics: `views_count`, `bookmarks_count`, `enquiries_call_count`, `enquiries_whatsapp_count`, `clicks_count`
|
||||
|
||||
**Current Usage:**
|
||||
- Created when seller lists an animal
|
||||
- Tracked engagement metrics
|
||||
- Can be marked as sold/expired/hidden
|
||||
|
||||
#### 12. `listing_images` - Listing Photos
|
||||
**Purpose:** Store images for listings
|
||||
**Key Fields:**
|
||||
- `id` (UUID)
|
||||
- `listing_id` - Links to listings
|
||||
- `image_url` - URL to image
|
||||
- `is_primary` - Main image flag
|
||||
- `sort_order` - Image ordering
|
||||
|
||||
---
|
||||
|
||||
### 🔮 **FUTURE TABLES**
|
||||
|
||||
#### 13. `oauth_accounts` - OAuth Integration (Placeholder)
|
||||
**Purpose:** Future OAuth support (Google, Facebook, Apple)
|
||||
**Key Fields:**
|
||||
- `id` (UUID)
|
||||
- `user_id` - Links to users
|
||||
- `provider` - 'google', 'facebook', 'apple'
|
||||
- `provider_user_id` - OAuth provider's user ID
|
||||
- UNIQUE(`provider`, `provider_user_id`)
|
||||
|
||||
**Current Usage:**
|
||||
- Table exists but not actively used yet
|
||||
|
||||
---
|
||||
|
||||
## Key Relationships & Constraints
|
||||
|
||||
### Foreign Key Relationships:
|
||||
```
|
||||
users (1) ──< (many) user_devices
|
||||
users (1) ──< (many) refresh_tokens
|
||||
users (1) ──< (many) listings
|
||||
users (1) ──< (many) locations
|
||||
listings (1) ──< (many) listing_images
|
||||
listings (many) ──> (1) animals
|
||||
animals (many) ──> (1) locations
|
||||
animals (many) ──> (1) species
|
||||
animals (many) ──> (1) breeds
|
||||
breeds (many) ──> (1) species
|
||||
```
|
||||
|
||||
### Cascade Behaviors:
|
||||
- **DELETE user** → Cascades delete to: `user_devices`, `refresh_tokens`, `listings`, `locations`
|
||||
- **DELETE species** → RESTRICTED (can't delete if animals reference it)
|
||||
- **DELETE breed** → Sets `breed_id` to NULL on animals (SET NULL)
|
||||
- **DELETE listing** → Cascades delete to: `listing_images`
|
||||
- **DELETE animal** → RESTRICTED (can't delete if listings reference it)
|
||||
- **DELETE location** → RESTRICTED (can't delete if animals reference it)
|
||||
|
||||
---
|
||||
|
||||
## Current Data Flow
|
||||
|
||||
### Authentication Flow:
|
||||
```
|
||||
1. User requests OTP
|
||||
└─> INSERT into `otp_codes` (phone_number, otp_hash, expires_at)
|
||||
|
||||
2. User verifies OTP
|
||||
├─> Verify OTP from `otp_codes` table
|
||||
├─> DELETE OTP from `otp_codes`
|
||||
├─> FIND or CREATE user in `users` table
|
||||
├─> INSERT/UPDATE `user_devices` table
|
||||
├─> INSERT into `refresh_tokens` table
|
||||
└─> INSERT into `auth_audit` (log event)
|
||||
|
||||
3. User refreshes token
|
||||
├─> Verify refresh token from `refresh_tokens` table
|
||||
├─> ROTATE token (revoke old, create new)
|
||||
└─> INSERT into `auth_audit` (log event)
|
||||
```
|
||||
|
||||
### Marketplace Flow:
|
||||
```
|
||||
1. Seller creates listing
|
||||
├─> CREATE `location` (where animal is)
|
||||
├─> CREATE `animal` (animal details)
|
||||
└─> CREATE `listing` (link animal to seller)
|
||||
|
||||
2. Buyer views listing
|
||||
└─> UPDATE `listings.views_count`
|
||||
|
||||
3. Buyer bookmarks/contacts
|
||||
└─> UPDATE engagement metrics (bookmarks_count, enquiries_*)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Features
|
||||
|
||||
### 1. **Automatic Timestamps**
|
||||
- All tables have `created_at` and `updated_at`
|
||||
- `updated_at` automatically updated via database triggers
|
||||
|
||||
### 2. **UUID Primary Keys**
|
||||
- All tables use UUIDs (not auto-increment integers)
|
||||
- Generated via PostgreSQL `uuid-ossp` extension
|
||||
|
||||
### 3. **Enum Types**
|
||||
- PostgreSQL ENUM types for controlled values:
|
||||
- `sex_enum`, `purpose_enum`, `health_status_enum`
|
||||
- `location_type_enum`, `listing_type_enum`, `listing_status_enum`
|
||||
- `user_type_enum`, `listing_role_enum`
|
||||
|
||||
### 4. **Indexes**
|
||||
- Indexes on foreign keys for fast joins
|
||||
- Indexes on frequently queried fields (phone_number, expires_at, status)
|
||||
- Partial indexes for performance (e.g., unconsumed OTPs)
|
||||
|
||||
### 5. **Data Integrity**
|
||||
- CHECK constraints (e.g., non-negative counters)
|
||||
- UNIQUE constraints (phone_number, user+device pairs)
|
||||
- Foreign key constraints with appropriate CASCADE/RESTRICT behaviors
|
||||
|
||||
---
|
||||
|
||||
## What's NOT in the Database (Handled Elsewhere)
|
||||
|
||||
1. **Access Tokens (JWT)** - Stored only on client, validated via signature
|
||||
2. **Rate Limiting Counters** - Stored in Redis or in-memory (not PostgreSQL)
|
||||
3. **OTP Codes (Plain)** - Only hashed versions stored, plain codes never persisted
|
||||
4. **SMS Messages** - Sent via Twilio, not stored in database
|
||||
5. **User Passwords** - Phone-based auth only, no password storage
|
||||
|
||||
---
|
||||
|
||||
## Current Issues / Design Notes
|
||||
|
||||
### ⚠️ Dual OTP Tables
|
||||
- `otp_codes` - **ACTIVELY USED** by your code
|
||||
- `otp_requests` - **NOT USED** (legacy/alternative table)
|
||||
- **Recommendation:** Consider removing `otp_requests` if not needed
|
||||
|
||||
### ⚠️ Phone Number Encryption
|
||||
- Phone numbers in `users` and `otp_codes` are **encrypted at application level**
|
||||
- Database stores encrypted values, not plaintext
|
||||
- Encryption handled by `src/utils/fieldEncryption.js`
|
||||
|
||||
### ✅ Multi-Device Support
|
||||
- Users can log in from multiple devices simultaneously
|
||||
- Each device has its own refresh token
|
||||
- Devices can be managed (viewed/revoked) via API
|
||||
|
||||
### ✅ Security Features
|
||||
- OTP attempt tracking (prevents brute force)
|
||||
- Token rotation (prevents token reuse)
|
||||
- Audit logging (tracks all auth events)
|
||||
- Risk scoring (detects suspicious activity)
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Your database is a **well-structured PostgreSQL database** that:
|
||||
- ✅ Handles phone-based authentication securely
|
||||
- ✅ Supports a farm marketplace (animals, listings, locations)
|
||||
- ✅ Tracks multi-device user sessions
|
||||
- ✅ Logs security events for monitoring
|
||||
- ✅ Uses proper constraints and relationships
|
||||
- ✅ Supports data encryption at application level
|
||||
|
||||
The design follows good practices with UUIDs, enums, cascades, and proper indexing.
|
||||
|
||||
|
||||
|
|
@ -1,428 +0,0 @@
|
|||
# Device Management Features
|
||||
|
||||
## Overview
|
||||
|
||||
The auth service now supports proper multi-device login with device management capabilities. **One phone number = One account**, but that account can be logged in from **multiple devices simultaneously**.
|
||||
|
||||
## Key Features
|
||||
|
||||
### ✅ Multi-Device Support
|
||||
- Same phone number can log in from multiple devices
|
||||
- Each device gets its own refresh token
|
||||
- Devices can be active simultaneously
|
||||
- Independent sessions per device
|
||||
|
||||
### ✅ Device Tracking
|
||||
- All login attempts are logged to `auth_audit` table
|
||||
- New device detection flags (`is_new_device`, `is_new_account`)
|
||||
- Device metadata tracking (platform, model, OS version, etc.)
|
||||
|
||||
### ✅ Device Management
|
||||
- List all active devices
|
||||
- Revoke/logout specific devices
|
||||
- Logout all other devices (keep current)
|
||||
|
||||
---
|
||||
|
||||
## Updated Endpoints
|
||||
|
||||
### 1. Verify OTP (Enhanced Response)
|
||||
|
||||
**Endpoint:** `POST /auth/verify-otp`
|
||||
|
||||
**Response now includes:**
|
||||
```json
|
||||
{
|
||||
"user": { ... },
|
||||
"access_token": "...",
|
||||
"refresh_token": "...",
|
||||
"needs_profile": true,
|
||||
"is_new_device": false, // ← NEW: Is this a new device?
|
||||
"is_new_account": false, // ← NEW: Is this a new account?
|
||||
"active_devices_count": 2 // ← NEW: How many devices are active?
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases:**
|
||||
- `is_new_device: true` → Show security notification to user
|
||||
- `is_new_account: true` → Welcome new user flow
|
||||
- `active_devices_count` → Display in settings/profile
|
||||
|
||||
---
|
||||
|
||||
### 2. Get User Info
|
||||
|
||||
**Endpoint:** `GET /users/me`
|
||||
|
||||
**Headers:**
|
||||
```
|
||||
Authorization: Bearer <access_token>
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"phone_number": "+919876543210",
|
||||
"name": "John Doe",
|
||||
"role": "user",
|
||||
"user_type": "seller",
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"last_login_at": "2024-01-15T10:30:00Z",
|
||||
"active_devices_count": 2
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. List Active Devices
|
||||
|
||||
**Endpoint:** `GET /users/me/devices`
|
||||
|
||||
**Headers:**
|
||||
```
|
||||
Authorization: Bearer <access_token>
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"devices": [
|
||||
{
|
||||
"device_identifier": "android-device-123",
|
||||
"device_platform": "android",
|
||||
"device_model": "Samsung Galaxy S21",
|
||||
"os_version": "Android 14",
|
||||
"app_version": "1.0.0",
|
||||
"language_code": "en-IN",
|
||||
"timezone": "Asia/Kolkata",
|
||||
"first_seen_at": "2024-01-10T08:00:00Z",
|
||||
"last_seen_at": "2024-01-15T10:30:00Z",
|
||||
"is_active": true
|
||||
},
|
||||
{
|
||||
"device_identifier": "iphone-device-456",
|
||||
"device_platform": "ios",
|
||||
"device_model": "iPhone 13",
|
||||
"os_version": "iOS 17.2",
|
||||
"app_version": "1.0.0",
|
||||
"language_code": "en-IN",
|
||||
"timezone": "Asia/Kolkata",
|
||||
"first_seen_at": "2024-01-12T14:20:00Z",
|
||||
"last_seen_at": "2024-01-15T09:15:00Z",
|
||||
"is_active": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Revoke Specific Device
|
||||
|
||||
**Endpoint:** `DELETE /users/me/devices/:device_id`
|
||||
|
||||
**Headers:**
|
||||
```
|
||||
Authorization: Bearer <access_token>
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"message": "Device logged out successfully"
|
||||
}
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- Marks device as inactive in `user_devices` table
|
||||
- Revokes all refresh tokens for that device
|
||||
- Logs the action in `auth_audit` table
|
||||
|
||||
**Kotlin Example:**
|
||||
```kotlin
|
||||
suspend fun revokeDevice(deviceId: String, accessToken: String): Result<Unit> {
|
||||
val response = apiClient.delete("/users/me/devices/$deviceId") {
|
||||
header("Authorization", "Bearer $accessToken")
|
||||
}
|
||||
return if (response.status.isSuccess()) {
|
||||
Result.success(Unit)
|
||||
} else {
|
||||
Result.failure(Exception("Failed to revoke device"))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Logout All Other Devices
|
||||
|
||||
**Endpoint:** `POST /users/me/logout-all-other-devices`
|
||||
|
||||
**Headers:**
|
||||
```
|
||||
Authorization: Bearer <access_token>
|
||||
X-Device-Id: <current_device_id> // ← Required header
|
||||
```
|
||||
|
||||
**OR Request Body:**
|
||||
```json
|
||||
{
|
||||
"current_device_id": "android-device-123"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"message": "Logged out 2 device(s)",
|
||||
"revoked_devices_count": 2
|
||||
}
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- Keeps current device active
|
||||
- Logs out all other devices
|
||||
- Revokes refresh tokens for all other devices
|
||||
|
||||
**Kotlin Example:**
|
||||
```kotlin
|
||||
suspend fun logoutAllOtherDevices(
|
||||
currentDeviceId: String,
|
||||
accessToken: String
|
||||
): Result<LogoutAllResponse> {
|
||||
val response = apiClient.post("/users/me/logout-all-other-devices") {
|
||||
header("Authorization", "Bearer $accessToken")
|
||||
header("X-Device-Id", currentDeviceId)
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(JsonObject(mapOf(
|
||||
"current_device_id" to JsonPrimitive(currentDeviceId)
|
||||
)))
|
||||
}
|
||||
return Result.success(response.body())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Authentication Flow Example
|
||||
|
||||
### Scenario: User logs in from new phone
|
||||
|
||||
1. **Request OTP** (same phone number)
|
||||
```kotlin
|
||||
POST /auth/request-otp
|
||||
{ "phone_number": "+919876543210" }
|
||||
```
|
||||
|
||||
2. **Verify OTP** (from new device)
|
||||
```kotlin
|
||||
POST /auth/verify-otp
|
||||
{
|
||||
"phone_number": "+919876543210",
|
||||
"code": "123456",
|
||||
"device_id": "new-phone-device-id",
|
||||
"device_info": { "platform": "android", ... }
|
||||
}
|
||||
```
|
||||
|
||||
3. **Response:**
|
||||
```json
|
||||
{
|
||||
"user": { ... },
|
||||
"access_token": "...",
|
||||
"refresh_token": "...",
|
||||
"is_new_device": true, // ← This is a new device
|
||||
"is_new_account": false, // ← But existing account
|
||||
"active_devices_count": 2 // ← Now 2 devices active
|
||||
}
|
||||
```
|
||||
|
||||
4. **Mobile App Action:**
|
||||
- Show notification: "New device logged in: Android Phone"
|
||||
- Display in security settings: "Active Devices: 2"
|
||||
- Allow user to revoke old device if needed
|
||||
|
||||
---
|
||||
|
||||
## Security Features
|
||||
|
||||
### ✅ New Device Detection
|
||||
- Automatically detected on login
|
||||
- Logged in `auth_audit` table
|
||||
- Flag returned in response for app to show alert
|
||||
|
||||
### ✅ Device Activity Tracking
|
||||
- `last_seen_at` updated on token refresh
|
||||
- Tracks when device was last active
|
||||
- Helps identify abandoned/inactive devices
|
||||
|
||||
### ✅ Audit Logging
|
||||
All authentication events logged:
|
||||
- Login attempts (success/failure)
|
||||
- Device revocations
|
||||
- Logout actions
|
||||
- Token refreshes
|
||||
|
||||
Query audit logs:
|
||||
```sql
|
||||
SELECT * FROM auth_audit
|
||||
WHERE user_id = 'user-uuid'
|
||||
ORDER BY created_at DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mobile App Implementation
|
||||
|
||||
### Show Active Devices Screen
|
||||
|
||||
```kotlin
|
||||
class DeviceManagementActivity : AppCompatActivity() {
|
||||
private val authManager = AuthManager(...)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
lifecycleScope.launch {
|
||||
val devices = authManager.getActiveDevices()
|
||||
devices.forEach { device ->
|
||||
// Display device info
|
||||
// Show "Revoke" button
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun revokeDevice(deviceId: String) {
|
||||
lifecycleScope.launch {
|
||||
authManager.revokeDevice(deviceId)
|
||||
.onSuccess {
|
||||
// Refresh device list
|
||||
// Show toast: "Device logged out"
|
||||
}
|
||||
.onFailure { showError("Failed to revoke device") }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Handle New Device Login
|
||||
|
||||
```kotlin
|
||||
private fun handleLoginResponse(response: VerifyOtpResponse) {
|
||||
if (response.is_new_device && !response.is_new_account) {
|
||||
// Show security alert
|
||||
showDialog(
|
||||
title = "New Device Detected",
|
||||
message = "You've logged in from a new device. " +
|
||||
"If this wasn't you, please change your password.",
|
||||
positiveButton = "OK"
|
||||
)
|
||||
}
|
||||
|
||||
// Save tokens
|
||||
tokenManager.saveTokens(response.access_token, response.refresh_token)
|
||||
|
||||
// Navigate to home/profile
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
### user_devices Table
|
||||
```sql
|
||||
CREATE TABLE user_devices (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID NOT NULL REFERENCES users(id),
|
||||
device_identifier TEXT,
|
||||
device_platform TEXT NOT NULL,
|
||||
device_model TEXT,
|
||||
os_version TEXT,
|
||||
app_version TEXT,
|
||||
language_code TEXT,
|
||||
timezone TEXT,
|
||||
first_seen_at TIMESTAMPTZ NOT NULL,
|
||||
last_seen_at TIMESTAMPTZ,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
UNIQUE (user_id, device_identifier)
|
||||
);
|
||||
```
|
||||
|
||||
### auth_audit Table
|
||||
```sql
|
||||
CREATE TABLE auth_audit (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID REFERENCES users(id),
|
||||
action VARCHAR(100) NOT NULL, -- 'login', 'device_revoked', etc.
|
||||
status VARCHAR(50) NOT NULL, -- 'success', 'failed'
|
||||
device_id VARCHAR(255),
|
||||
ip_address VARCHAR(45),
|
||||
user_agent TEXT,
|
||||
meta JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **Phone Number = Account Ownership**
|
||||
- One phone number = One account
|
||||
- If someone else uses your phone number, they access your account
|
||||
- Always protect your phone number/SIM card
|
||||
|
||||
2. **Multiple Devices = Same Account**
|
||||
- All devices access the same user account
|
||||
- Data is shared across devices
|
||||
- Logout on one device doesn't affect others
|
||||
|
||||
3. **Device ID Must Be Consistent**
|
||||
- Use same `device_id` for same physical device
|
||||
- Don't randomly generate new IDs
|
||||
- Use Android ID, Installation ID, or Firebase Installation ID
|
||||
|
||||
4. **Token Rotation**
|
||||
- Refresh tokens rotate on each refresh
|
||||
- Always save the new `refresh_token`
|
||||
- Old tokens become invalid
|
||||
|
||||
5. **Device Revocation**
|
||||
- Revoking a device logs it out immediately
|
||||
- Refresh tokens for that device are revoked
|
||||
- User must re-login on that device
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Multi-Device Login
|
||||
```bash
|
||||
# Device 1
|
||||
curl -X POST http://localhost:3000/auth/verify-otp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"phone_number": "+919876543210",
|
||||
"code": "123456",
|
||||
"device_id": "device-1"
|
||||
}'
|
||||
|
||||
# Device 2 (same phone, different device)
|
||||
curl -X POST http://localhost:3000/auth/verify-otp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"phone_number": "+919876543210",
|
||||
"code": "123456",
|
||||
"device_id": "device-2"
|
||||
}'
|
||||
|
||||
# Check active devices
|
||||
curl -X GET http://localhost:3000/users/me/devices \
|
||||
-H "Authorization: Bearer <access_token>"
|
||||
```
|
||||
|
||||
Both devices should be logged in and visible in the devices list.
|
||||
374
DOCKER_SETUP.md
374
DOCKER_SETUP.md
|
|
@ -1,374 +0,0 @@
|
|||
# Docker PostgreSQL Setup Guide
|
||||
|
||||
This guide will help you set up and run PostgreSQL using Docker for the Farm Auth Service.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### 1. Install Docker Desktop
|
||||
|
||||
**For Windows:**
|
||||
1. Download Docker Desktop from: https://www.docker.com/products/docker-desktop
|
||||
2. Run the installer and follow the setup wizard
|
||||
3. Restart your computer if prompted
|
||||
4. Launch Docker Desktop and wait for it to start (you'll see a whale icon in the system tray)
|
||||
|
||||
**Verify Docker is installed:**
|
||||
```bash
|
||||
docker --version
|
||||
docker-compose --version
|
||||
```
|
||||
|
||||
You should see version numbers for both commands.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Step 1: Navigate to Docker Compose Directory
|
||||
|
||||
```bash
|
||||
cd g:\LivingAi\farm-auth-service\db\farmmarket-db
|
||||
```
|
||||
|
||||
### Step 2: Start PostgreSQL Container
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
The `-d` flag runs the container in **detached mode** (in the background).
|
||||
|
||||
**What this does:**
|
||||
- Downloads PostgreSQL 16 image (if not already downloaded)
|
||||
- Creates a container named `farmmarket-postgres`
|
||||
- Starts PostgreSQL on port `5433` (host) → `5432` (container)
|
||||
- Automatically runs `init.sql` to create database schema
|
||||
- Creates a persistent volume to store data
|
||||
|
||||
### Step 3: Verify Container is Running
|
||||
|
||||
```bash
|
||||
docker ps
|
||||
```
|
||||
|
||||
You should see `farmmarket-postgres` in the list with status "Up".
|
||||
|
||||
### Step 4: Check Logs (Optional)
|
||||
|
||||
```bash
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
Press `Ctrl+C` to exit log view.
|
||||
|
||||
---
|
||||
|
||||
## Database Configuration
|
||||
|
||||
### Connection Details
|
||||
|
||||
From your `docker-compose.yml`:
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| **Host** | `localhost` |
|
||||
| **Port** | `5433` |
|
||||
| **Database** | `farmmarket` |
|
||||
| **Username** | `postgres` |
|
||||
| **Password** | `password123` |
|
||||
|
||||
### Connection String
|
||||
|
||||
Use this in your `.env` file:
|
||||
|
||||
```env
|
||||
DATABASE_URL=postgres://postgres:password123@localhost:5433/farmmarket
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Understanding docker-compose.yml
|
||||
|
||||
```yaml
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16 # PostgreSQL version 16
|
||||
container_name: farmmarket-postgres # Container name
|
||||
restart: always # Auto-restart if container stops
|
||||
environment:
|
||||
POSTGRES_USER: postgres # Database user
|
||||
POSTGRES_PASSWORD: password123 # Database password
|
||||
POSTGRES_DB: farmmarket # Database name
|
||||
ports:
|
||||
- "5433:5432" # Host:Container port mapping
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data # Persistent data storage
|
||||
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro # Auto-run SQL on first start
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- **Port 5433**: External port (what you connect to)
|
||||
- **Port 5432**: Internal container port (PostgreSQL default)
|
||||
- **Volumes**: Data persists even if container is removed
|
||||
- **init.sql**: Runs automatically on first container start
|
||||
|
||||
---
|
||||
|
||||
## Common Commands
|
||||
|
||||
### Start Database
|
||||
```bash
|
||||
cd g:\LivingAi\farm-auth-service\db\farmmarket-db
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Stop Database
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### Stop and Remove All Data (⚠️ WARNING: Deletes database)
|
||||
```bash
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
### Restart Database
|
||||
```bash
|
||||
docker-compose restart
|
||||
```
|
||||
|
||||
### View Container Status
|
||||
```bash
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
### View Logs
|
||||
```bash
|
||||
# All logs
|
||||
docker-compose logs
|
||||
|
||||
# Follow logs (live)
|
||||
docker-compose logs -f
|
||||
|
||||
# Last 100 lines
|
||||
docker-compose logs --tail=100
|
||||
```
|
||||
|
||||
### Access PostgreSQL CLI
|
||||
```bash
|
||||
docker exec -it farmmarket-postgres psql -U postgres -d farmmarket
|
||||
```
|
||||
|
||||
Once inside, you can run SQL commands:
|
||||
```sql
|
||||
\dt -- List all tables
|
||||
SELECT * FROM users; -- Query users table
|
||||
\q -- Exit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Problem: "Cannot connect to Docker daemon"
|
||||
|
||||
**Solution:**
|
||||
- Make sure Docker Desktop is running
|
||||
- Check the system tray for Docker icon
|
||||
- Restart Docker Desktop if needed
|
||||
|
||||
### Problem: "Port 5433 is already in use"
|
||||
|
||||
**Solution 1:** Stop the existing service using port 5433
|
||||
```bash
|
||||
# Find what's using the port (Windows)
|
||||
netstat -ano | findstr :5433
|
||||
|
||||
# Kill the process (replace PID with actual process ID)
|
||||
taskkill /PID <PID> /F
|
||||
```
|
||||
|
||||
**Solution 2:** Change the port in `docker-compose.yml`
|
||||
```yaml
|
||||
ports:
|
||||
- "5434:5432" # Change 5433 to 5434
|
||||
```
|
||||
|
||||
Then update your `.env`:
|
||||
```env
|
||||
DATABASE_URL=postgres://postgres:password123@localhost:5434/farmmarket
|
||||
```
|
||||
|
||||
### Problem: Container keeps stopping
|
||||
|
||||
**Check logs:**
|
||||
```bash
|
||||
docker-compose logs
|
||||
```
|
||||
|
||||
**Common causes:**
|
||||
- Port conflict
|
||||
- Insufficient disk space
|
||||
- Memory issues
|
||||
|
||||
**Restart container:**
|
||||
```bash
|
||||
docker-compose restart
|
||||
```
|
||||
|
||||
### Problem: Database schema not created
|
||||
|
||||
**Solution:**
|
||||
1. Stop and remove container:
|
||||
```bash
|
||||
docker-compose down -v
|
||||
```
|
||||
2. Start again (this will re-run init.sql):
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Problem: "Permission denied" on init.sql
|
||||
|
||||
**Solution (Windows):**
|
||||
- Make sure `init.sql` file exists in `db/farmmarket-db/` directory
|
||||
- Check file permissions (should be readable)
|
||||
|
||||
### Problem: Can't connect from application
|
||||
|
||||
**Check:**
|
||||
1. Container is running: `docker ps`
|
||||
2. Port is correct: `5433` (not `5432`)
|
||||
3. Connection string in `.env` matches:
|
||||
```env
|
||||
DATABASE_URL=postgres://postgres:password123@localhost:5433/farmmarket
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Change Database Password
|
||||
|
||||
1. Edit `docker-compose.yml`:
|
||||
```yaml
|
||||
POSTGRES_PASSWORD: your-new-password
|
||||
```
|
||||
|
||||
2. Update `.env`:
|
||||
```env
|
||||
DATABASE_URL=postgres://postgres:your-new-password@localhost:5433/farmmarket
|
||||
```
|
||||
|
||||
3. Recreate container:
|
||||
```bash
|
||||
docker-compose down -v
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Change Port
|
||||
|
||||
1. Edit `docker-compose.yml`:
|
||||
```yaml
|
||||
ports:
|
||||
- "5434:5432" # Change 5433 to your desired port
|
||||
```
|
||||
|
||||
2. Update `.env`:
|
||||
```env
|
||||
DATABASE_URL=postgres://postgres:password123@localhost:5434/farmmarket
|
||||
```
|
||||
|
||||
3. Restart:
|
||||
```bash
|
||||
docker-compose restart
|
||||
```
|
||||
|
||||
### Backup Database
|
||||
|
||||
```bash
|
||||
# Create backup
|
||||
docker exec farmmarket-postgres pg_dump -U postgres farmmarket > backup.sql
|
||||
|
||||
# Restore backup
|
||||
docker exec -i farmmarket-postgres psql -U postgres farmmarket < backup.sql
|
||||
```
|
||||
|
||||
### View Database Size
|
||||
|
||||
```bash
|
||||
docker exec farmmarket-postgres psql -U postgres -d farmmarket -c "SELECT pg_size_pretty(pg_database_size('farmmarket'));"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## First Time Setup Checklist
|
||||
|
||||
- [ ] Docker Desktop installed and running
|
||||
- [ ] Navigate to `db/farmmarket-db` directory
|
||||
- [ ] Run `docker-compose up -d`
|
||||
- [ ] Verify container is running: `docker ps`
|
||||
- [ ] Check logs for any errors: `docker-compose logs`
|
||||
- [ ] Update `.env` with correct `DATABASE_URL`
|
||||
- [ ] Test connection from your application
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables for Application
|
||||
|
||||
After Docker is running, add this to your `.env` file in the project root:
|
||||
|
||||
```env
|
||||
# Database (from Docker)
|
||||
DATABASE_URL=postgres://postgres:password123@localhost:5433/farmmarket
|
||||
|
||||
# JWT Secrets (generate these)
|
||||
JWT_ACCESS_SECRET=<generate-with-node-command>
|
||||
JWT_REFRESH_SECRET=<generate-with-node-command>
|
||||
```
|
||||
|
||||
**Generate JWT secrets:**
|
||||
```bash
|
||||
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||
```
|
||||
|
||||
Run this command twice to get two different secrets.
|
||||
|
||||
---
|
||||
|
||||
## Stopping Everything
|
||||
|
||||
When you're done working:
|
||||
|
||||
```bash
|
||||
# Stop database (keeps data)
|
||||
docker-compose down
|
||||
|
||||
# Stop and delete all data (fresh start)
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Need Help?
|
||||
|
||||
- Check Docker Desktop logs
|
||||
- View container logs: `docker-compose logs`
|
||||
- Verify container status: `docker ps`
|
||||
- Test connection: `docker exec -it farmmarket-postgres psql -U postgres -d farmmarket`
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
After Docker PostgreSQL is running:
|
||||
|
||||
1. ✅ Update `.env` with `DATABASE_URL`
|
||||
2. ✅ Generate JWT secrets and add to `.env`
|
||||
3. ✅ Start your auth service: `npm run dev`
|
||||
4. ✅ Test OTP request: `POST http://localhost:3000/auth/request-otp`
|
||||
|
||||
Your database is ready! 🎉
|
||||
|
||||
|
|
@ -1,428 +0,0 @@
|
|||
# Gemini Prompt: Implement JWT Authentication with Refresh Token Rotation
|
||||
|
||||
## Context
|
||||
I have a partially built Android Kotlin application that needs secure JWT authentication with rotating refresh tokens for persistent login. The authentication service is already built and running at `http://localhost:3000`. The app should keep users logged in using secure token storage and automatic token refresh.
|
||||
|
||||
---
|
||||
|
||||
## Authentication Service Details
|
||||
|
||||
**Base URL:** `http://localhost:3000` (development)
|
||||
|
||||
**Authentication Flow:**
|
||||
1. User requests OTP via `POST /auth/request-otp`
|
||||
2. User verifies OTP via `POST /auth/verify-otp` → receives `access_token` and `refresh_token`
|
||||
3. Access token (15 min expiry) is used for authenticated API calls
|
||||
4. Refresh token (7 days expiry) is used to get new tokens when access token expires
|
||||
5. Refresh tokens rotate on each use (must save new refresh_token)
|
||||
|
||||
**Key Endpoints:**
|
||||
|
||||
### 1. Request OTP
|
||||
```
|
||||
POST /auth/request-otp
|
||||
Body: { "phone_number": "+919876543210" }
|
||||
Response: { "ok": true }
|
||||
```
|
||||
|
||||
### 2. Verify OTP (Login)
|
||||
```
|
||||
POST /auth/verify-otp
|
||||
Body: {
|
||||
"phone_number": "+919876543210",
|
||||
"code": "123456",
|
||||
"device_id": "android-installation-id",
|
||||
"device_info": {
|
||||
"platform": "android",
|
||||
"model": "Samsung SM-M326B",
|
||||
"os_version": "Android 14",
|
||||
"app_version": "1.0.0",
|
||||
"language_code": "en-IN",
|
||||
"timezone": "Asia/Kolkata"
|
||||
}
|
||||
}
|
||||
Response: {
|
||||
"user": { "id": "...", "phone_number": "...", "name": null, ... },
|
||||
"access_token": "eyJhbGc...",
|
||||
"refresh_token": "eyJhbGc...",
|
||||
"needs_profile": true,
|
||||
"is_new_device": true,
|
||||
"is_new_account": false
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Refresh Token (Get New Tokens)
|
||||
```
|
||||
POST /auth/refresh
|
||||
Body: { "refresh_token": "eyJhbGc..." }
|
||||
Response: {
|
||||
"access_token": "eyJhbGc...",
|
||||
"refresh_token": "eyJhbGc..." // NEW token - MUST save this
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Get User Details (Authenticated)
|
||||
```
|
||||
GET /users/me
|
||||
Headers: Authorization: Bearer <access_token>
|
||||
Response: {
|
||||
"id": "...",
|
||||
"phone_number": "+919876543210",
|
||||
"name": "John Doe",
|
||||
"user_type": "seller",
|
||||
"last_login_at": "2024-01-20T14:22:00Z",
|
||||
"location": { ... },
|
||||
"locations": [ ... ],
|
||||
"active_devices_count": 2
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Logout
|
||||
```
|
||||
POST /auth/logout
|
||||
Body: { "refresh_token": "eyJhbGc..." }
|
||||
Response: { "ok": true }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Requirements
|
||||
|
||||
### 1. Secure Token Storage
|
||||
|
||||
**Use EncryptedSharedPreferences (Android Security Library):**
|
||||
- Store `access_token` and `refresh_token` securely
|
||||
- Never use plain SharedPreferences
|
||||
- Never log tokens in console/logs
|
||||
- Clear tokens on logout or app uninstall
|
||||
|
||||
**Implementation:**
|
||||
```kotlin
|
||||
// Use androidx.security:security-crypto:1.1.0-alpha06
|
||||
// Store tokens using EncryptedSharedPreferences with MasterKey
|
||||
```
|
||||
|
||||
### 2. Token Management
|
||||
|
||||
**Access Token:**
|
||||
- Lifetime: 15 minutes
|
||||
- Used in `Authorization: Bearer <token>` header for all authenticated requests
|
||||
- Automatically refreshed when expired (401 response)
|
||||
|
||||
**Refresh Token:**
|
||||
- Lifetime: 7 days (configurable)
|
||||
- Idle timeout: 3 days (if unused for 3 days, becomes invalid)
|
||||
- **ROTATES on each refresh** - always save the new refresh_token
|
||||
- Stored securely, never sent in URLs or logs
|
||||
|
||||
**Token Refresh Flow:**
|
||||
```
|
||||
1. API call returns 401 (Unauthorized)
|
||||
2. Get refresh_token from secure storage
|
||||
3. Call POST /auth/refresh with refresh_token
|
||||
4. Receive new access_token and new refresh_token
|
||||
5. Save BOTH new tokens securely
|
||||
6. Retry original API call with new access_token
|
||||
7. If refresh fails → clear tokens → redirect to login
|
||||
```
|
||||
|
||||
### 3. Auto-Refresh Interceptor/Plugin
|
||||
|
||||
**Implement automatic token refresh:**
|
||||
- Intercept all API requests
|
||||
- Add `Authorization: Bearer <access_token>` header automatically
|
||||
- On 401 response:
|
||||
- Refresh token automatically
|
||||
- Retry original request
|
||||
- If refresh fails, clear tokens and redirect to login
|
||||
|
||||
**Use either:**
|
||||
- Ktor: HTTP client interceptor/plugin
|
||||
- Retrofit: OkHttp interceptor
|
||||
|
||||
### 4. Success Page Implementation
|
||||
|
||||
**After successful login (OTP verification), show a Success/Home screen that:**
|
||||
|
||||
1. **Fetches User Details:**
|
||||
- Call `GET /users/me` with access_token
|
||||
- Display: name, phone_number, user_type, last_login_at
|
||||
- Display location if available (city, state, pincode)
|
||||
- Handle loading and error states
|
||||
|
||||
2. **Persistent Login:**
|
||||
- Check for stored tokens on app launch
|
||||
- If tokens exist and valid → auto-login user
|
||||
- If tokens expired → attempt refresh
|
||||
- If refresh fails → show login screen
|
||||
|
||||
3. **Logout Button:**
|
||||
- Clear all tokens from secure storage
|
||||
- Call `POST /auth/logout` with refresh_token
|
||||
- Navigate back to login screen
|
||||
- Show confirmation dialog before logout
|
||||
|
||||
### 5. Security Requirements
|
||||
|
||||
**Critical Security Rules:**
|
||||
1. ✅ Store tokens only in `EncryptedSharedPreferences`
|
||||
2. ✅ Never log tokens or sensitive data
|
||||
3. ✅ Always use HTTPS in production (http://localhost only for dev)
|
||||
4. ✅ Implement certificate pinning for production
|
||||
5. ✅ Handle token reuse detection (if refresh returns 401, force re-login)
|
||||
6. ✅ Clear tokens on logout, app uninstall, or security breach
|
||||
7. ✅ Validate device_id consistently (use Android ID or Installation ID)
|
||||
8. ✅ Handle network errors gracefully without exposing tokens
|
||||
|
||||
### 6. Error Handling
|
||||
|
||||
**Handle these scenarios:**
|
||||
- **401 Unauthorized** → Refresh token → Retry
|
||||
- **401 on refresh** → Clear tokens → Redirect to login (session expired)
|
||||
- **Network errors** → Show user-friendly message, retry option
|
||||
- **Invalid OTP** → Show error, allow retry
|
||||
- **Token expired** → Auto-refresh silently (user shouldn't notice)
|
||||
- **Refresh token expired** → Show "Session expired, please login again"
|
||||
|
||||
### 7. Device Information
|
||||
|
||||
**Send device info during login:**
|
||||
- Use Android ID or Firebase Installation ID for `device_id`
|
||||
- Collect: platform, model, OS version, app version, language, timezone
|
||||
- Send in `device_info` object during OTP verification
|
||||
|
||||
---
|
||||
|
||||
## Code Structure Requirements
|
||||
|
||||
### Data Models (Kotlinx Serialization)
|
||||
```kotlin
|
||||
@Serializable
|
||||
data class User(...)
|
||||
@Serializable
|
||||
data class VerifyOtpResponse(...)
|
||||
@Serializable
|
||||
data class RefreshResponse(...)
|
||||
// Include all necessary models with @SerialName for snake_case JSON
|
||||
```
|
||||
|
||||
### Token Manager
|
||||
```kotlin
|
||||
class TokenManager(context: Context) {
|
||||
fun saveTokens(accessToken: String, refreshToken: String)
|
||||
fun getAccessToken(): String?
|
||||
fun getRefreshToken(): String?
|
||||
fun clearTokens()
|
||||
}
|
||||
```
|
||||
|
||||
### API Client with Auto-Refresh
|
||||
```kotlin
|
||||
class AuthApiClient {
|
||||
// Automatically adds Authorization header
|
||||
// Automatically refreshes token on 401
|
||||
// Handles token rotation
|
||||
}
|
||||
```
|
||||
|
||||
### ViewModel Pattern
|
||||
```kotlin
|
||||
class SuccessViewModel : ViewModel() {
|
||||
// Fetch user details
|
||||
// Handle logout
|
||||
// Observe authentication state
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## User Experience Flow
|
||||
|
||||
1. **App Launch:**
|
||||
- Check for stored tokens
|
||||
- If valid → navigate to Success/Home screen
|
||||
- If invalid/expired → show Login screen
|
||||
|
||||
2. **Login Screen:**
|
||||
- Enter phone number → Request OTP
|
||||
- Enter OTP code → Verify OTP
|
||||
- On success → Save tokens → Navigate to Success screen
|
||||
|
||||
3. **Success/Home Screen:**
|
||||
- Show loading indicator
|
||||
- Fetch user details from `/users/me`
|
||||
- Display: Name, Phone, Profile Type, Location, Last Login
|
||||
- Show logout button
|
||||
- Handle token refresh silently if needed
|
||||
|
||||
4. **Logout:**
|
||||
- Show confirmation dialog
|
||||
- Call logout API
|
||||
- Clear tokens
|
||||
- Navigate to Login screen
|
||||
|
||||
---
|
||||
|
||||
## Dependencies Required
|
||||
|
||||
```kotlin
|
||||
// HTTP Client
|
||||
implementation("io.ktor:ktor-client-android:2.3.5")
|
||||
implementation("io.ktor:ktor-client-content-negotiation:2.3.5")
|
||||
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.5")
|
||||
|
||||
// Secure Storage
|
||||
implementation("androidx.security:security-crypto:1.1.0-alpha06")
|
||||
|
||||
// Serialization
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
|
||||
|
||||
// Coroutines
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
|
||||
|
||||
// ViewModel
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Login with OTP and receive tokens
|
||||
- [ ] Tokens stored securely in EncryptedSharedPreferences
|
||||
- [ ] Success page displays user details from `/users/me`
|
||||
- [ ] Access token auto-refreshes when expired (wait 15+ min)
|
||||
- [ ] Refresh token rotates correctly (save new token)
|
||||
- [ ] Logout clears tokens and navigates to login
|
||||
- [ ] App remembers login after restart (if tokens valid)
|
||||
- [ ] Session expires gracefully after 3 days idle
|
||||
- [ ] Network errors handled gracefully
|
||||
- [ ] No tokens logged in console/logs
|
||||
|
||||
---
|
||||
|
||||
## Specific Implementation Notes
|
||||
|
||||
1. **Phone Number Format:**
|
||||
- Accept user input (can be 10 digits or with +91)
|
||||
- Server auto-normalizes: `9876543210` → `+919876543210`
|
||||
- Always send in E.164 format
|
||||
|
||||
2. **Device ID:**
|
||||
- Use consistent identifier: Android ID or Firebase Installation ID
|
||||
- Must be 4-128 alphanumeric characters
|
||||
- Server sanitizes invalid IDs
|
||||
|
||||
3. **Token Rotation:**
|
||||
- **CRITICAL:** After refresh, the new `refresh_token` replaces the old one
|
||||
- Old refresh_token becomes invalid immediately
|
||||
- Always save the new refresh_token from refresh response
|
||||
|
||||
4. **Base URL Configuration:**
|
||||
- Development: `http://localhost:3000`
|
||||
- Production: Use environment-based configuration
|
||||
- Consider using BuildConfig for different environments
|
||||
|
||||
5. **API Response Handling:**
|
||||
- All error responses: `{ "error": "message" }`
|
||||
- Success responses vary by endpoint
|
||||
- Always check HTTP status code before parsing JSON
|
||||
|
||||
---
|
||||
|
||||
## Security Validation Checklist
|
||||
|
||||
Before considering the implementation complete, verify:
|
||||
|
||||
- ✅ No tokens in logs/console
|
||||
- ✅ Tokens only in EncryptedSharedPreferences
|
||||
- ✅ Automatic token refresh works
|
||||
- ✅ Token rotation handled correctly
|
||||
- ✅ Tokens cleared on logout
|
||||
- ✅ Session expiry handled
|
||||
- ✅ Network errors don't expose tokens
|
||||
- ✅ HTTPS for production (when deployed)
|
||||
|
||||
---
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
**On Successful Login:**
|
||||
1. Save `access_token` and `refresh_token` securely
|
||||
2. Navigate to Success/Home screen
|
||||
3. Automatically fetch user details from `/users/me`
|
||||
4. Display user information
|
||||
5. Show logout button
|
||||
|
||||
**On App Restart (with valid tokens):**
|
||||
1. Check stored tokens
|
||||
2. Validate token (or refresh if expired)
|
||||
3. Auto-navigate to Success/Home screen
|
||||
4. Fetch and display user details
|
||||
|
||||
**On Token Expiration:**
|
||||
1. Next API call returns 401
|
||||
2. Automatically refresh token (silently)
|
||||
3. Retry API call with new token
|
||||
4. User experience uninterrupted
|
||||
|
||||
**On Refresh Token Expiration:**
|
||||
1. Refresh attempt fails with 401
|
||||
2. Clear all tokens
|
||||
3. Show "Session expired" message
|
||||
4. Navigate to login screen
|
||||
|
||||
**On Logout:**
|
||||
1. Show confirmation dialog
|
||||
2. Call logout API
|
||||
3. Clear all tokens from storage
|
||||
4. Navigate to login screen
|
||||
|
||||
---
|
||||
|
||||
## Reference Documentation
|
||||
|
||||
The complete API documentation and data models are available in:
|
||||
- `how_to_use_Auth.md` (in your Android project)
|
||||
- Full API reference with request/response examples
|
||||
- All data models with Kotlinx Serialization annotations
|
||||
|
||||
---
|
||||
|
||||
## Deliverables
|
||||
|
||||
Please implement:
|
||||
|
||||
1. **TokenManager** - Secure token storage using EncryptedSharedPreferences
|
||||
2. **AuthApiClient** - API client with automatic token refresh and rotation
|
||||
3. **Success/Home Screen** - Displays user details from `/users/me`
|
||||
4. **Logout functionality** - With confirmation and proper cleanup
|
||||
5. **Auto-login on app launch** - Check tokens and auto-login if valid
|
||||
6. **Error handling** - Graceful handling of all error scenarios
|
||||
7. **Loading states** - Show loading indicators during API calls
|
||||
|
||||
**Code Quality:**
|
||||
- Use Kotlin best practices
|
||||
- Follow MVVM architecture pattern
|
||||
- Use Kotlin Coroutines for async operations
|
||||
- Proper error handling with Result type
|
||||
- Clean, maintainable, and secure code
|
||||
|
||||
**Security:**
|
||||
- All tokens encrypted at rest
|
||||
- No tokens in logs
|
||||
- Proper token rotation
|
||||
- Secure network communication
|
||||
|
||||
---
|
||||
|
||||
This implementation should provide a secure, production-ready authentication system with persistent login capability. The user should be able to login once and remain logged in (until tokens expire or logout) with seamless token refresh happening automatically in the background.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
# Gemini Prompt: JWT Auth with Refresh Token Rotation - Copy This to Gemini
|
||||
|
||||
---
|
||||
|
||||
I need you to implement secure JWT authentication with rotating refresh tokens in my Android Kotlin app for persistent login. The auth service runs at `http://localhost:3000`.
|
||||
|
||||
## API Endpoints
|
||||
|
||||
**Base URL:** `http://localhost:3000`
|
||||
|
||||
1. **Request OTP:** `POST /auth/request-otp` → Body: `{ "phone_number": "+919876543210" }`
|
||||
2. **Verify OTP:** `POST /auth/verify-otp` → Returns: `{ "access_token", "refresh_token", "user", ... }`
|
||||
3. **Refresh Token:** `POST /auth/refresh` → Body: `{ "refresh_token": "..." }` → Returns new access_token AND new refresh_token (ROTATES)
|
||||
4. **Get User:** `GET /users/me` → Header: `Authorization: Bearer <access_token>` → Returns user details with location
|
||||
5. **Logout:** `POST /auth/logout` → Body: `{ "refresh_token": "..." }`
|
||||
|
||||
## Critical Requirements
|
||||
|
||||
**Token Storage (SECURITY):**
|
||||
- ✅ Use `EncryptedSharedPreferences` (androidx.security:security-crypto)
|
||||
- ❌ NEVER use plain SharedPreferences
|
||||
- ❌ NEVER log tokens in console/logs
|
||||
- ✅ Clear tokens on logout
|
||||
|
||||
**Token Management:**
|
||||
- Access token: 15 min lifetime, used in `Authorization: Bearer <token>` header
|
||||
- Refresh token: 7 days lifetime, rotates on each refresh (SAVE NEW TOKEN)
|
||||
- Auto-refresh on 401: Get new tokens, retry request, if refresh fails → logout
|
||||
|
||||
**Success/Home Screen:**
|
||||
- After login → Navigate to Success screen
|
||||
- Fetch user details from `GET /users/me` with access_token
|
||||
- Display: name, phone_number, user_type, last_login_at, location
|
||||
- Show logout button with confirmation
|
||||
- Handle loading/error states
|
||||
|
||||
**Persistent Login:**
|
||||
- On app launch: Check stored tokens → If valid, auto-login to Success screen
|
||||
- If tokens expired: Try refresh → If fails, show login screen
|
||||
- User should stay logged in until logout or 7 days of inactivity
|
||||
|
||||
## Implementation Tasks
|
||||
|
||||
1. **TokenManager** - Secure storage using EncryptedSharedPreferences
|
||||
2. **AuthApiClient** - With auto-refresh interceptor (handles 401, refreshes, retries)
|
||||
3. **Success/Home Activity/Fragment** - Displays user details from `/users/me`
|
||||
4. **Logout** - Calls logout API, clears tokens, navigates to login
|
||||
5. **Auto-login** - Check tokens on app launch
|
||||
|
||||
## Code Requirements
|
||||
|
||||
- Use Kotlinx Serialization for JSON
|
||||
- Use Ktor or Retrofit for HTTP client
|
||||
- Use MVVM architecture
|
||||
- Use Kotlin Coroutines
|
||||
- Handle all errors gracefully
|
||||
- Show loading indicators
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- ✅ Tokens only in EncryptedSharedPreferences
|
||||
- ✅ Auto-refresh on token expiration
|
||||
- ✅ Token rotation handled (save new refresh_token)
|
||||
- ✅ No tokens in logs
|
||||
- ✅ Clear tokens on logout
|
||||
|
||||
## Dependencies
|
||||
|
||||
```kotlin
|
||||
implementation("io.ktor:ktor-client-android:2.3.5")
|
||||
implementation("io.ktor:ktor-client-content-negotiation:2.3.5")
|
||||
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.5")
|
||||
implementation("androidx.security:security-crypto:1.1.0-alpha06")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
|
||||
```
|
||||
|
||||
**IMPORTANT:** Refresh tokens ROTATE - always save the new refresh_token from refresh response. Reference: See `how_to_use_Auth.md` in the project for complete API documentation.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,649 +0,0 @@
|
|||
# How to Use `/users/me` Endpoint in Kotlin Application
|
||||
|
||||
Complete guide for integrating the authenticated `/users/me` endpoint in your Kotlin/Android application.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
**Endpoint:** `GET http://localhost:3000/users/me` (or your production URL)
|
||||
|
||||
**Authentication Required:** Yes (JWT Bearer Token)
|
||||
|
||||
**What it returns:**
|
||||
- User details (phone, name, profile type)
|
||||
- Last login time
|
||||
- Location information (primary + all saved locations)
|
||||
- Active devices count
|
||||
|
||||
---
|
||||
|
||||
## Complete Flow
|
||||
|
||||
### Step 1: User Login (Get Tokens)
|
||||
```
|
||||
POST /auth/verify-otp
|
||||
→ Returns: { access_token, refresh_token, user, ... }
|
||||
```
|
||||
|
||||
### Step 2: Store Tokens Securely
|
||||
```
|
||||
Save access_token and refresh_token in EncryptedSharedPreferences
|
||||
```
|
||||
|
||||
### Step 3: Make Authenticated Request
|
||||
```
|
||||
GET /users/me
|
||||
Header: Authorization: Bearer <access_token>
|
||||
→ Returns: { id, phone_number, name, location, ... }
|
||||
```
|
||||
|
||||
### Step 4: Handle Token Expiration
|
||||
```
|
||||
If 401 error → Use refresh_token to get new tokens
|
||||
If refresh fails → Redirect to login
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Kotlin Implementation
|
||||
|
||||
### 1. Add Dependencies (build.gradle.kts)
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
// Ktor for HTTP requests
|
||||
implementation("io.ktor:ktor-client-android:2.3.5")
|
||||
implementation("io.ktor:ktor-client-content-negotiation:2.3.5")
|
||||
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.5")
|
||||
|
||||
// Secure storage
|
||||
implementation("androidx.security:security-crypto:1.1.0-alpha06")
|
||||
|
||||
// Coroutines
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
|
||||
|
||||
// Serialization
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Data Models
|
||||
|
||||
```kotlin
|
||||
// models/User.kt
|
||||
package com.farm.auth.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Coordinates(
|
||||
val latitude: Double,
|
||||
val longitude: Double
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Location(
|
||||
val id: String,
|
||||
val country: String?,
|
||||
val state: String?,
|
||||
val district: String?,
|
||||
@SerialName("city_village") val cityVillage: String?,
|
||||
val pincode: String?,
|
||||
val coordinates: Coordinates?,
|
||||
@SerialName("location_type") val locationType: String?,
|
||||
@SerialName("is_saved_address") val isSavedAddress: Boolean,
|
||||
@SerialName("source_type") val sourceType: String?,
|
||||
@SerialName("source_confidence") val sourceConfidence: String?,
|
||||
@SerialName("created_at") val createdAt: String,
|
||||
@SerialName("updated_at") val updatedAt: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class User(
|
||||
val id: String,
|
||||
@SerialName("phone_number") val phoneNumber: String,
|
||||
val name: String?,
|
||||
val role: String,
|
||||
@SerialName("user_type") val userType: String?,
|
||||
@SerialName("avatar_url") val avatarUrl: String? = null,
|
||||
val language: String? = null,
|
||||
val timezone: String? = null,
|
||||
@SerialName("created_at") val createdAt: String? = null,
|
||||
@SerialName("last_login_at") val lastLoginAt: String? = null,
|
||||
@SerialName("active_devices_count") val activeDevicesCount: Int? = null,
|
||||
val location: Location? = null,
|
||||
val locations: List<Location> = emptyList()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ErrorResponse(val error: String)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Secure Token Storage
|
||||
|
||||
```kotlin
|
||||
// storage/TokenManager.kt
|
||||
package com.farm.auth.storage
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
|
||||
class TokenManager(private val context: Context) {
|
||||
|
||||
private val masterKey = MasterKey.Builder(context)
|
||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||
.build()
|
||||
|
||||
private val prefs: SharedPreferences = EncryptedSharedPreferences.create(
|
||||
context,
|
||||
"auth_tokens",
|
||||
masterKey,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
)
|
||||
|
||||
fun saveTokens(accessToken: String, refreshToken: String) {
|
||||
prefs.edit().apply {
|
||||
putString("access_token", accessToken)
|
||||
putString("refresh_token", refreshToken)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
fun getAccessToken(): String? = prefs.getString("access_token", null)
|
||||
|
||||
fun getRefreshToken(): String? = prefs.getString("refresh_token", null)
|
||||
|
||||
fun clearTokens() {
|
||||
prefs.edit().clear().apply()
|
||||
}
|
||||
|
||||
fun hasTokens(): Boolean {
|
||||
return getAccessToken() != null && getRefreshToken() != null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. API Client with Auto-Refresh
|
||||
|
||||
```kotlin
|
||||
// network/AuthApiClient.kt
|
||||
package com.farm.auth.network
|
||||
|
||||
import com.farm.auth.models.*
|
||||
import com.farm.auth.storage.TokenManager
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.engine.android.Android
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.client.plugins.defaultrequest.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class AuthApiClient(
|
||||
private val baseUrl: String,
|
||||
private val tokenManager: TokenManager
|
||||
) {
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
isLenient = true
|
||||
encodeDefaults = false
|
||||
}
|
||||
|
||||
private val client = HttpClient(Android) {
|
||||
install(ContentNegotiation) {
|
||||
json(json)
|
||||
}
|
||||
install(DefaultRequest) {
|
||||
url(baseUrl)
|
||||
contentType(ContentType.Application.Json)
|
||||
}
|
||||
// Add auth token to all requests automatically
|
||||
engine {
|
||||
addInterceptor { request ->
|
||||
val token = tokenManager.getAccessToken()
|
||||
if (token != null && !request.url.encodedPath.contains("/auth/")) {
|
||||
request.headers.append("Authorization", "Bearer $token")
|
||||
}
|
||||
request
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val _currentUser = MutableStateFlow<User?>(null)
|
||||
val currentUser: StateFlow<User?> = _currentUser.asStateFlow()
|
||||
|
||||
/**
|
||||
* Refresh access token using refresh token
|
||||
*/
|
||||
suspend fun refreshTokens(): Result<Pair<String, String>> {
|
||||
val refreshToken = tokenManager.getRefreshToken()
|
||||
?: return Result.failure(Exception("No refresh token available"))
|
||||
|
||||
return try {
|
||||
val response = client.post("/auth/refresh") {
|
||||
setBody(mapOf("refresh_token" to refreshToken))
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
val data: RefreshResponse = response.body()
|
||||
tokenManager.saveTokens(data.accessToken, data.refreshToken)
|
||||
Result.success(data.accessToken to data.refreshToken)
|
||||
} else {
|
||||
val error: ErrorResponse = response.body()
|
||||
Result.failure(Exception(error.error))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user details from /users/me endpoint
|
||||
* Automatically handles token refresh on 401
|
||||
*/
|
||||
suspend fun getCurrentUser(): Result<User> {
|
||||
return try {
|
||||
val response = client.get("/users/me")
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
val user: User = response.body()
|
||||
_currentUser.value = user
|
||||
Result.success(user)
|
||||
} else if (response.status == HttpStatusCode.Unauthorized) {
|
||||
// Token expired, try to refresh
|
||||
refreshTokens().fold(
|
||||
onSuccess = { (newAccessToken, _) ->
|
||||
// Retry with new token
|
||||
val retryResponse = client.get("/users/me") {
|
||||
header("Authorization", "Bearer $newAccessToken")
|
||||
}
|
||||
if (retryResponse.status.isSuccess()) {
|
||||
val user: User = retryResponse.body()
|
||||
_currentUser.value = user
|
||||
Result.success(user)
|
||||
} else {
|
||||
// Refresh worked but retry failed - force re-login
|
||||
tokenManager.clearTokens()
|
||||
_currentUser.value = null
|
||||
Result.failure(Exception("Authentication failed. Please login again."))
|
||||
}
|
||||
},
|
||||
onFailure = { error ->
|
||||
// Refresh failed - force re-login
|
||||
tokenManager.clearTokens()
|
||||
_currentUser.value = null
|
||||
Result.failure(Exception("Session expired. Please login again."))
|
||||
}
|
||||
)
|
||||
} else {
|
||||
val error: ErrorResponse = response.body()
|
||||
Result.failure(Exception(error.error))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class RefreshResponse(
|
||||
@SerialName("access_token") val accessToken: String,
|
||||
@SerialName("refresh_token") val refreshToken: String
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Repository Pattern (Optional but Recommended)
|
||||
|
||||
```kotlin
|
||||
// repository/UserRepository.kt
|
||||
package com.farm.auth.repository
|
||||
|
||||
import com.farm.auth.models.User
|
||||
import com.farm.auth.network.AuthApiClient
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
class UserRepository(private val apiClient: AuthApiClient) {
|
||||
|
||||
val currentUser: StateFlow<User?> = apiClient.currentUser
|
||||
|
||||
suspend fun getUserDetails(): Result<User> {
|
||||
return apiClient.getCurrentUser()
|
||||
}
|
||||
|
||||
suspend fun refreshUserData(): Result<User> {
|
||||
return apiClient.getCurrentUser()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. ViewModel Usage
|
||||
|
||||
```kotlin
|
||||
// ui/UserProfileViewModel.kt
|
||||
package com.farm.auth.ui
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.farm.auth.models.User
|
||||
import com.farm.auth.repository.UserRepository
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class UserProfileViewModel(
|
||||
private val userRepository: UserRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow<UserProfileUiState>(UserProfileUiState.Loading)
|
||||
val uiState: StateFlow<UserProfileUiState> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
loadUserProfile()
|
||||
}
|
||||
|
||||
fun loadUserProfile() {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = UserProfileUiState.Loading
|
||||
|
||||
userRepository.getUserDetails()
|
||||
.fold(
|
||||
onSuccess = { user ->
|
||||
_uiState.value = UserProfileUiState.Success(user)
|
||||
},
|
||||
onFailure = { error ->
|
||||
_uiState.value = UserProfileUiState.Error(error.message ?: "Failed to load profile")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshProfile() {
|
||||
loadUserProfile()
|
||||
}
|
||||
}
|
||||
|
||||
sealed class UserProfileUiState {
|
||||
object Loading : UserProfileUiState()
|
||||
data class Success(val user: User) : UserProfileUiState()
|
||||
data class Error(val message: String) : UserProfileUiState()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. Activity/Fragment Usage
|
||||
|
||||
```kotlin
|
||||
// ui/UserProfileActivity.kt
|
||||
package com.farm.auth.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.farm.auth.network.AuthApiClient
|
||||
import com.farm.auth.repository.UserRepository
|
||||
import com.farm.auth.storage.TokenManager
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class UserProfileActivity : AppCompatActivity() {
|
||||
|
||||
private val viewModel: UserProfileViewModel by viewModels {
|
||||
val tokenManager = TokenManager(this)
|
||||
val apiClient = AuthApiClient("http://localhost:3000", tokenManager)
|
||||
val repository = UserRepository(apiClient)
|
||||
UserProfileViewModelFactory(repository)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_user_profile)
|
||||
|
||||
// Observe UI state
|
||||
lifecycleScope.launch {
|
||||
viewModel.uiState.collect { state ->
|
||||
when (state) {
|
||||
is UserProfileUiState.Loading -> {
|
||||
// Show loading indicator
|
||||
showLoading()
|
||||
}
|
||||
is UserProfileUiState.Success -> {
|
||||
// Display user data
|
||||
displayUserData(state.user)
|
||||
hideLoading()
|
||||
}
|
||||
is UserProfileUiState.Error -> {
|
||||
// Show error message
|
||||
showError(state.message)
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun displayUserData(user: User) {
|
||||
// Update UI with user data
|
||||
findViewById<TextView>(R.id.tvName).text = user.name ?: "Not set"
|
||||
findViewById<TextView>(R.id.tvPhone).text = user.phoneNumber
|
||||
findViewById<TextView>(R.id.tvProfileType).text = user.userType ?: "Not set"
|
||||
findViewById<TextView>(R.id.tvLastLogin).text = user.lastLoginAt ?: "Never"
|
||||
|
||||
// Display location
|
||||
user.location?.let { location ->
|
||||
findViewById<TextView>(R.id.tvLocation).text = buildString {
|
||||
append(location.cityVillage ?: "")
|
||||
if (location.district != null) append(", ${location.district}")
|
||||
if (location.state != null) append(", ${location.state}")
|
||||
if (location.pincode != null) append(" - ${location.pincode}")
|
||||
}
|
||||
} ?: run {
|
||||
findViewById<TextView>(R.id.tvLocation).text = "No location saved"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Simple Example (Without Repository)
|
||||
|
||||
If you want a simpler approach without repository pattern:
|
||||
|
||||
```kotlin
|
||||
// Simple usage in Activity
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var tokenManager: TokenManager
|
||||
private lateinit var apiClient: AuthApiClient
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
tokenManager = TokenManager(this)
|
||||
apiClient = AuthApiClient("http://localhost:3000", tokenManager)
|
||||
|
||||
// Load user profile
|
||||
loadUserProfile()
|
||||
}
|
||||
|
||||
private fun loadUserProfile() {
|
||||
lifecycleScope.launch {
|
||||
apiClient.getCurrentUser()
|
||||
.onSuccess { user ->
|
||||
// Use user data
|
||||
Log.d("User", "Name: ${user.name}")
|
||||
Log.d("User", "Phone: ${user.phoneNumber}")
|
||||
Log.d("User", "Location: ${user.location?.cityVillage}")
|
||||
}
|
||||
.onFailure { error ->
|
||||
Log.e("Error", "Failed: ${error.message}")
|
||||
// Handle error - maybe redirect to login
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Flow Example
|
||||
|
||||
```kotlin
|
||||
// Complete authentication and profile loading flow
|
||||
class AuthFlow {
|
||||
|
||||
suspend fun loginAndLoadProfile(
|
||||
phoneNumber: String,
|
||||
otpCode: String
|
||||
): Result<User> {
|
||||
// 1. Login and get tokens
|
||||
val loginResult = login(phoneNumber, otpCode)
|
||||
|
||||
return loginResult.fold(
|
||||
onSuccess = { tokens ->
|
||||
// 2. Save tokens
|
||||
tokenManager.saveTokens(tokens.accessToken, tokens.refreshToken)
|
||||
|
||||
// 3. Get user profile
|
||||
apiClient.getCurrentUser()
|
||||
},
|
||||
onFailure = { error ->
|
||||
Result.failure(error)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Points
|
||||
|
||||
### 1. **Authentication Header Format**
|
||||
```
|
||||
Authorization: Bearer <access_token>
|
||||
```
|
||||
|
||||
### 2. **Token Storage**
|
||||
- ✅ Use `EncryptedSharedPreferences` (secure)
|
||||
- ❌ Don't use plain `SharedPreferences`
|
||||
- ❌ Don't log tokens in console/logs
|
||||
|
||||
### 3. **Token Refresh Flow**
|
||||
```
|
||||
401 Error → Refresh Token → Retry Request
|
||||
If refresh fails → Clear tokens → Redirect to login
|
||||
```
|
||||
|
||||
### 4. **Base URL Configuration**
|
||||
```kotlin
|
||||
// Development
|
||||
val baseUrl = "http://localhost:3000"
|
||||
|
||||
// Production (change based on your deployment)
|
||||
val baseUrl = "https://your-domain.com"
|
||||
```
|
||||
|
||||
### 5. **Error Handling**
|
||||
- **401 Unauthorized**: Token expired → Try refresh
|
||||
- **404 Not Found**: User doesn't exist
|
||||
- **500 Server Error**: Server issue
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Login successfully and get tokens
|
||||
- [ ] Store tokens securely
|
||||
- [ ] Fetch user profile with valid token
|
||||
- [ ] Handle token expiration (wait 15+ min, then retry)
|
||||
- [ ] Handle refresh token rotation
|
||||
- [ ] Handle refresh token expiration (force re-login)
|
||||
- [ ] Handle network errors gracefully
|
||||
- [ ] Display user data correctly
|
||||
- [ ] Display location data if available
|
||||
|
||||
---
|
||||
|
||||
## Production Considerations
|
||||
|
||||
1. **Base URL**: Use environment-based configuration
|
||||
```kotlin
|
||||
val baseUrl = if (BuildConfig.DEBUG)
|
||||
"http://localhost:3000"
|
||||
else
|
||||
"https://api.yourdomain.com"
|
||||
```
|
||||
|
||||
2. **Certificate Pinning**: For production, implement SSL pinning
|
||||
|
||||
3. **Error Logging**: Log errors to crash reporting (Firebase Crashlytics, etc.)
|
||||
|
||||
4. **Network Timeout**: Set appropriate timeouts for network requests
|
||||
|
||||
5. **Token Refresh Strategy**: Consider proactive refresh (refresh before expiration)
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
**Endpoint:** `GET /users/me`
|
||||
|
||||
**Headers:**
|
||||
```http
|
||||
Authorization: Bearer <access_token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Success Response (200):**
|
||||
```json
|
||||
{
|
||||
"id": "...",
|
||||
"phone_number": "+919876543210",
|
||||
"name": "John Doe",
|
||||
"user_type": "seller",
|
||||
"last_login_at": "2024-01-20T14:22:00Z",
|
||||
"location": { ... },
|
||||
"locations": [ ... ]
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
- **401**: Token expired/invalid → Refresh token
|
||||
- **404**: User not found
|
||||
- **500**: Server error
|
||||
|
||||
---
|
||||
|
||||
This guide provides everything you need to integrate the `/users/me` endpoint in your Kotlin application!
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
# Logout from All Devices - Implementation Summary
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the implementation of the "Logout from All Devices" feature, which allows users to immediately invalidate all access and refresh tokens across all devices when a security breach or account compromise is suspected.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. Database Schema Changes
|
||||
|
||||
**Added Column to `users` table:**
|
||||
- `token_version` (INT, NOT NULL, DEFAULT 1)
|
||||
- Incremented on logout-all-devices to invalidate all existing access tokens
|
||||
- Validated on each access token verification
|
||||
|
||||
**Migration Required:**
|
||||
For existing databases, run:
|
||||
```sql
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS token_version INT NOT NULL DEFAULT 1;
|
||||
```
|
||||
|
||||
### 2. Token Service Updates
|
||||
|
||||
**File: `src/services/tokenService.js`**
|
||||
|
||||
- **Updated `signAccessToken()`**: Now includes `token_version` claim in JWT payload
|
||||
- **Added `revokeAllUserTokens(userId)`**:
|
||||
- Revokes all refresh tokens for the user
|
||||
- Marks all devices as inactive
|
||||
- Increments user's `token_version` to invalidate all access tokens
|
||||
|
||||
### 3. Authentication Middleware Updates
|
||||
|
||||
**File: `src/middleware/authMiddleware.js`**
|
||||
|
||||
- **Made middleware async**: Required for database query
|
||||
- **Added token version validation**:
|
||||
- Queries user's current `token_version` from database
|
||||
- Compares with `token_version` claim in access token
|
||||
- Rejects token if versions don't match (token has been invalidated)
|
||||
|
||||
### 4. New API Endpoint
|
||||
|
||||
**Endpoint: `POST /users/me/logout-all-devices`**
|
||||
|
||||
**Security Requirements:**
|
||||
- Requires authentication (access token)
|
||||
- Requires step-up authentication (recent OTP or `high_assurance` token)
|
||||
- Rate limited: 10 requests per hour per user
|
||||
|
||||
**Functionality:**
|
||||
1. Revokes all refresh tokens for the user
|
||||
2. Marks all devices as inactive
|
||||
3. Increments user's `token_version` (invalidates all access tokens)
|
||||
4. Logs HIGH_RISK security event (triggers security alert webhook)
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"message": "Logged out from all devices successfully",
|
||||
"revoked_tokens_count": 5
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Audit Logging
|
||||
|
||||
**Event Type:** `logout_all_devices`
|
||||
**Risk Level:** `HIGH_RISK`
|
||||
**Status:** `success`
|
||||
|
||||
**Metadata:**
|
||||
- `revoked_tokens_count`: Number of refresh tokens revoked
|
||||
- `new_token_version`: New token version after increment
|
||||
- `reason`: "user_initiated_global_logout"
|
||||
- `message`: "User initiated logout from all devices - security breach suspected"
|
||||
|
||||
**Alerting:** This event triggers security alert webhook (if configured) due to HIGH_RISK level.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Access Token Invalidation
|
||||
|
||||
1. **Token Issuance**: When an access token is issued, it includes the user's current `token_version` in the JWT payload.
|
||||
|
||||
2. **Token Validation**: On each authenticated request:
|
||||
- `authMiddleware` extracts `token_version` from the JWT payload
|
||||
- Queries the user's current `token_version` from the database
|
||||
- Compares versions - if they don't match, the token is rejected
|
||||
|
||||
3. **Global Logout**: When user calls `/users/me/logout-all-devices`:
|
||||
- All refresh tokens are revoked (marked with `revoked_at`)
|
||||
- All devices are marked as inactive
|
||||
- User's `token_version` is incremented
|
||||
- All existing access tokens (even if not expired) become invalid immediately
|
||||
|
||||
### Refresh Token Invalidation
|
||||
|
||||
Refresh tokens are stored in the database and can be directly revoked by setting `revoked_at`. The `revokeAllUserTokens()` function revokes all refresh tokens for a user.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Step-Up Authentication Required**: Users must provide recent OTP verification or have a `high_assurance` token to perform this action.
|
||||
|
||||
2. **Rate Limiting**: Limited to 10 requests per hour per user to prevent abuse.
|
||||
|
||||
3. **HIGH_RISK Logging**: All logout-all-devices events are logged with HIGH_RISK level and trigger security alerts.
|
||||
|
||||
4. **Immediate Invalidation**: Access tokens are invalidated immediately via token versioning, not just on expiry.
|
||||
|
||||
5. **Database Query Overhead**: Token version validation requires a database query on each authenticated request. This is acceptable for security-critical operations.
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual Testing
|
||||
|
||||
1. **Login and get tokens:**
|
||||
```bash
|
||||
POST /auth/verify-otp
|
||||
# Save access_token and refresh_token
|
||||
```
|
||||
|
||||
2. **Verify token works:**
|
||||
```bash
|
||||
GET /users/me
|
||||
Authorization: Bearer <access_token>
|
||||
# Should return 200 OK
|
||||
```
|
||||
|
||||
3. **Logout from all devices:**
|
||||
```bash
|
||||
POST /users/me/logout-all-devices
|
||||
Authorization: Bearer <access_token>
|
||||
# Requires step-up auth (recent OTP or high_assurance token)
|
||||
```
|
||||
|
||||
4. **Verify old token is invalid:**
|
||||
```bash
|
||||
GET /users/me
|
||||
Authorization: Bearer <old_access_token>
|
||||
# Should return 401 Unauthorized
|
||||
```
|
||||
|
||||
5. **Verify refresh token is invalid:**
|
||||
```bash
|
||||
POST /auth/refresh
|
||||
{ "refresh_token": "<old_refresh_token>" }
|
||||
# Should return 401 Unauthorized
|
||||
```
|
||||
|
||||
## API Integration
|
||||
|
||||
### Request
|
||||
|
||||
```http
|
||||
POST /users/me/logout-all-devices
|
||||
Authorization: Bearer <access_token>
|
||||
```
|
||||
|
||||
**Note:** The access token must have `high_assurance: true` or the user must have verified OTP within the last 5 minutes.
|
||||
|
||||
### Response
|
||||
|
||||
**Success (200 OK):**
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"message": "Logged out from all devices successfully",
|
||||
"revoked_tokens_count": 5
|
||||
}
|
||||
```
|
||||
|
||||
**Error (403 Forbidden - Step-up required):**
|
||||
```json
|
||||
{
|
||||
"error": "step_up_required",
|
||||
"message": "This action requires additional verification. Please verify your OTP first.",
|
||||
"requires_otp": true
|
||||
}
|
||||
```
|
||||
|
||||
**Error (429 Too Many Requests):**
|
||||
```json
|
||||
{
|
||||
"error": "Too many requests",
|
||||
"retry_after": 3600
|
||||
}
|
||||
```
|
||||
|
||||
## Files Modified
|
||||
|
||||
1. `db/farmmarket-db/init.sql` - Added `token_version` column to users table
|
||||
2. `src/services/tokenService.js` - Added token versioning and `revokeAllUserTokens()` function
|
||||
3. `src/middleware/authMiddleware.js` - Added token version validation
|
||||
4. `src/routes/authRoutes.js` - Updated user queries to include `token_version`
|
||||
5. `src/routes/userRoutes.js` - Added `/users/me/logout-all-devices` endpoint
|
||||
6. `docs/ARCHITECTURE.md` - Updated documentation with new flow
|
||||
|
||||
## Future Improvements
|
||||
|
||||
1. **Caching**: Consider caching user's `token_version` in Redis to reduce database queries (with TTL matching access token expiry).
|
||||
|
||||
2. **Metrics**: Add metrics for logout-all-devices events to track security incidents.
|
||||
|
||||
3. **Notification**: Optionally notify user via email/SMS when logout-all-devices is triggered.
|
||||
|
||||
4. **Admin Override**: Allow admins to trigger logout-all-devices for a user (with proper audit logging).
|
||||
|
||||
|
||||
|
|
@ -1,316 +0,0 @@
|
|||
# OTP Tables Analysis - What Your Code Actually Does
|
||||
|
||||
## Summary: **NO `user_id` in OTP Tables - This is CORRECT**
|
||||
|
||||
Your code uses **phone number only** for OTP tables. Users are created **AFTER** OTP verification.
|
||||
|
||||
---
|
||||
|
||||
## Database Tables
|
||||
|
||||
### 1. `otp_requests` Table (in `init.sql` lines 94-109)
|
||||
**Status: ❌ NOT USED BY YOUR CODE**
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS otp_requests (
|
||||
id UUID PRIMARY KEY,
|
||||
phone_number VARCHAR(20) NOT NULL, -- NO user_id!
|
||||
otp_hash VARCHAR(255) NOT NULL,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
consumed_at TIMESTAMPTZ, -- Extra field for tracking consumption
|
||||
attempt_count INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
**Note:** This table exists in your database schema but is **never referenced in your code**.
|
||||
|
||||
---
|
||||
|
||||
### 2. `otp_codes` Table (ACTUALLY USED)
|
||||
**Status: ✅ ACTIVELY USED BY YOUR CODE**
|
||||
|
||||
#### Database Schema (`init.sql` lines 115-122):
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS otp_codes (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
phone_number VARCHAR(20) NOT NULL, -- NO user_id!
|
||||
otp_hash VARCHAR(255) NOT NULL,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
attempt_count INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
#### Code Definition (`src/services/otpService.js` lines 34-41):
|
||||
```javascript
|
||||
CREATE TABLE IF NOT EXISTS otp_codes (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
phone_number VARCHAR(20) NOT NULL, // ← NO user_id!
|
||||
otp_hash VARCHAR(255) NOT NULL,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
attempt_count INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
**Both schemas match - NO `user_id` column!**
|
||||
|
||||
---
|
||||
|
||||
## How Your Code Uses `otp_codes`
|
||||
|
||||
### Step 1: Request OTP (`/auth/request-otp`)
|
||||
|
||||
**Location:** `src/routes/authRoutes.js` line 189
|
||||
```javascript
|
||||
const { code } = await createOtp(normalizedPhone);
|
||||
```
|
||||
|
||||
**Function:** `src/services/otpService.js` lines 82-117
|
||||
```javascript
|
||||
async function createOtp(phoneNumber) {
|
||||
await ensureOtpCodesTable();
|
||||
const code = generateOtpCode();
|
||||
const expiresAt = new Date(Date.now() + OTP_EXPIRY_MS);
|
||||
const otpHash = await bcrypt.hash(code, 10);
|
||||
|
||||
// Encrypt phone number before storing
|
||||
const encryptedPhone = encryptPhoneNumber(phoneNumber);
|
||||
|
||||
// Delete any existing OTPs for this phone number
|
||||
await db.query(
|
||||
'DELETE FROM otp_codes WHERE phone_number = $1 OR phone_number = $2',
|
||||
[encryptedPhone, phoneNumber]
|
||||
);
|
||||
|
||||
// ← INSERT: Only phone_number, NO user_id!
|
||||
await db.query(
|
||||
`INSERT INTO otp_codes (phone_number, otp_hash, expires_at, attempt_count)
|
||||
VALUES ($1, $2, $3, 0)`,
|
||||
[encryptedPhone, otpHash, expiresAt] // ← NO user_id here!
|
||||
);
|
||||
|
||||
return { code };
|
||||
}
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
- ✅ User requests OTP with **phone number only**
|
||||
- ✅ OTP stored in `otp_codes` with **phone_number only**
|
||||
- ❌ **NO user_id** because user doesn't exist yet!
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Verify OTP (`/auth/verify-otp`)
|
||||
|
||||
#### Part A: Verify OTP Code
|
||||
**Location:** `src/routes/authRoutes.js` line 267
|
||||
```javascript
|
||||
const result = await verifyOtp(normalizedPhone, code);
|
||||
```
|
||||
|
||||
**Function:** `src/services/otpService.js` lines 131-220
|
||||
```javascript
|
||||
async function verifyOtp(phoneNumber, code) {
|
||||
await ensureOtpCodesTable();
|
||||
|
||||
const encryptedPhone = encryptPhoneNumber(phoneNumber);
|
||||
|
||||
// ← SELECT: Lookup by phone_number only!
|
||||
const result = await db.query(
|
||||
`SELECT id, otp_hash, expires_at, attempt_count, phone_number
|
||||
FROM otp_codes
|
||||
WHERE phone_number = $1 OR phone_number = $2 // ← NO user_id in WHERE clause!
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1`,
|
||||
[encryptedPhone, phoneNumber]
|
||||
);
|
||||
|
||||
// ... verification logic ...
|
||||
|
||||
// ← DELETE: Remove OTP after verification
|
||||
await db.query('DELETE FROM otp_codes WHERE id = $1', [otpRecord.id]);
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
- ✅ OTP verified using **phone_number only**
|
||||
- ✅ OTP deleted from `otp_codes` table
|
||||
- ❌ **Still NO user_id** at this point!
|
||||
|
||||
---
|
||||
|
||||
#### Part B: Create/Find User (AFTER OTP Verification)
|
||||
**Location:** `src/routes/authRoutes.js` lines 310-335
|
||||
```javascript
|
||||
// ← This happens AFTER OTP verification succeeds!
|
||||
|
||||
// find or create user
|
||||
const encryptedPhone = encryptPhoneNumber(normalizedPhone);
|
||||
const phoneSearchParams = preparePhoneSearchParams(normalizedPhone);
|
||||
|
||||
let user;
|
||||
const found = await db.query(
|
||||
`SELECT id, phone_number, name, role, NULL::user_type_enum as user_type,
|
||||
COALESCE(token_version, 1) as token_version
|
||||
FROM users
|
||||
WHERE phone_number = $1 OR phone_number = $2`,
|
||||
phoneSearchParams
|
||||
);
|
||||
|
||||
if (found.rows.length === 0) {
|
||||
// ← CREATE USER HERE (after OTP verified!)
|
||||
const inserted = await db.query(
|
||||
`INSERT INTO users (phone_number) // ← Only phone_number, user gets auto-generated UUID id
|
||||
VALUES ($1)
|
||||
RETURNING id, phone_number, name, role, NULL::user_type_enum as user_type,
|
||||
COALESCE(token_version, 1) as token_version`,
|
||||
[encryptedPhone]
|
||||
);
|
||||
user = inserted.rows[0]; // ← user.id is created HERE!
|
||||
} else {
|
||||
user = found.rows[0]; // ← Existing user found
|
||||
}
|
||||
|
||||
// Now user.id exists!
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
- ✅ **AFTER** OTP verification succeeds
|
||||
- ✅ User is **found or created** based on phone_number
|
||||
- ✅ `user.id` (UUID) is **assigned at this point**
|
||||
- ✅ This is when user_id first exists!
|
||||
|
||||
---
|
||||
|
||||
## Complete Flow Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ STEP 1: Request OTP │
|
||||
│ POST /auth/request-otp │
|
||||
│ Body: { phone_number: "+919876543210" } │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ INSERT INTO otp_codes │
|
||||
│ (phone_number, otp_hash, expires_at, attempt_count) │
|
||||
│ VALUES ('+919876543210', 'hash...', '2024-...', 0) │
|
||||
│ │
|
||||
│ ❌ NO user_id - User doesn't exist yet! │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ STEP 2: Verify OTP │
|
||||
│ POST /auth/verify-otp │
|
||||
│ Body: { phone_number: "+919876543210", code: "123456" } │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ SELECT FROM otp_codes │
|
||||
│ WHERE phone_number = '+919876543210' │
|
||||
│ │
|
||||
│ ❌ NO user_id in query - phone_number only! │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼ (if OTP valid)
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ DELETE FROM otp_codes WHERE id = ... │
|
||||
│ (OTP consumed) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ STEP 3: Create/Find User │
|
||||
│ (AFTER OTP verification succeeds) │
|
||||
│ │
|
||||
│ SELECT FROM users WHERE phone_number = '+919876543210' │
|
||||
│ │
|
||||
│ If NOT found: │
|
||||
│ INSERT INTO users (phone_number) │
|
||||
│ VALUES ('+919876543210') │
|
||||
│ RETURNING id ← user.id CREATED HERE! │
|
||||
│ │
|
||||
│ ✅ user.id EXISTS NOW! │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ STEP 4: Create Session │
|
||||
│ │
|
||||
│ INSERT INTO refresh_tokens │
|
||||
│ (user_id, token_hash, device_id, ...) │
|
||||
│ VALUES (user.id, ...) ← user_id used here! │
|
||||
│ │
|
||||
│ INSERT INTO user_devices │
|
||||
│ (user_id, device_identifier, ...) │
|
||||
│ VALUES (user.id, ...) ← user_id used here! │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Why NO `user_id` in OTP Tables?
|
||||
|
||||
### ✅ Current Design (CORRECT):
|
||||
|
||||
1. **User doesn't exist when OTP is requested**
|
||||
- OTP can be requested for phone numbers that don't have accounts yet
|
||||
- User is created **after** successful OTP verification
|
||||
|
||||
2. **Phone number is the identifier**
|
||||
- Phone number is UNIQUE in `users` table
|
||||
- Phone number links OTP → User
|
||||
- No need for user_id in OTP table
|
||||
|
||||
3. **Supports both registration and login**
|
||||
- New users: OTP → Create user
|
||||
- Existing users: OTP → Find user
|
||||
- Same flow works for both!
|
||||
|
||||
### ❌ Alternative (Would Be Wrong):
|
||||
|
||||
If you added `user_id` to `otp_codes`:
|
||||
- ❌ Can't request OTP for new users (user_id doesn't exist)
|
||||
- ❌ Would need to create user before OTP (defeats purpose of verification)
|
||||
- ❌ Complicates the flow unnecessarily
|
||||
|
||||
---
|
||||
|
||||
## Summary Table
|
||||
|
||||
| Table | Has `user_id`? | When Used | Purpose |
|
||||
|-------|---------------|-----------|---------|
|
||||
| `otp_codes` | ❌ **NO** | Request/Verify OTP | Store OTP codes before user exists |
|
||||
| `otp_requests` | ❌ **NO** | ❌ **NOT USED** | Legacy table (ignore it) |
|
||||
| `users` | ✅ **YES** (primary key) | After OTP verification | Store user accounts |
|
||||
| `refresh_tokens` | ✅ **YES** | After OTP verification | Store sessions (needs user_id) |
|
||||
| `user_devices` | ✅ **YES** | After OTP verification | Track devices (needs user_id) |
|
||||
|
||||
---
|
||||
|
||||
## Answer to Your Question
|
||||
|
||||
**Q: Should `otp_code` and `otp_req` include user_id?**
|
||||
|
||||
**A: ❌ NO** - Your current code is **correct as-is**:
|
||||
|
||||
1. **`otp_codes`** - Does NOT have `user_id` ✅ (correct)
|
||||
2. **`otp_requests`** - Does NOT have `user_id`, but also **NOT USED** by your code
|
||||
|
||||
**Q: Are we going to create a user ID before and assign it, or only use phone number?**
|
||||
|
||||
**A:** Your code uses **phone number only** for OTP, then creates user_id **AFTER** OTP verification:
|
||||
- OTP request/verify: Uses **phone_number only**
|
||||
- User creation: Happens **AFTER** OTP verification succeeds
|
||||
- User_id assignment: Happens when user is created/found in `users` table
|
||||
|
||||
This is the **standard and secure** pattern for phone-based authentication! ✅
|
||||
|
||||
|
||||
348
QUICK_START.md
348
QUICK_START.md
|
|
@ -1,348 +0,0 @@
|
|||
# Farm Auth Service - Quick Start
|
||||
|
||||
## Base URL
|
||||
```
|
||||
Development: http://localhost:3000
|
||||
Production: https://your-domain.com
|
||||
```
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
```
|
||||
1. POST /auth/request-otp → User enters phone number
|
||||
2. POST /auth/verify-otp → User enters OTP code → Get tokens
|
||||
3. Use access_token in header → Authorization: Bearer <token>
|
||||
4. POST /auth/refresh → Get new tokens when expired
|
||||
5. POST /auth/logout → Revoke token
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Endpoints
|
||||
|
||||
### 1. Request OTP
|
||||
```http
|
||||
POST /auth/request-otp
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"phone_number": "+919876543210"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{ "ok": true }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Verify OTP (Login)
|
||||
```http
|
||||
POST /auth/verify-otp
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"phone_number": "+919876543210",
|
||||
"code": "123456",
|
||||
"device_id": "android-device-id-123",
|
||||
"device_info": {
|
||||
"platform": "android",
|
||||
"model": "Samsung Galaxy",
|
||||
"os_version": "Android 14",
|
||||
"app_version": "1.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"user": {
|
||||
"id": "uuid",
|
||||
"phone_number": "+919876543210",
|
||||
"name": null,
|
||||
"role": "user",
|
||||
"user_type": null
|
||||
},
|
||||
"access_token": "eyJhbGc...",
|
||||
"refresh_token": "eyJhbGc...",
|
||||
"needs_profile": true
|
||||
}
|
||||
```
|
||||
|
||||
**Store:** `access_token` and `refresh_token` securely (EncryptedSharedPreferences)
|
||||
|
||||
---
|
||||
|
||||
### 3. Refresh Token
|
||||
```http
|
||||
POST /auth/refresh
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"refresh_token": "eyJhbGc..."
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"access_token": "eyJhbGc...",
|
||||
"refresh_token": "eyJhbGc..." ← Always save this new token!
|
||||
}
|
||||
```
|
||||
|
||||
**Important:** Refresh tokens rotate. Always save the new `refresh_token`.
|
||||
|
||||
---
|
||||
|
||||
### 4. Update Profile
|
||||
```http
|
||||
PUT /users/me
|
||||
Authorization: Bearer <access_token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "John Doe",
|
||||
"user_type": "seller" // or "buyer" or "service_provider"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"phone_number": "+919876543210",
|
||||
"name": "John Doe",
|
||||
"role": "user",
|
||||
"user_type": "seller"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Logout
|
||||
```http
|
||||
POST /auth/logout
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"refresh_token": "eyJhbGc..."
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{ "ok": true }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Kotlin Data Classes
|
||||
|
||||
```kotlin
|
||||
// Request Models
|
||||
data class RequestOtpRequest(val phone_number: String)
|
||||
|
||||
data class VerifyOtpRequest(
|
||||
val phone_number: String,
|
||||
val code: String,
|
||||
val device_id: String,
|
||||
val device_info: DeviceInfo? = null
|
||||
)
|
||||
|
||||
data class DeviceInfo(
|
||||
val platform: String = "android",
|
||||
val model: String? = null,
|
||||
val os_version: String? = null,
|
||||
val app_version: String? = null,
|
||||
val language_code: String? = null,
|
||||
val timezone: String? = null
|
||||
)
|
||||
|
||||
data class RefreshRequest(val refresh_token: String)
|
||||
|
||||
data class UpdateProfileRequest(
|
||||
val name: String,
|
||||
val user_type: String // "seller" | "buyer" | "service_provider"
|
||||
)
|
||||
|
||||
// Response Models
|
||||
data class User(
|
||||
val id: String,
|
||||
val phone_number: String,
|
||||
val name: String?,
|
||||
val role: String,
|
||||
val user_type: String?
|
||||
)
|
||||
|
||||
data class VerifyOtpResponse(
|
||||
val user: User,
|
||||
val access_token: String,
|
||||
val refresh_token: String,
|
||||
val needs_profile: Boolean
|
||||
)
|
||||
|
||||
data class RefreshResponse(
|
||||
val access_token: String,
|
||||
val refresh_token: String
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example: Kotlin HTTP Client
|
||||
|
||||
```kotlin
|
||||
// Using Retrofit + OkHttp
|
||||
interface AuthApi {
|
||||
@POST("auth/request-otp")
|
||||
suspend fun requestOtp(@Body request: RequestOtpRequest): Response<Unit>
|
||||
|
||||
@POST("auth/verify-otp")
|
||||
suspend fun verifyOtp(@Body request: VerifyOtpRequest): Response<VerifyOtpResponse>
|
||||
|
||||
@POST("auth/refresh")
|
||||
suspend fun refreshToken(@Body request: RefreshRequest): Response<RefreshResponse>
|
||||
|
||||
@PUT("users/me")
|
||||
suspend fun updateProfile(
|
||||
@Header("Authorization") token: String,
|
||||
@Body request: UpdateProfileRequest
|
||||
): Response<User>
|
||||
|
||||
@POST("auth/logout")
|
||||
suspend fun logout(@Body request: RefreshRequest): Response<Unit>
|
||||
}
|
||||
|
||||
// Usage
|
||||
val api = Retrofit.Builder()
|
||||
.baseUrl("http://your-api-url")
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
.create(AuthApi::class.java)
|
||||
|
||||
// Request OTP
|
||||
api.requestOtp(RequestOtpRequest("+919876543210"))
|
||||
|
||||
// Verify OTP
|
||||
val response = api.verifyOtp(
|
||||
VerifyOtpRequest(
|
||||
phone_number = "+919876543210",
|
||||
code = "123456",
|
||||
device_id = getDeviceId(),
|
||||
device_info = DeviceInfo(platform = "android")
|
||||
)
|
||||
)
|
||||
val tokens = response.body() // Save access_token & refresh_token
|
||||
|
||||
// Make authenticated request
|
||||
val user = api.updateProfile(
|
||||
"Bearer ${accessToken}",
|
||||
UpdateProfileRequest("John", "seller")
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Codes
|
||||
|
||||
| Code | Error | Solution |
|
||||
|------|-------|----------|
|
||||
| 400 | `phone_number is required` | Include phone_number in request |
|
||||
| 400 | `Invalid or expired OTP` | Re-request OTP (expires in 10 min) |
|
||||
| 401 | `Invalid refresh token` | Force re-login |
|
||||
| 401 | `Missing Authorization header` | Include `Authorization: Bearer <token>` |
|
||||
| 403 | `Origin not allowed` | CORS issue (production) |
|
||||
| 500 | `Internal server error` | Retry later |
|
||||
|
||||
---
|
||||
|
||||
## Token Storage (Android)
|
||||
|
||||
```kotlin
|
||||
// Use EncryptedSharedPreferences
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
|
||||
class TokenStorage(context: Context) {
|
||||
private val prefs = EncryptedSharedPreferences.create(
|
||||
context,
|
||||
"auth_tokens",
|
||||
MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(),
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
)
|
||||
|
||||
fun saveTokens(access: String, refresh: String) {
|
||||
prefs.edit().apply {
|
||||
putString("access_token", access)
|
||||
putString("refresh_token", refresh)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
fun getAccessToken() = prefs.getString("access_token", null)
|
||||
fun getRefreshToken() = prefs.getString("refresh_token", null)
|
||||
fun clear() = prefs.edit().clear().apply()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Token Expiration
|
||||
|
||||
- **Access Token:** 15 minutes (auto-refresh on 401)
|
||||
- **Refresh Token:** 7 days
|
||||
- **OTP:** 10 minutes
|
||||
|
||||
---
|
||||
|
||||
## Phone Number Format
|
||||
|
||||
- `9876543210` → Auto-converted to `+919876543210` (10-digit = India)
|
||||
- `+919876543210` → Used as-is
|
||||
- Always send in E.164 format: `+<country_code><number>`
|
||||
|
||||
---
|
||||
|
||||
## Device ID
|
||||
|
||||
- Must be 4-128 alphanumeric characters
|
||||
- Use: Android ID, Installation ID, or Firebase Installation ID
|
||||
- Invalid formats are auto-hashed
|
||||
|
||||
---
|
||||
|
||||
## Security Notes
|
||||
|
||||
1. ✅ Store tokens in EncryptedSharedPreferences
|
||||
2. ✅ Auto-refresh access token on 401 errors
|
||||
3. ✅ Always save new refresh_token after refresh (tokens rotate)
|
||||
4. ✅ Logout clears tokens and revokes refresh token
|
||||
5. ⚠️ If refresh returns 401 → Force re-login (token compromised/reused)
|
||||
|
||||
---
|
||||
|
||||
## Full Example Flow
|
||||
|
||||
```kotlin
|
||||
// 1. Request OTP
|
||||
api.requestOtp(RequestOtpRequest("+919876543210"))
|
||||
|
||||
// 2. Verify OTP & Save Tokens
|
||||
val loginResponse = api.verifyOtp(...)
|
||||
tokenStorage.saveTokens(loginResponse.access_token, loginResponse.refresh_token)
|
||||
|
||||
// 3. Use Access Token
|
||||
val user = api.updateProfile("Bearer ${tokenStorage.getAccessToken()}", ...)
|
||||
|
||||
// 4. Handle Token Expiration (401) → Refresh
|
||||
val refreshResponse = api.refreshToken(RefreshRequest(tokenStorage.getRefreshToken()!!))
|
||||
tokenStorage.saveTokens(refreshResponse.access_token, refreshResponse.refresh_token)
|
||||
|
||||
// 5. Logout
|
||||
api.logout(RefreshRequest(tokenStorage.getRefreshToken()!!))
|
||||
tokenStorage.clear()
|
||||
```
|
||||
|
|
@ -1,247 +0,0 @@
|
|||
# Rate Limiting & OTP Throttling Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
This document explains the secure rate limiting and OTP throttling implementation added to the authentication service.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Components Added
|
||||
|
||||
1. **Redis Client** (`src/services/redisClient.js`)
|
||||
- Manages Redis connection with graceful fallback to in-memory storage
|
||||
- Supports `REDIS_URL` or `REDIS_HOST`/`REDIS_PORT` configuration
|
||||
|
||||
2. **Rate Limiting Middleware** (`src/middleware/rateLimitMiddleware.js`)
|
||||
- Per-phone rate limiting for OTP requests
|
||||
- Per-IP rate limiting for OTP requests
|
||||
- Active OTP checking (2-minute no-resend rule)
|
||||
- Failed verification attempt tracking
|
||||
|
||||
3. **Updated OTP Service** (`src/services/otpService.js`)
|
||||
- Changed OTP expiry from 10 minutes to 2 minutes (120 seconds)
|
||||
- Enhanced attempt tracking with generic error responses
|
||||
|
||||
4. **Updated Auth Routes** (`src/routes/authRoutes.js`)
|
||||
- Integrated all rate limiting middleware
|
||||
- Updated error responses to be generic
|
||||
|
||||
## Rate Limiting Rules
|
||||
|
||||
### POST /auth/request-otp
|
||||
|
||||
**Per Phone Number:**
|
||||
- Max 3 requests per 10 minutes
|
||||
- Max 10 requests per 24 hours
|
||||
- **No resend if active OTP exists** (within 2 minutes of generation)
|
||||
|
||||
**Per IP Address:**
|
||||
- Max 20 requests per 10 minutes
|
||||
- Max 100 requests per 24 hours
|
||||
|
||||
**Response on Limit Exceeded:**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Too many OTP requests. Please try again later."
|
||||
}
|
||||
```
|
||||
HTTP Status: `429 Too Many Requests`
|
||||
|
||||
**Response if Active OTP Exists:**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "An OTP is already active. Please wait a moment before requesting a new one."
|
||||
}
|
||||
```
|
||||
HTTP Status: `429 Too Many Requests`
|
||||
|
||||
### POST /auth/verify-otp
|
||||
|
||||
**Per OTP:**
|
||||
- Max 5 verification attempts per OTP
|
||||
- After 5 failed attempts, OTP is invalidated
|
||||
|
||||
**Per Phone Number:**
|
||||
- Max 10 failed verifications per hour
|
||||
- If exceeded, all verification attempts are blocked temporarily
|
||||
|
||||
**Response on Failure:**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "OTP invalid or expired. Please request a new one."
|
||||
}
|
||||
```
|
||||
HTTP Status: `400 Bad Request`
|
||||
|
||||
**Response on Too Many Failed Attempts:**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Too many attempts. Please try again later."
|
||||
}
|
||||
```
|
||||
HTTP Status: `429 Too Many Requests`
|
||||
|
||||
## OTP Validity
|
||||
|
||||
- **OTP expires after 2 minutes (120 seconds)** from generation
|
||||
- **No new OTP can be sent** to the same phone number while an active OTP exists
|
||||
- Once verified successfully, OTP is immediately deleted
|
||||
|
||||
## Redis Setup
|
||||
|
||||
### Configuration
|
||||
|
||||
The system uses Redis for rate limiting counters with automatic TTL expiration. If Redis is not available, it falls back to in-memory storage.
|
||||
|
||||
**Environment Variables:**
|
||||
|
||||
```bash
|
||||
# Option 1: Full Redis URL
|
||||
REDIS_URL=redis://localhost:6379
|
||||
# or with password
|
||||
REDIS_URL=redis://:password@localhost:6379
|
||||
|
||||
# Option 2: Separate host/port
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=your_password # Optional
|
||||
```
|
||||
|
||||
### Redis Keys Used
|
||||
|
||||
- `otp_req:phone:{phone}:10min` - Phone requests (10 min window)
|
||||
- `otp_req:phone:{phone}:day` - Phone requests (24 hour window)
|
||||
- `otp_req:ip:{ip}:10min` - IP requests (10 min window)
|
||||
- `otp_req:ip:{ip}:day` - IP requests (24 hour window)
|
||||
- `otp_active:phone:{phone}` - Active OTP marker (2 min TTL)
|
||||
- `otp_verify_failed:phone:{phone}:hour` - Failed verifications (1 hour window)
|
||||
|
||||
All keys automatically expire based on their TTL.
|
||||
|
||||
## Configuration
|
||||
|
||||
All limits are configurable via environment variables:
|
||||
|
||||
```bash
|
||||
# OTP Request Limits
|
||||
OTP_REQ_PHONE_10MIN_LIMIT=3 # Default: 3
|
||||
OTP_REQ_PHONE_DAY_LIMIT=10 # Default: 10
|
||||
OTP_REQ_IP_10MIN_LIMIT=20 # Default: 20
|
||||
OTP_REQ_IP_DAY_LIMIT=100 # Default: 100
|
||||
|
||||
# OTP Verification Limits
|
||||
OTP_VERIFY_MAX_ATTEMPTS=5 # Default: 5
|
||||
OTP_VERIFY_FAILED_PER_HOUR_LIMIT=10 # Default: 10
|
||||
|
||||
# OTP Validity
|
||||
OTP_TTL_SECONDS=120 # Default: 120 (2 minutes)
|
||||
|
||||
# Proxy Support (for correct IP detection)
|
||||
TRUST_PROXY=true # Set if behind reverse proxy
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Redis Client Initialization
|
||||
|
||||
The Redis client is initialized in `src/index.js`:
|
||||
|
||||
```javascript
|
||||
const { initRedis } = require('./services/redisClient');
|
||||
initRedis().catch((err) => {
|
||||
console.warn('Redis initialization warning:', err.message);
|
||||
});
|
||||
```
|
||||
|
||||
If Redis is unavailable, the system logs a warning and continues with in-memory fallback.
|
||||
|
||||
### Rate Limiting Logic
|
||||
|
||||
1. **Request Flow for `/auth/request-otp`:**
|
||||
```
|
||||
Request → checkActiveOtpForPhone → rateLimitRequestOtpByPhone → rateLimitRequestOtpByIp → createOtp
|
||||
```
|
||||
|
||||
2. **Request Flow for `/auth/verify-otp`:**
|
||||
```
|
||||
Request → rateLimitVerifyOtpByPhone → verifyOtp → (on failure) incrementFailedVerify
|
||||
```
|
||||
|
||||
### Memory Store Fallback
|
||||
|
||||
When Redis is not available, the system uses an in-memory store that:
|
||||
- Maintains counters with expiration timestamps
|
||||
- Automatically cleans up expired entries every minute
|
||||
- Works identically to Redis for rate limiting purposes
|
||||
- **Note:** In-memory store is per-process and doesn't persist across restarts
|
||||
|
||||
### Error Handling
|
||||
|
||||
All rate limiting middleware uses a "fail open" strategy:
|
||||
- If Redis errors occur, the request is allowed to proceed
|
||||
- Errors are logged but don't block legitimate users
|
||||
- This ensures availability even if rate limiting infrastructure fails
|
||||
|
||||
## Security Features
|
||||
|
||||
1. **Generic Error Messages:** All error responses use generic messages to avoid information leakage
|
||||
2. **No Information Leakage:** Errors don't distinguish between "wrong OTP", "expired OTP", or "max attempts"
|
||||
3. **Automatic Cleanup:** Expired OTPs and rate limit counters are automatically cleaned up
|
||||
4. **IP-based Protection:** Prevents abuse from single IP addresses
|
||||
5. **Phone-based Protection:** Prevents abuse targeting specific phone numbers
|
||||
|
||||
## Testing
|
||||
|
||||
To test the rate limiting:
|
||||
|
||||
1. **Test Active OTP Rule:**
|
||||
```bash
|
||||
# Request OTP
|
||||
curl -X POST http://localhost:3000/auth/request-otp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"phone_number": "+1234567890"}'
|
||||
|
||||
# Immediately try again (should be blocked)
|
||||
curl -X POST http://localhost:3000/auth/request-otp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"phone_number": "+1234567890"}'
|
||||
```
|
||||
|
||||
2. **Test Rate Limits:**
|
||||
```bash
|
||||
# Send multiple requests quickly (should hit limit after 3)
|
||||
for i in {1..5}; do
|
||||
curl -X POST http://localhost:3000/auth/request-otp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"phone_number": "+1234567890"}'
|
||||
sleep 1
|
||||
done
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
Added to `package.json`:
|
||||
- `redis`: ^4.7.0
|
||||
|
||||
Install with:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The system gracefully degrades if Redis is unavailable
|
||||
- All rate limits are enforced independently (phone and IP limits both apply)
|
||||
- OTP expiry time changed from 10 minutes to 2 minutes
|
||||
- SMS message updated to reflect 2-minute expiry
|
||||
- Existing JWT and user creation logic remains unchanged
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,354 +0,0 @@
|
|||
# Remaining Security Gaps Analysis
|
||||
|
||||
## ✅ Fully Resolved Issues
|
||||
|
||||
### 1. ✅ Rate Limiting / OTP Throttling
|
||||
**Status:** **RESOLVED**
|
||||
- Rate limiting implemented per phone and per IP
|
||||
- OTP request throttling (3 per 10 min, 10 per 24h per phone)
|
||||
- OTP verification attempt limits (5 per OTP, 10 failed per hour)
|
||||
- **Location:** `src/middleware/rateLimitMiddleware.js`
|
||||
|
||||
### 2. ✅ OTP Exposure in Logs
|
||||
**Status:** **RESOLVED**
|
||||
- Safe OTP logging helper created (`src/utils/otpLogger.js`)
|
||||
- Only logs in development mode
|
||||
- Never logs to production
|
||||
- **Location:** `src/services/smsService.js` uses `logOtpForDebug()`
|
||||
|
||||
### 3. ✅ IP/Device Risk Controls
|
||||
**Status:** **RESOLVED**
|
||||
- IP blocking for configured CIDR ranges
|
||||
- Risk scoring based on IP/device/user-agent changes
|
||||
- Suspicious refresh detection
|
||||
- **Location:** `src/services/riskScoring.js`, integrated in `src/routes/authRoutes.js`
|
||||
|
||||
### 4. ✅ Refresh Token Theft Mitigation
|
||||
**Status:** **RESOLVED**
|
||||
- Environment fingerprinting (IP, user-agent, device ID)
|
||||
- Suspicious refresh detection with risk scoring
|
||||
- Optional OTP re-verification for suspicious refreshes
|
||||
- Enhanced logging of suspicious events
|
||||
- **Location:** `src/routes/authRoutes.js` (refresh endpoint)
|
||||
|
||||
### 5. ✅ JWT Claims Validation
|
||||
**Status:** **RESOLVED**
|
||||
- Strict validation of `iss`, `aud`, `exp`, `iat`, `nbf`
|
||||
- Centralized validation function
|
||||
- Used in all token verification paths
|
||||
- **Location:** `src/services/jwtKeys.js` - `validateTokenClaims()`
|
||||
|
||||
### 6. ✅ CORS Hardening
|
||||
**Status:** **RESOLVED**
|
||||
- Strict origin whitelisting
|
||||
- No wildcard support when credentials involved
|
||||
- Production requirement for explicit origins
|
||||
- **Location:** `src/index.js`
|
||||
|
||||
### 7. ✅ CSRF Protection (Documented)
|
||||
**Status:** **RESOLVED** (Not needed - using Bearer tokens)
|
||||
- Comprehensive documentation provided
|
||||
- Guidance for future cookie-based implementation
|
||||
- **Location:** `CSRF_NOTES.md`
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Partially Resolved Issues
|
||||
|
||||
### 8. ⚠️ Access Token Replay Mitigation
|
||||
**Status:** **PARTIALLY RESOLVED**
|
||||
|
||||
**What's Done:**
|
||||
- ✅ Step-up authentication middleware created (`src/middleware/stepUpAuth.js`)
|
||||
- ✅ Access tokens include `high_assurance` claim after OTP verification
|
||||
- ✅ Middleware checks for recent OTP or high assurance token
|
||||
|
||||
**What's Missing:**
|
||||
- ❌ **Step-up auth NOT applied to sensitive routes**
|
||||
- ❌ Sensitive operations in `src/routes/userRoutes.js` don't use `requireRecentOtpOrReauth`:
|
||||
- `PUT /users/me` - Profile updates (could change phone number)
|
||||
- `DELETE /users/me/devices/:device_id` - Device revocation
|
||||
- `POST /users/me/logout-all-other-devices` - Mass device logout
|
||||
|
||||
**Risk Level:** 🟡 **MEDIUM**
|
||||
- Stolen access token can be used for sensitive operations within 15-minute window
|
||||
- Attacker could revoke devices, update profile, etc.
|
||||
|
||||
**Recommendation:**
|
||||
```javascript
|
||||
// Apply to sensitive routes:
|
||||
router.put('/me', auth, requireRecentOtpOrReauth, async (req, res) => { ... });
|
||||
router.delete('/me/devices/:device_id', auth, requireRecentOtpOrReauth, async (req, res) => { ... });
|
||||
router.post('/me/logout-all-other-devices', auth, requireRecentOtpOrReauth, async (req, res) => { ... });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9. ⚠️ Secrets Management & Rotation
|
||||
**Status:** **PARTIALLY RESOLVED**
|
||||
|
||||
**What's Done:**
|
||||
- ✅ JWT key rotation structure implemented
|
||||
- ✅ Support for multiple keys with `kid` (key ID)
|
||||
- ✅ Graceful rotation without breaking existing tokens
|
||||
- ✅ Code structure ready for secrets manager integration
|
||||
|
||||
**What's Missing:**
|
||||
- ❌ **Still reads secrets from `.env` file**
|
||||
- ❌ No integration with secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.)
|
||||
- ❌ Manual key rotation process (no automation)
|
||||
- ❌ Twilio credentials still in `.env`
|
||||
|
||||
**Risk Level:** 🟡 **MEDIUM-HIGH**
|
||||
- If `.env` leaks or is committed to git, attacker can:
|
||||
- Forge JWTs
|
||||
- Send SMS via Twilio
|
||||
- Access database
|
||||
|
||||
**Recommendation:**
|
||||
1. **Immediate:** Ensure `.env` is in `.gitignore` and never committed
|
||||
2. **Short-term:** Use environment variables from secure deployment platform
|
||||
3. **Long-term:** Integrate with secrets manager (see TODOs in `src/services/jwtKeys.js`)
|
||||
|
||||
**TODO in Code:**
|
||||
- `src/services/jwtKeys.js` line 169: Example implementation for AWS Secrets Manager
|
||||
- Need to implement `loadKeysFromSecretsManager()` function
|
||||
|
||||
---
|
||||
|
||||
### 10. ⚠️ Audit Logs Active Monitoring
|
||||
**Status:** **PARTIALLY RESOLVED**
|
||||
|
||||
**What's Done:**
|
||||
- ✅ Enhanced audit logging with risk levels (INFO, SUSPICIOUS, HIGH_RISK)
|
||||
- ✅ Structured logging with metadata
|
||||
- ✅ Anomaly detection helper function (`checkAnomalies()`)
|
||||
- ✅ Suspicious events logged (failed OTPs, suspicious refreshes, blocked IPs)
|
||||
|
||||
**What's Missing:**
|
||||
- ❌ **No active alerting/monitoring integration**
|
||||
- ❌ No automated alerts for HIGH_RISK events
|
||||
- ❌ No integration with PagerDuty, Slack, email, etc.
|
||||
- ❌ Anomaly detection only logs to console (line 183 in `auditLogger.js`)
|
||||
|
||||
**Risk Level:** 🟡 **MEDIUM**
|
||||
- Attacks happen silently until manual log inspection
|
||||
- No real-time notification of security events
|
||||
|
||||
**Recommendation:**
|
||||
1. **Immediate:** Set up log aggregation (CloudWatch, Datadog, etc.)
|
||||
2. **Short-term:** Implement alerting for HIGH_RISK events
|
||||
3. **Long-term:** Integrate with SIEM system
|
||||
|
||||
**TODO in Code:**
|
||||
- `src/services/auditLogger.js` line 183: `// TODO: Trigger alert (email, webhook, etc.)`
|
||||
- `src/services/auditLogger.js` line 193: Example implementation for external alerting
|
||||
|
||||
---
|
||||
|
||||
### 11. ⚠️ Input Validation
|
||||
**Status:** **PARTIALLY RESOLVED**
|
||||
|
||||
**What's Done:**
|
||||
- ✅ Input validation middleware created (`src/middleware/validation.js`)
|
||||
- ✅ Validation applied to all auth routes:
|
||||
- `/auth/request-otp`
|
||||
- `/auth/verify-otp`
|
||||
- `/auth/refresh`
|
||||
- `/auth/logout`
|
||||
|
||||
**What's Missing:**
|
||||
- ❌ **Validation NOT applied to user routes** (`src/routes/userRoutes.js`):
|
||||
- `PUT /users/me` - No validation for `name`, `user_type`
|
||||
- `DELETE /users/me/devices/:device_id` - No validation for `device_id` param
|
||||
- `POST /users/me/logout-all-other-devices` - No validation for `current_device_id`
|
||||
|
||||
**Risk Level:** 🟢 **LOW-MEDIUM**
|
||||
- Less critical than auth endpoints, but still important
|
||||
- Could allow unexpected payloads or edge-case bugs
|
||||
|
||||
**Recommendation:**
|
||||
```javascript
|
||||
// Add validation middleware to user routes:
|
||||
const { validateUpdateProfileBody, validateDeviceIdParam } = require('../middleware/validation');
|
||||
|
||||
router.put('/me', auth, validateUpdateProfileBody, async (req, res) => { ... });
|
||||
router.delete('/me/devices/:device_id', auth, validateDeviceIdParam, async (req, res) => { ... });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ❌ Unaddressed Attack Scenarios
|
||||
|
||||
### 12. ❌ Database Compromise
|
||||
**Status:** **NOT ADDRESSED**
|
||||
|
||||
**Risk:**
|
||||
- If database is compromised, attacker can:
|
||||
- See phone numbers, device metadata, audit logs
|
||||
- Correlate patterns for phishing/SIM-swap attacks
|
||||
- Access user data
|
||||
|
||||
**Mitigation Needed:**
|
||||
- ✅ Already using parameterized queries (good)
|
||||
- ❌ No encryption at rest for sensitive fields
|
||||
- ❌ No field-level encryption for PII
|
||||
- ❌ No database access logging/auditing
|
||||
|
||||
**Recommendation:**
|
||||
- Encrypt sensitive fields (phone numbers) at rest
|
||||
- Implement database access logging
|
||||
- Use database encryption (TDE, etc.)
|
||||
- Regular security audits
|
||||
|
||||
---
|
||||
|
||||
### 13. ❌ Man-in-the-Middle (Non-HTTPS)
|
||||
**Status:** **NOT ADDRESSED** (Infrastructure concern)
|
||||
|
||||
**Risk:**
|
||||
- If client app talks to service over HTTP (no TLS):
|
||||
- Attacker can intercept access/refresh tokens
|
||||
- Attacker can intercept OTPs
|
||||
- Full account compromise
|
||||
|
||||
**Mitigation:**
|
||||
- ✅ Server assumes TLS termination in front (reverse proxy)
|
||||
- ❌ No enforcement of HTTPS-only connections
|
||||
- ❌ No HSTS headers
|
||||
- ❌ No certificate pinning guidance
|
||||
|
||||
**Recommendation:**
|
||||
- Enforce HTTPS at reverse proxy/load balancer
|
||||
- Add HSTS headers
|
||||
- Document TLS requirements
|
||||
- Consider certificate pinning for mobile apps
|
||||
|
||||
---
|
||||
|
||||
### 14. ❌ Misconfigured CORS + XSS
|
||||
**Status:** **PARTIALLY ADDRESSED**
|
||||
|
||||
**What's Done:**
|
||||
- ✅ CORS hardened with strict origin whitelisting
|
||||
- ✅ Documentation warns about misconfiguration
|
||||
|
||||
**What's Missing:**
|
||||
- ❌ No validation that CORS is properly configured in production
|
||||
- ❌ No runtime checks for CORS misconfiguration
|
||||
- ❌ No guidance for XSS prevention in frontend
|
||||
|
||||
**Risk Level:** 🟡 **MEDIUM**
|
||||
- If frontend has XSS and CORS is misconfigured:
|
||||
- Attacker can use victim's tokens from malicious page
|
||||
- Account takeover possible
|
||||
|
||||
**Recommendation:**
|
||||
- Add startup validation that CORS origins are configured in production
|
||||
- Document XSS prevention best practices
|
||||
- Consider Content Security Policy (CSP) headers
|
||||
|
||||
---
|
||||
|
||||
## Summary Table
|
||||
|
||||
| Issue | Status | Risk Level | Priority |
|
||||
|-------|--------|------------|----------|
|
||||
| 1. Rate Limiting | ✅ Resolved | - | - |
|
||||
| 2. OTP Logging | ✅ Resolved | - | - |
|
||||
| 3. IP/Device Risk | ✅ Resolved | - | - |
|
||||
| 4. Refresh Token Theft | ✅ Resolved | - | - |
|
||||
| 5. JWT Claims | ✅ Resolved | - | - |
|
||||
| 6. CORS | ✅ Resolved | - | - |
|
||||
| 7. CSRF | ✅ Documented | - | - |
|
||||
| 8. Access Token Replay | ⚠️ Partial | 🟡 Medium | **HIGH** |
|
||||
| 9. Secrets Management | ⚠️ Partial | 🟡 Medium-High | **HIGH** |
|
||||
| 10. Audit Monitoring | ⚠️ Partial | 🟡 Medium | **MEDIUM** |
|
||||
| 11. Input Validation | ⚠️ Partial | 🟢 Low-Medium | **MEDIUM** |
|
||||
| 12. Database Compromise | ❌ Not Addressed | 🔴 High | **MEDIUM** |
|
||||
| 13. MITM (HTTP) | ❌ Not Addressed | 🔴 High | **LOW** (Infra) |
|
||||
| 14. CORS + XSS | ⚠️ Partial | 🟡 Medium | **LOW** |
|
||||
|
||||
---
|
||||
|
||||
## Immediate Action Items (Priority Order)
|
||||
|
||||
### 🔴 HIGH PRIORITY
|
||||
|
||||
1. **Apply Step-Up Auth to Sensitive Routes**
|
||||
- Add `requireRecentOtpOrReauth` to user routes
|
||||
- Prevents access token replay attacks
|
||||
|
||||
2. **Secrets Management**
|
||||
- Move secrets to environment variables (not `.env` file)
|
||||
- Plan integration with secrets manager
|
||||
- Ensure `.env` is never committed
|
||||
|
||||
### 🟡 MEDIUM PRIORITY
|
||||
|
||||
3. **Active Monitoring/Alerting**
|
||||
- Set up alerts for HIGH_RISK audit events
|
||||
- Integrate with monitoring system (CloudWatch, Datadog, etc.)
|
||||
|
||||
4. **Complete Input Validation**
|
||||
- Add validation middleware to user routes
|
||||
- Validate all request parameters
|
||||
|
||||
### 🟢 LOW PRIORITY
|
||||
|
||||
5. **Database Security**
|
||||
- Plan encryption at rest
|
||||
- Implement database access logging
|
||||
|
||||
6. **Infrastructure Security**
|
||||
- Document TLS/HTTPS requirements
|
||||
- Add HSTS headers
|
||||
- Validate CORS configuration at startup
|
||||
|
||||
---
|
||||
|
||||
## Code Locations for Fixes
|
||||
|
||||
### Step-Up Auth Application
|
||||
**File:** `src/routes/userRoutes.js`
|
||||
- Line 99: `PUT /users/me` - Add `requireRecentOtpOrReauth`
|
||||
- Line 160: `DELETE /users/me/devices/:device_id` - Add `requireRecentOtpOrReauth`
|
||||
- Line 203: `POST /users/me/logout-all-other-devices` - Add `requireRecentOtpOrReauth`
|
||||
|
||||
### Input Validation
|
||||
**File:** `src/routes/userRoutes.js`
|
||||
- Add validation middleware imports
|
||||
- Apply to all routes
|
||||
|
||||
### Secrets Manager Integration
|
||||
**File:** `src/services/jwtKeys.js`
|
||||
- Line 169: Implement `loadKeysFromSecretsManager()` function
|
||||
- Replace `.env` reads with secrets manager calls
|
||||
|
||||
### Alerting Integration
|
||||
**File:** `src/services/auditLogger.js`
|
||||
- Line 183: Implement alert triggering
|
||||
- Line 193: Add external alerting integration
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
**7 out of 14 issues are fully resolved** ✅
|
||||
**4 issues are partially resolved** ⚠️ (need completion)
|
||||
**3 issues are not addressed** ❌ (infrastructure/planning)
|
||||
|
||||
**Overall Security Posture:** 🟡 **GOOD** (with room for improvement)
|
||||
|
||||
The most critical remaining gaps are:
|
||||
1. Step-up auth not applied to sensitive routes
|
||||
2. Secrets still in `.env` file
|
||||
3. No active monitoring/alerting
|
||||
|
||||
These should be addressed before production deployment.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,675 +0,0 @@
|
|||
# Security Audit Report
|
||||
**Date:** $(date)
|
||||
**Service:** Farm Auth Service
|
||||
**Status:** Comprehensive Security Review
|
||||
|
||||
---
|
||||
|
||||
## ✅ **FULLY RESOLVED ISSUES**
|
||||
|
||||
### **1. ✅ Rate Limiting / OTP Throttling**
|
||||
**Status:** **RESOLVED** ✅
|
||||
- Rate limiting implemented per phone and per IP
|
||||
- OTP request throttling (3 per 10 min, 10 per 24h per phone)
|
||||
- OTP verification attempt limits (5 per OTP, 10 failed per hour)
|
||||
- **Location:** `src/middleware/rateLimitMiddleware.js`
|
||||
|
||||
### **2. ✅ OTP Exposure in Logs**
|
||||
**Status:** **RESOLVED** ✅
|
||||
- Safe OTP logging helper created (`src/utils/otpLogger.js`)
|
||||
- Only logs in development mode
|
||||
- Never logs to production
|
||||
- **Location:** `src/services/smsService.js` uses `logOtpForDebug()`
|
||||
|
||||
### **3. ✅ IP/Device Risk Controls**
|
||||
**Status:** **RESOLVED** ✅
|
||||
- IP blocking for configured CIDR ranges
|
||||
- Risk scoring based on IP/device/user-agent changes
|
||||
- Suspicious refresh detection
|
||||
- **Location:** `src/services/riskScoring.js`, integrated in `src/routes/authRoutes.js`
|
||||
|
||||
### **4. ✅ Refresh Token Theft Mitigation**
|
||||
**Status:** **RESOLVED** ✅
|
||||
- Environment fingerprinting (IP, user-agent, device ID)
|
||||
- Suspicious refresh detection with risk scoring
|
||||
- Optional OTP re-verification for suspicious refreshes
|
||||
- Enhanced logging of suspicious events
|
||||
- **Location:** `src/routes/authRoutes.js` (refresh endpoint)
|
||||
|
||||
### **5. ✅ JWT Claims Validation**
|
||||
**Status:** **RESOLVED** ✅
|
||||
- Strict validation of `iss`, `aud`, `exp`, `iat`, `nbf`
|
||||
- Centralized validation function
|
||||
- Used in all token verification paths
|
||||
- **Location:** `src/services/jwtKeys.js` - `validateTokenClaims()`
|
||||
|
||||
### **6. ✅ CORS Hardening**
|
||||
**Status:** **RESOLVED** ✅
|
||||
- Strict origin whitelisting
|
||||
- No wildcard support when credentials involved
|
||||
- Production requirement for explicit origins
|
||||
- **Location:** `src/index.js`
|
||||
|
||||
### **7. ✅ CSRF Protection (Documented)**
|
||||
**Status:** **RESOLVED** ✅ (Not needed - using Bearer tokens)
|
||||
- Comprehensive documentation provided
|
||||
- Guidance for future cookie-based implementation
|
||||
- **Location:** `CSRF_NOTES.md`
|
||||
|
||||
### **8. ✅ Access Token Replay Mitigation**
|
||||
**Status:** **RESOLVED** ✅ **FIXED!**
|
||||
- ✅ Step-up authentication middleware created (`src/middleware/stepUpAuth.js`)
|
||||
- ✅ Access tokens include `high_assurance` claim after OTP verification
|
||||
- ✅ Middleware checks for recent OTP or high assurance token
|
||||
- ✅ **Step-up auth IS APPLIED to sensitive routes:**
|
||||
- `PUT /users/me` - Line 113 in `src/routes/userRoutes.js` ✅
|
||||
- `DELETE /users/me/devices/:device_id` - Line 181 in `src/routes/userRoutes.js` ✅
|
||||
- `POST /users/me/logout-all-other-devices` - Line 231 in `src/routes/userRoutes.js` ✅
|
||||
- **Risk Level:** 🟢 **LOW** (Previously 🟡 MEDIUM)
|
||||
|
||||
### **11. ✅ Input Validation**
|
||||
**Status:** **RESOLVED** ✅ **FIXED!**
|
||||
- ✅ Input validation middleware created (`src/middleware/validation.js`)
|
||||
- ✅ Validation applied to all auth routes
|
||||
- ✅ **Validation IS APPLIED to user routes:**
|
||||
- `PUT /users/me` - `validateUpdateProfileBody` (Line 114) ✅
|
||||
- `DELETE /users/me/devices/:device_id` - `validateDeviceIdParam` (Line 182) ✅
|
||||
- `POST /users/me/logout-all-other-devices` - `validateLogoutOthersBody` (Line 232) ✅
|
||||
- **Risk Level:** 🟢 **LOW** (Previously 🟢 LOW-MEDIUM)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ **PARTIALLY RESOLVED ISSUES**
|
||||
|
||||
### **9. ⚠️ Secrets Management & Rotation**
|
||||
**Status:** **PARTIALLY RESOLVED** ⚠️
|
||||
|
||||
**What's Done:**
|
||||
- ✅ JWT key rotation structure implemented
|
||||
- ✅ Support for multiple keys with `kid` (key ID)
|
||||
- ✅ Graceful rotation without breaking existing tokens
|
||||
- ✅ Code structure ready for secrets manager integration
|
||||
- ✅ `.env` is in `.gitignore` (verified)
|
||||
|
||||
**What's Missing:**
|
||||
- ❌ **Still reads secrets from environment variables (via `.env` file)**
|
||||
- ❌ No integration with secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.)
|
||||
- ❌ Manual key rotation process (no automation)
|
||||
- ❌ Twilio credentials still in environment variables
|
||||
|
||||
**Risk Level:** 🟡 **MEDIUM-HIGH**
|
||||
|
||||
**Recommendation:**
|
||||
1. **Immediate:** ✅ Ensure `.env` is in `.gitignore` (DONE)
|
||||
2. **Short-term:** Use environment variables from secure deployment platform (not `.env` file)
|
||||
3. **Long-term:** Integrate with secrets manager (see TODOs in `src/services/jwtKeys.js` line 169)
|
||||
|
||||
**TODO in Code:**
|
||||
- `src/services/jwtKeys.js` line 169: Example implementation for AWS Secrets Manager
|
||||
- Need to implement `loadKeysFromSecretsManager()` function
|
||||
|
||||
---
|
||||
|
||||
### **10. ⚠️ Audit Logs Active Monitoring**
|
||||
**Status:** **PARTIALLY RESOLVED** ⚠️
|
||||
|
||||
**What's Done:**
|
||||
- ✅ Enhanced audit logging with risk levels (INFO, SUSPICIOUS, HIGH_RISK)
|
||||
- ✅ Structured logging with metadata
|
||||
- ✅ Anomaly detection helper function (`checkAnomalies()`)
|
||||
- ✅ Suspicious events logged (failed OTPs, suspicious refreshes, blocked IPs)
|
||||
- ✅ **Webhook alerting infrastructure implemented** (`src/services/auditLogger.js`)
|
||||
- ✅ Configurable via `SECURITY_ALERT_WEBHOOK_URL` and `SECURITY_ALERT_MIN_LEVEL`
|
||||
|
||||
**What's Missing:**
|
||||
- ❌ **No active alerting/monitoring integration configured by default**
|
||||
- ❌ Requires manual configuration of `SECURITY_ALERT_WEBHOOK_URL`
|
||||
- ❌ No integration with PagerDuty, Slack, email out of the box
|
||||
- ❌ Anomaly detection only logs to console if webhook not configured
|
||||
|
||||
**Risk Level:** 🟡 **MEDIUM**
|
||||
|
||||
**Recommendation:**
|
||||
1. **Immediate:** Configure `SECURITY_ALERT_WEBHOOK_URL` in production environment
|
||||
2. **Short-term:** Set up log aggregation (CloudWatch, Datadog, etc.)
|
||||
3. **Long-term:** Integrate with SIEM system
|
||||
|
||||
**Configuration:**
|
||||
```bash
|
||||
# Set in production environment
|
||||
SECURITY_ALERT_WEBHOOK_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
|
||||
SECURITY_ALERT_MIN_LEVEL=HIGH_RISK # or SUSPICIOUS for more alerts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ❌ **UNADDRESSED ATTACK SCENARIOS**
|
||||
|
||||
### **12. ⚠️ Database Compromise**
|
||||
**Status:** **PARTIALLY RESOLVED** ⚠️ **FIXED!**
|
||||
|
||||
**What's Done:**
|
||||
- ✅ Field-level encryption for phone numbers implemented (`src/utils/fieldEncryption.js`)
|
||||
- ✅ AES-256-GCM encryption for PII fields
|
||||
- ✅ Automatic encryption/decryption in application layer
|
||||
- ✅ Backward compatibility with existing plaintext data
|
||||
- ✅ Database access logging implemented (`src/middleware/dbAccessLogger.js`)
|
||||
- ✅ All queries to sensitive tables are logged
|
||||
- ✅ User context (IP, user agent, user ID) captured in logs
|
||||
- ✅ Sensitive parameters redacted in logs
|
||||
- ✅ Using parameterized queries (SQL injection protection)
|
||||
- ✅ OTP codes are hashed with bcrypt (not stored in plaintext)
|
||||
- ✅ No passwords stored (phone-based auth)
|
||||
|
||||
**What's Missing:**
|
||||
- ⚠️ **Database-level encryption (TDE) not configured** (infrastructure-level)
|
||||
- ⚠️ Encryption key still in environment variables (should use secrets manager)
|
||||
- ⚠️ No automated key rotation process
|
||||
|
||||
**Risk Level:** 🟡 **MEDIUM** (Previously 🔴 HIGH)
|
||||
|
||||
**Configuration Required:**
|
||||
1. **Enable Field-Level Encryption:**
|
||||
```bash
|
||||
ENCRYPTION_ENABLED=true
|
||||
ENCRYPTION_KEY=<32-byte-base64-key>
|
||||
```
|
||||
|
||||
2. **Enable Database Access Logging:**
|
||||
```bash
|
||||
DB_ACCESS_LOGGING_ENABLED=true
|
||||
DB_ACCESS_LOG_LEVEL=sensitive
|
||||
```
|
||||
|
||||
3. **Enable Database-Level Encryption (TDE):**
|
||||
- Configure at infrastructure level (AWS RDS, Google Cloud SQL, Azure)
|
||||
- See `DATABASE_ENCRYPTION_SETUP.md` for instructions
|
||||
|
||||
**Recommendation:**
|
||||
- ✅ Field-level encryption implemented - **CONFIGURE** `ENCRYPTION_ENABLED=true`
|
||||
- ✅ Database access logging implemented - **CONFIGURE** `DB_ACCESS_LOGGING_ENABLED=true`
|
||||
- ⚠️ Enable TDE at database infrastructure level (see `DATABASE_ENCRYPTION_SETUP.md`)
|
||||
- Move encryption keys to secrets manager
|
||||
- Set up automated key rotation
|
||||
|
||||
---
|
||||
|
||||
### **13. ❌ Man-in-the-Middle (Non-HTTPS)**
|
||||
**Status:** **PARTIALLY ADDRESSED** ⚠️
|
||||
|
||||
**Current Protection:**
|
||||
- ✅ HSTS header set in production (`src/middleware/securityHeaders.js` line 26)
|
||||
- ✅ Server assumes TLS termination in front (reverse proxy)
|
||||
|
||||
**What's Missing:**
|
||||
- ❌ No enforcement of HTTPS-only connections at application level
|
||||
- ❌ No startup validation that HTTPS is configured
|
||||
- ❌ No certificate pinning guidance
|
||||
- ❌ HSTS only applied to admin routes (not all routes)
|
||||
|
||||
**Risk Level:** 🔴 **HIGH** (if misconfigured)
|
||||
|
||||
**Recommendation:**
|
||||
- Enforce HTTPS at reverse proxy/load balancer
|
||||
- Add HSTS headers to all routes (not just admin)
|
||||
- Document TLS requirements
|
||||
- Consider certificate pinning for mobile apps
|
||||
- Add startup validation that HTTPS is configured in production
|
||||
|
||||
---
|
||||
|
||||
### **14. ✅ CORS + XSS**
|
||||
**Status:** **RESOLVED** ✅ **FIXED!**
|
||||
|
||||
**What's Done:**
|
||||
- ✅ CORS hardened with strict origin whitelisting
|
||||
- ✅ Documentation warns about misconfiguration
|
||||
- ✅ Security headers include XSS protection (`X-XSS-Protection`)
|
||||
- ✅ **Startup validation for CORS configuration in production** (`src/utils/corsValidator.js`)
|
||||
- ✅ **Runtime checks for CORS misconfiguration** (suspicious patterns detected)
|
||||
- ✅ **Content Security Policy (CSP) headers implemented** (`src/middleware/securityHeaders.js`)
|
||||
- ✅ **CSP nonce support for dynamic content** (nonce generation and injection)
|
||||
- ✅ **XSS prevention best practices documented** (`XSS_PREVENTION_GUIDE.md`)
|
||||
- ✅ **Security headers applied to all routes** (not just admin routes)
|
||||
- ✅ Additional security headers: `Referrer-Policy`, `Permissions-Policy`
|
||||
|
||||
**Risk Level:** 🟢 **LOW** (Previously 🟡 MEDIUM)
|
||||
|
||||
**Implementation Details:**
|
||||
- **Location:**
|
||||
- `src/utils/corsValidator.js` - CORS validation utilities
|
||||
- `src/middleware/securityHeaders.js` - Enhanced security headers with CSP
|
||||
- `src/index.js` - Startup validation and global security headers
|
||||
- `XSS_PREVENTION_GUIDE.md` - Comprehensive frontend XSS prevention guide
|
||||
|
||||
**Configuration:**
|
||||
- CSP is automatically configured with nonce support
|
||||
- CORS validation runs at startup and fails fast if misconfigured in production
|
||||
- Runtime CORS checks log warnings for suspicious patterns
|
||||
|
||||
---
|
||||
|
||||
## 🔍 **ADDITIONAL VULNERABILITIES FOUND**
|
||||
|
||||
### **15. ⚠️ Error Information Disclosure**
|
||||
**Status:** **GOOD** ✅
|
||||
|
||||
**Current State:**
|
||||
- ✅ Generic error messages returned to users (`"Internal server error"`)
|
||||
- ✅ No stack traces exposed in production
|
||||
- ✅ Detailed errors only logged server-side
|
||||
|
||||
**Recommendation:**
|
||||
- ✅ Current implementation is secure
|
||||
- Consider adding request ID to error responses for debugging (without exposing internals)
|
||||
|
||||
---
|
||||
|
||||
### **16. ⚠️ SQL Injection Protection**
|
||||
**Status:** **GOOD** ✅
|
||||
|
||||
**Current State:**
|
||||
- ✅ All database queries use parameterized queries (`$1, $2, etc.`)
|
||||
- ✅ No string concatenation in SQL queries
|
||||
- ✅ Using PostgreSQL's `pg` library with proper parameterization
|
||||
|
||||
**Recommendation:**
|
||||
- ✅ Current implementation is secure
|
||||
- Continue using parameterized queries for all new code
|
||||
|
||||
---
|
||||
|
||||
### **17. ✅ Security Headers Coverage**
|
||||
**Status:** **RESOLVED** ✅ **FIXED!**
|
||||
|
||||
**Current State:**
|
||||
- ✅ Security headers middleware exists (`src/middleware/securityHeaders.js`)
|
||||
- ✅ **Applied to all routes** (not just admin routes)
|
||||
- ✅ Content Security Policy (CSP) implemented with nonce support
|
||||
- ✅ Referrer-Policy header set (`strict-origin-when-cross-origin`)
|
||||
- ✅ Permissions-Policy header set (restricts browser features)
|
||||
- ✅ All existing headers maintained (X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, HSTS)
|
||||
|
||||
**Risk Level:** 🟢 **LOW** (Previously 🟡 MEDIUM)
|
||||
|
||||
**Note:** CSP currently allows `'unsafe-inline'` and `'unsafe-eval'` for compatibility. Consider tightening in production by using nonces exclusively.
|
||||
|
||||
---
|
||||
|
||||
### **18. ⚠️ Phone Number Validation**
|
||||
**Status:** **GOOD** ✅
|
||||
|
||||
**Current State:**
|
||||
- ✅ Phone numbers validated for E.164 format
|
||||
- ✅ Short codes rejected
|
||||
- ✅ Normalization applied
|
||||
|
||||
**Recommendation:**
|
||||
- ✅ Current implementation is secure
|
||||
- Consider adding country-specific validation if needed
|
||||
|
||||
---
|
||||
|
||||
### **19. ⚠️ Hardcoded Credentials in Docker Compose**
|
||||
**Status:** **VULNERABILITY FOUND** ⚠️
|
||||
|
||||
**Risk:**
|
||||
- Hardcoded database password in `db/farmmarket-db/docker-compose.yml`
|
||||
- Password `password123` is visible in version control
|
||||
- If repository is public or compromised, database credentials are exposed
|
||||
|
||||
**Location:**
|
||||
- `db/farmmarket-db/docker-compose.yml` line 8: `POSTGRES_PASSWORD: password123`
|
||||
|
||||
**Risk Level:** 🟡 **MEDIUM-HIGH**
|
||||
|
||||
**Recommendation:**
|
||||
- Use environment variables for database credentials
|
||||
- Never commit passwords to version control
|
||||
- Use `.env` file (already in `.gitignore`) or secrets manager
|
||||
- Update docker-compose.yml to use environment variables
|
||||
|
||||
**Fix:**
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-farmmarket}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **20. ✅ Phone Number Enumeration**
|
||||
**Status:** **RESOLVED** ✅ **FIXED!**
|
||||
|
||||
**What's Done:**
|
||||
- ✅ OTP request endpoint always returns success (prevents enumeration)
|
||||
- ✅ Generic error messages for OTP verification
|
||||
- ✅ **Constant-time delays implemented for OTP requests** (`src/utils/timingProtection.js`)
|
||||
- ✅ **Constant-time delays implemented for OTP verification**
|
||||
- ✅ **Enumeration detection and monitoring** (`src/utils/enumerationDetection.js`)
|
||||
- ✅ **Enhanced rate limiting for suspicious enumeration patterns**
|
||||
- ✅ **IP blocking for enumeration attempts**
|
||||
|
||||
**Implementation Details:**
|
||||
- **Location:**
|
||||
- `src/utils/timingProtection.js` - Constant-time delay utilities
|
||||
- `src/utils/enumerationDetection.js` - Enumeration detection and monitoring
|
||||
- `src/routes/authRoutes.js` - Timing protection applied to OTP endpoints
|
||||
- `src/middleware/rateLimitMiddleware.js` - Enhanced rate limiting for enumeration
|
||||
|
||||
**Configuration:**
|
||||
```bash
|
||||
# Timing protection delays (milliseconds)
|
||||
OTP_REQUEST_MIN_DELAY=500 # Minimum delay for OTP requests
|
||||
OTP_VERIFY_MIN_DELAY=300 # Minimum delay for OTP verification
|
||||
TIMING_MAX_JITTER=100 # Random jitter to prevent pattern detection
|
||||
|
||||
# Enumeration detection thresholds
|
||||
ENUMERATION_MAX_PHONES_PER_IP_10MIN=5 # Max unique phones per IP in 10 min
|
||||
ENUMERATION_MAX_PHONES_PER_IP_HOUR=20 # Max unique phones per IP in 1 hour
|
||||
ENUMERATION_ALERT_THRESHOLD_10MIN=10 # Alert threshold for 10 min window
|
||||
ENUMERATION_ALERT_THRESHOLD_HOUR=50 # Alert threshold for 1 hour window
|
||||
|
||||
# Stricter rate limits when enumeration detected
|
||||
ENUMERATION_IP_10MIN_LIMIT=2 # Reduced limit for enumeration IPs
|
||||
ENUMERATION_IP_HOUR_LIMIT=5 # Reduced limit for enumeration IPs
|
||||
ENUMERATION_BLOCK_DURATION=3600 # Block duration in seconds (1 hour)
|
||||
```
|
||||
|
||||
**Risk Level:** 🟢 **LOW** (Previously 🟡 MEDIUM)
|
||||
|
||||
**Features:**
|
||||
1. **Constant-Time Delays:** All OTP requests and verifications take similar time regardless of outcome
|
||||
2. **Enumeration Detection:** Tracks unique phone numbers per IP and detects suspicious patterns
|
||||
3. **Automatic Blocking:** IPs with enumeration attempts are automatically blocked
|
||||
4. **Enhanced Monitoring:** All enumeration attempts are logged with risk levels
|
||||
5. **Stricter Rate Limiting:** Reduced limits for IPs with enumeration patterns
|
||||
|
||||
---
|
||||
|
||||
### **21. ⚠️ User Enumeration via Error Messages**
|
||||
**Status:** **GOOD** ✅
|
||||
|
||||
**Current State:**
|
||||
- ✅ Generic error messages ("OTP invalid or expired")
|
||||
- ✅ No distinction between "user not found" and "invalid OTP"
|
||||
- ✅ User creation happens silently (find-or-create pattern)
|
||||
|
||||
**Recommendation:**
|
||||
- ✅ Current implementation is secure
|
||||
- Continue using generic error messages
|
||||
|
||||
---
|
||||
|
||||
### **22. ⚠️ Timing Attack on OTP Verification**
|
||||
**Status:** **PARTIALLY ADDRESSED** ⚠️
|
||||
|
||||
**Current State:**
|
||||
- ✅ Uses bcrypt for OTP hashing (constant-time comparison)
|
||||
- ⚠️ Early returns for expired/max attempts might leak timing information
|
||||
- ⚠️ Database query execution time might differ
|
||||
|
||||
**Risk:**
|
||||
- Attackers could measure response times to determine if OTP exists
|
||||
- Different code paths have different execution times
|
||||
|
||||
**Risk Level:** 🟢 **LOW-MEDIUM**
|
||||
|
||||
**Recommendation:**
|
||||
- Add constant-time delays for all OTP verification paths
|
||||
- Ensure all code paths take similar time regardless of outcome
|
||||
- Consider adding artificial delays to normalize response times
|
||||
|
||||
---
|
||||
|
||||
### **23. ✅ Missing Rate Limiting on User Routes**
|
||||
**Status:** **RESOLVED** ✅
|
||||
|
||||
**Current State:**
|
||||
- ✅ Rate limiting on auth routes (OTP request, verify, refresh, logout)
|
||||
- ✅ Rate limiting on admin routes
|
||||
- ✅ **Rate limiting on user routes:**
|
||||
- `GET /users/me` - Read limit: 100 requests per 15 minutes per user
|
||||
- `PUT /users/me` - Write limit: 20 requests per 15 minutes per user
|
||||
- `GET /users/me/devices` - Read limit: 100 requests per 15 minutes per user
|
||||
- `DELETE /users/me/devices/:device_id` - Sensitive limit: 10 requests per hour per user
|
||||
- `POST /users/me/logout-all-other-devices` - Sensitive limit: 10 requests per hour per user
|
||||
|
||||
**Implementation:**
|
||||
- Created `src/middleware/userRateLimit.js` with three rate limit tiers:
|
||||
- **Read operations**: 100 requests per 15 minutes (configurable via `USER_RATE_LIMIT_READ_MAX` and `USER_RATE_LIMIT_READ_WINDOW`)
|
||||
- **Write operations**: 20 requests per 15 minutes (configurable via `USER_RATE_LIMIT_WRITE_MAX` and `USER_RATE_LIMIT_WRITE_WINDOW`)
|
||||
- **Sensitive operations**: 10 requests per hour (configurable via `USER_RATE_LIMIT_SENSITIVE_MAX` and `USER_RATE_LIMIT_SENSITIVE_WINDOW`)
|
||||
- Per-user rate limiting using user ID from JWT token
|
||||
- Redis-backed with in-memory fallback
|
||||
- Rate limit headers included in responses (`X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, `X-RateLimit-Type`)
|
||||
|
||||
**Risk Level:** 🟢 **RESOLVED**
|
||||
|
||||
**Configuration:**
|
||||
Environment variables available for customization:
|
||||
- `USER_RATE_LIMIT_READ_MAX` (default: 100)
|
||||
- `USER_RATE_LIMIT_READ_WINDOW` (default: 900 seconds = 15 minutes)
|
||||
- `USER_RATE_LIMIT_WRITE_MAX` (default: 20)
|
||||
- `USER_RATE_LIMIT_WRITE_WINDOW` (default: 900 seconds = 15 minutes)
|
||||
- `USER_RATE_LIMIT_SENSITIVE_MAX` (default: 10)
|
||||
- `USER_RATE_LIMIT_SENSITIVE_WINDOW` (default: 3600 seconds = 1 hour)
|
||||
|
||||
---
|
||||
|
||||
### **24. ⚠️ Information Disclosure in Admin Routes**
|
||||
**Status:** **PARTIALLY ADDRESSED** ⚠️
|
||||
|
||||
**Current State:**
|
||||
- ✅ Phone numbers are masked in admin security events endpoint
|
||||
- ✅ Admin routes require authentication and admin role
|
||||
- ⚠️ Admin can see user IDs, IP addresses, device IDs
|
||||
- ⚠️ Full metadata (JSONB) is returned without sanitization
|
||||
|
||||
**Risk:**
|
||||
- Admins have access to sensitive user data
|
||||
- Metadata might contain sensitive information
|
||||
- No audit trail for what admins access
|
||||
|
||||
**Risk Level:** 🟡 **MEDIUM**
|
||||
|
||||
**Recommendation:**
|
||||
- ✅ Current masking is good
|
||||
- Consider additional sanitization of metadata
|
||||
- Add more granular admin permissions
|
||||
- Log all admin data access
|
||||
|
||||
---
|
||||
|
||||
## 📊 **SUMMARY TABLE**
|
||||
|
||||
| Issue | Status | Risk Level | Priority | Notes |
|
||||
|-------|--------|------------|----------|-------|
|
||||
| 1. Rate Limiting | ✅ Resolved | - | - | Fully implemented |
|
||||
| 2. OTP Logging | ✅ Resolved | - | - | Safe logging in place |
|
||||
| 3. IP/Device Risk | ✅ Resolved | - | - | Risk scoring active |
|
||||
| 4. Refresh Token Theft | ✅ Resolved | - | - | Environment fingerprinting |
|
||||
| 5. JWT Claims | ✅ Resolved | - | - | Strict validation |
|
||||
| 6. CORS | ✅ Resolved | - | - | Strict whitelisting |
|
||||
| 7. CSRF | ✅ Documented | - | - | Not needed (Bearer tokens) |
|
||||
| 8. Access Token Replay | ✅ **FIXED** | 🟢 Low | - | Step-up auth applied |
|
||||
| 9. Secrets Management | ⚠️ Partial | 🟡 Medium-High | **HIGH** | Needs secrets manager |
|
||||
| 10. Audit Monitoring | ⚠️ Partial | 🟡 Medium | **MEDIUM** | Needs webhook config |
|
||||
| 11. Input Validation | ✅ **FIXED** | 🟢 Low | - | All routes validated |
|
||||
| 12. Database Compromise | ⚠️ **FIXED** | 🟡 Medium | **LOW** | Needs TDE config |
|
||||
| 13. MITM (HTTP) | ⚠️ Partial | 🔴 High | **MEDIUM** | Needs HTTPS enforcement |
|
||||
| 14. CORS + XSS | ✅ **FIXED** | 🟢 Low | - | Fully implemented |
|
||||
| 15. Error Disclosure | ✅ Good | - | - | No issues found |
|
||||
| 16. SQL Injection | ✅ Good | - | - | Parameterized queries |
|
||||
| 17. Security Headers | ✅ **FIXED** | 🟢 Low | - | Fully implemented |
|
||||
| 18. Phone Validation | ✅ Good | - | - | Proper validation |
|
||||
| 19. Hardcoded Credentials | ⚠️ Found | 🟡 Medium-High | **HIGH** | Docker compose |
|
||||
| 20. Phone Enumeration | ✅ **FIXED** | 🟢 Low | - | Constant-time delays + detection |
|
||||
| 21. User Enumeration | ✅ Good | - | - | Generic errors |
|
||||
| 22. Timing Attacks | ⚠️ Partial | 🟢 Low-Medium | **LOW** | Constant-time delays |
|
||||
| 23. Missing Rate Limits | ✅ **FIXED** | 🟢 Low | - | User routes protected |
|
||||
| 24. Admin Info Disclosure | ⚠️ Partial | 🟡 Medium | **LOW** | Metadata sanitization |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 **IMMEDIATE ACTION ITEMS (Priority Order)**
|
||||
|
||||
### **🔴 HIGH PRIORITY**
|
||||
|
||||
1. **Hardcoded Credentials** (Issue #19) - **NEW!** ⚠️
|
||||
- ❌ Remove hardcoded password from `docker-compose.yml`
|
||||
- ⚠️ Use environment variables for database credentials
|
||||
- ⚠️ Ensure no secrets are committed to version control
|
||||
|
||||
2. **✅ Secrets Management** (Issue #9)
|
||||
- ✅ `.env` is in `.gitignore` (verified)
|
||||
- ⚠️ Move to environment variables from deployment platform (not `.env` file)
|
||||
- ⚠️ Plan integration with secrets manager (AWS Secrets Manager, HashiCorp Vault)
|
||||
|
||||
3. **✅ Step-Up Auth** (Issue #8) - **FIXED!** ✅
|
||||
- ✅ Applied to all sensitive routes
|
||||
|
||||
4. **✅ Input Validation** (Issue #11) - **FIXED!** ✅
|
||||
- ✅ Applied to all user routes
|
||||
|
||||
### **🟡 MEDIUM PRIORITY**
|
||||
|
||||
1. **✅ Phone Number Enumeration** (Issue #20) - **FIXED!** ✅
|
||||
- ✅ Constant-time delays implemented for OTP requests and verification
|
||||
- ✅ Enumeration detection and monitoring implemented
|
||||
- ✅ Enhanced rate limiting for suspicious patterns
|
||||
- ✅ IP blocking for enumeration attempts
|
||||
|
||||
2. **✅ Missing Rate Limiting** (Issue #23) - **FIXED!** ✅
|
||||
- ✅ Rate limiting added to all user routes
|
||||
- ✅ Different limits for read (100/15min), write (20/15min), and sensitive (10/hour) operations
|
||||
- ✅ Per-user rate limits implemented using JWT user ID
|
||||
- ✅ Redis-backed with in-memory fallback
|
||||
- ✅ Rate limit headers included in responses
|
||||
|
||||
3. **Active Monitoring/Alerting** (Issue #10)
|
||||
- Configure `SECURITY_ALERT_WEBHOOK_URL` in production
|
||||
- Set up log aggregation (CloudWatch, Datadog, etc.)
|
||||
- Test alerting with HIGH_RISK events
|
||||
|
||||
4. **HTTPS Enforcement** (Issue #13)
|
||||
- Add startup validation that HTTPS is configured in production
|
||||
- Apply HSTS headers to all routes (not just admin)
|
||||
- Document TLS requirements
|
||||
|
||||
5. **Database Security** (Issue #12) - **FIXED!** ✅
|
||||
- ✅ Field-level encryption implemented - **CONFIGURE** `ENCRYPTION_ENABLED=true`
|
||||
- ✅ Database access logging implemented - **CONFIGURE** `DB_ACCESS_LOGGING_ENABLED=true`
|
||||
- ⚠️ Enable TDE at database infrastructure level (see `DATABASE_ENCRYPTION_SETUP.md`)
|
||||
|
||||
### **🟢 LOW PRIORITY**
|
||||
|
||||
1. **Security Headers Enhancement** (Issue #17) - **PARTIALLY FIXED** ✅
|
||||
- ✅ Security headers applied to all routes
|
||||
- ✅ Content Security Policy (CSP) headers added
|
||||
- ✅ Referrer-Policy and Permissions-Policy headers added
|
||||
- ⚠️ Consider tightening CSP (remove 'unsafe-inline' and 'unsafe-eval' in production)
|
||||
|
||||
2. **Timing Attacks** (Issue #22)
|
||||
- Add constant-time delays for OTP verification
|
||||
- Normalize response times across all code paths
|
||||
|
||||
3. **Admin Info Disclosure** (Issue #24)
|
||||
- Additional sanitization of metadata in admin routes
|
||||
- More granular admin permissions
|
||||
|
||||
4. **CORS Validation** (Issue #14) - **FIXED!** ✅
|
||||
- ✅ Startup validation that CORS origins are configured in production
|
||||
- ✅ XSS prevention best practices documented
|
||||
|
||||
---
|
||||
|
||||
## 📝 **CODE LOCATIONS FOR FIXES**
|
||||
|
||||
### **Secrets Manager Integration**
|
||||
**File:** `src/services/jwtKeys.js`
|
||||
- Line 169: Implement `loadKeysFromSecretsManager()` function
|
||||
- Replace environment variable reads with secrets manager calls
|
||||
|
||||
### **Alerting Configuration**
|
||||
**File:** `src/services/auditLogger.js`
|
||||
- Line 34: `SECURITY_ALERT_WEBHOOK_URL` - Configure in production
|
||||
- Line 35: `SECURITY_ALERT_MIN_LEVEL` - Set to 'HIGH_RISK' or 'SUSPICIOUS'
|
||||
|
||||
### **CORS + XSS Protection (Issue #14) - FIXED!** ✅
|
||||
**Files:**
|
||||
- `src/utils/corsValidator.js` - CORS validation at startup and runtime
|
||||
- `src/middleware/securityHeaders.js` - Enhanced with CSP, nonce support, and additional headers
|
||||
- `src/index.js` - Startup validation and global security headers application
|
||||
- `XSS_PREVENTION_GUIDE.md` - Comprehensive frontend XSS prevention documentation
|
||||
|
||||
### **Security Headers Enhancement (Issue #17) - FIXED!** ✅
|
||||
**Files:**
|
||||
- `src/middleware/securityHeaders.js` - CSP, Referrer-Policy, Permissions-Policy implemented
|
||||
- `src/index.js` - Security headers applied globally to all routes
|
||||
|
||||
### **HTTPS Enforcement**
|
||||
**File:** `src/index.js`
|
||||
- Add startup validation for HTTPS in production
|
||||
- Apply HSTS headers to all routes
|
||||
|
||||
### **Hardcoded Credentials Fix**
|
||||
**File:** `db/farmmarket-db/docker-compose.yml`
|
||||
- Replace hardcoded password with environment variable
|
||||
- Use `${POSTGRES_PASSWORD}` instead of `password123`
|
||||
|
||||
### **Rate Limiting for User Routes**
|
||||
**Files:**
|
||||
- `src/middleware/userRateLimit.js` - Rate limiting middleware with three tiers (read, write, sensitive)
|
||||
- `src/routes/userRoutes.js` - All user routes now have rate limiting applied
|
||||
- ✅ **Implemented:**
|
||||
- Per-user rate limiting using JWT user ID
|
||||
- Different limits for read (100/15min), write (20/15min), and sensitive (10/hour) operations
|
||||
- Redis-backed with in-memory fallback
|
||||
- Rate limit headers in responses
|
||||
- Configurable via environment variables
|
||||
|
||||
---
|
||||
|
||||
## 🎉 **CONCLUSION**
|
||||
|
||||
**Overall Security Posture:** 🟢 **GOOD** (Improved from 🟡 GOOD)
|
||||
|
||||
**Latest Update:**
|
||||
- ✅ **Issue #23 (Missing Rate Limiting on User Routes) - FIXED!**
|
||||
- Rate limiting middleware created with three tiers (read, write, sensitive)
|
||||
- All user routes now protected with per-user rate limits
|
||||
- Redis-backed with in-memory fallback
|
||||
- Configurable via environment variables
|
||||
- ✅ **Issue #12 (Database Compromise) - FIXED!**
|
||||
- Field-level encryption for phone numbers implemented
|
||||
- Database access logging implemented
|
||||
- See `DATABASE_ENCRYPTION_SETUP.md` for configuration instructions
|
||||
|
||||
**Progress:**
|
||||
- **12 out of 14 original issues are fully resolved** ✅ (up from 11)
|
||||
- **2 issues are partially resolved** ⚠️ (need configuration/completion)
|
||||
- **6 new vulnerabilities found** ⚠️ (Issues #19-24)
|
||||
- **0 critical vulnerabilities** 🔴 (down from 2)
|
||||
|
||||
**Key Improvements:**
|
||||
1. ✅ Step-up auth now applied to all sensitive routes
|
||||
2. ✅ Input validation now applied to all user routes
|
||||
3. ✅ Webhook alerting infrastructure ready (needs configuration)
|
||||
4. ✅ **CORS + XSS protection fully implemented** (Issue #14)
|
||||
5. ✅ **Security headers enhanced and applied globally** (Issue #17)
|
||||
6. ✅ **Rate limiting on user routes fully implemented** (Issue #23)
|
||||
|
||||
**Remaining Gaps:**
|
||||
1. **🔴 HIGH:** Hardcoded credentials in docker-compose.yml (Issue #19)
|
||||
2. Secrets management needs secrets manager integration
|
||||
3. Alerting needs webhook URL configuration
|
||||
4. ✅ Database field-level encryption implemented - **NEEDS CONFIGURATION**
|
||||
5. Database TDE needs infrastructure-level setup
|
||||
6. ✅ Phone number enumeration via timing attacks (Issue #20) - **FIXED!**
|
||||
7. HTTPS enforcement needs startup validation
|
||||
|
||||
**Recommendation:** The service is **production-ready** with proper configuration, but should address the HIGH and MEDIUM priority items before handling sensitive production data.
|
||||
|
||||
|
|
@ -1,330 +0,0 @@
|
|||
# Security Hardening Implementation Summary
|
||||
|
||||
## Overview
|
||||
|
||||
This document summarizes the security hardening improvements added to the authentication service. All changes are marked with clear comments in the code for easy identification.
|
||||
|
||||
## 1. OTP Logging Safety ✅
|
||||
|
||||
**Implementation:** `src/utils/otpLogger.js`
|
||||
|
||||
- **Safe OTP logging helper** that only logs in development mode
|
||||
- Never logs OTPs to production or centralized logging systems
|
||||
- Uses `logOtpForDebug()` function that:
|
||||
- Only logs when `NODE_ENV === 'development'`
|
||||
- Masks phone numbers (shows only last 4 digits)
|
||||
- Clearly marked as `[DEV-ONLY]`
|
||||
|
||||
**Updated Files:**
|
||||
- `src/services/smsService.js` - Now uses safe logging instead of direct `console.log`
|
||||
|
||||
**Configuration:**
|
||||
- No configuration needed - automatically detects environment
|
||||
|
||||
## 2. IP/Device Risk Controls ✅
|
||||
|
||||
**Implementation:** `src/services/riskScoring.js`
|
||||
|
||||
- **IP blocking**: Blocks logins from configured CIDR ranges (private IPs, test ranges)
|
||||
- **Risk scoring**: Calculates risk score based on:
|
||||
- IP address changes
|
||||
- Device changes
|
||||
- User agent changes
|
||||
- **Suspicious refresh detection**: Detects when refresh tokens are used from different environments
|
||||
|
||||
**Features:**
|
||||
- Configurable blocked IP ranges via `BLOCKED_IP_RANGES` environment variable
|
||||
- Optional OTP re-verification requirement for suspicious refreshes
|
||||
- Risk scores logged to audit trail
|
||||
|
||||
**Updated Files:**
|
||||
- `src/routes/authRoutes.js` - Integrated IP blocking and risk scoring
|
||||
|
||||
**Configuration:**
|
||||
```bash
|
||||
BLOCKED_IP_RANGES=10.0.0.0/8,172.16.0.0/12 # Comma-separated CIDR blocks
|
||||
REQUIRE_OTP_ON_SUSPICIOUS_REFRESH=true # Require OTP on suspicious refresh
|
||||
```
|
||||
|
||||
## 3. Access Token Replay Mitigation ✅
|
||||
|
||||
**Implementation:** `src/middleware/stepUpAuth.js`
|
||||
|
||||
- **Step-up authentication middleware** for sensitive operations
|
||||
- Requires either:
|
||||
- Recent OTP verification (within configurable window)
|
||||
- High assurance flag in token (set after OTP verification)
|
||||
- Access tokens now include `high_assurance` claim after OTP verification
|
||||
|
||||
**Usage:**
|
||||
```javascript
|
||||
router.post('/users/me/change-phone',
|
||||
authMiddleware,
|
||||
requireRecentOtpOrReauth,
|
||||
async (req, res) => { ... }
|
||||
);
|
||||
```
|
||||
|
||||
**Updated Files:**
|
||||
- `src/services/tokenService.js` - Added `highAssurance` option to `signAccessToken()`
|
||||
- `src/middleware/authMiddleware.js` - Extracts `high_assurance` claim from token
|
||||
- `src/routes/authRoutes.js` - Issues tokens with `highAssurance: true` after OTP verification
|
||||
|
||||
**Configuration:**
|
||||
```bash
|
||||
STEP_UP_OTP_WINDOW_MINUTES=5 # Time window for "recent" OTP (default: 5)
|
||||
```
|
||||
|
||||
## 4. Refresh Token Theft Mitigation ✅
|
||||
|
||||
**Implementation:** Enhanced in `src/routes/authRoutes.js` and `src/services/riskScoring.js`
|
||||
|
||||
- **Environment fingerprinting**: Tracks IP, user-agent, device ID
|
||||
- **Suspicious refresh detection**: Compares current refresh with previous environment
|
||||
- **Optional OTP requirement**: Can require OTP re-verification for suspicious refreshes
|
||||
- **Enhanced logging**: All suspicious refreshes logged with risk scores
|
||||
|
||||
**Features:**
|
||||
- Detects IP changes, device changes, user-agent changes
|
||||
- Calculates risk score (0-100)
|
||||
- Logs suspicious events to audit trail
|
||||
- Optionally blocks suspicious refreshes until OTP verification
|
||||
|
||||
**Configuration:**
|
||||
```bash
|
||||
REQUIRE_OTP_ON_SUSPICIOUS_REFRESH=true # Require OTP on suspicious refresh
|
||||
```
|
||||
|
||||
## 5. JWT Key Rotation & Secrets Management ✅
|
||||
|
||||
**Implementation:** `src/services/jwtKeys.js`
|
||||
|
||||
- **Multiple signing keys** with key IDs (kid) in JWT header
|
||||
- **Active key for signing**: Configurable via `JWT_ACTIVE_KEY_ID`
|
||||
- **Multiple verification keys**: Supports key rotation without breaking existing tokens
|
||||
- **Strict claims validation**: Validates `iss`, `aud`, `exp`, `iat`, `nbf`
|
||||
|
||||
**Key Features:**
|
||||
- Tokens include `kid` in header for key identification
|
||||
- Supports multiple keys for graceful rotation
|
||||
- Validates issuer and audience claims
|
||||
- Backward compatible with legacy single-key setup
|
||||
|
||||
**Updated Files:**
|
||||
- `src/services/tokenService.js` - Uses new key management system
|
||||
- `src/middleware/authMiddleware.js` - Validates tokens with key rotation support
|
||||
|
||||
**Configuration:**
|
||||
```bash
|
||||
JWT_ACTIVE_KEY_ID=1 # Key ID for signing new tokens
|
||||
JWT_KEYS_JSON='{"1":"secret1","2":"secret2"}' # Multiple keys for rotation
|
||||
JWT_ISSUER=farm-auth-service # Issuer claim
|
||||
JWT_AUDIENCE=mobile-app # Audience claim
|
||||
JWT_REFRESH_KEY_ID=1 # Key ID for refresh tokens (optional)
|
||||
```
|
||||
|
||||
**TODO for Production:**
|
||||
- Load keys from secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.)
|
||||
- See comments in `src/services/jwtKeys.js` for implementation example
|
||||
|
||||
## 6. Stricter JWT Claims Validation ✅
|
||||
|
||||
**Implementation:** `src/services/jwtKeys.js` - `validateTokenClaims()`
|
||||
|
||||
- **Issuer (iss) validation**: Ensures tokens are from correct service
|
||||
- **Audience (aud) validation**: Ensures tokens are for correct application
|
||||
- **Expiration (exp) validation**: Already handled, but now with clock skew tolerance
|
||||
- **Issued at (iat) validation**: Prevents tokens issued in the future
|
||||
- **Not before (nbf) validation**: Validates if present
|
||||
|
||||
**Features:**
|
||||
- Configurable clock skew (default: 60 seconds)
|
||||
- Centralized validation function
|
||||
- Used in all token verification paths
|
||||
|
||||
**Configuration:**
|
||||
- Claims values set via `JWT_ISSUER` and `JWT_AUDIENCE` environment variables
|
||||
|
||||
## 7. CORS Hardening ✅
|
||||
|
||||
**Implementation:** `src/index.js`
|
||||
|
||||
- **Strict origin whitelisting**: Only allows explicitly configured origins
|
||||
- **No wildcard support**: Never uses `*` when credentials are involved
|
||||
- **Clear warnings**: Logs when allowing all origins (development only)
|
||||
- **Production requirement**: CORS origins must be configured in production
|
||||
|
||||
**Features:**
|
||||
- Development mode: Allows all origins only if none configured (with warning)
|
||||
- Production mode: Requires explicit origin whitelist
|
||||
- Validates origin on every request
|
||||
- Blocks unauthorized origins with 403
|
||||
|
||||
**Configuration:**
|
||||
```bash
|
||||
CORS_ALLOWED_ORIGINS=https://app.example.com,https://api.example.com
|
||||
```
|
||||
|
||||
**WARNING:** Never use `*` as an allowed origin when credentials or tokens are involved.
|
||||
|
||||
## 8. CSRF Future-Proofing ✅
|
||||
|
||||
**Implementation:** `CSRF_NOTES.md`
|
||||
|
||||
- **Documentation**: Comprehensive notes on CSRF protection
|
||||
- **Current status**: No CSRF protection needed (using Bearer tokens)
|
||||
- **Future guidance**: Implementation strategy if moving to cookies
|
||||
|
||||
**Key Points:**
|
||||
- Current implementation (Bearer tokens) is CSRF-safe
|
||||
- If moving to HTTP-only cookies, CSRF protection becomes mandatory
|
||||
- Recommended: SameSite cookies + CSRF token validation
|
||||
|
||||
## 9. Enhanced Audit Logging ✅
|
||||
|
||||
**Implementation:** `src/services/auditLogger.js`
|
||||
|
||||
- **Risk levels**: INFO, SUSPICIOUS, HIGH_RISK
|
||||
- **Enhanced logging**: All events include risk level
|
||||
- **Anomaly detection**: Helper functions for pattern detection
|
||||
- **Suspicious event logging**:
|
||||
- Multiple failed OTP attempts
|
||||
- Suspicious refresh events
|
||||
- Blocked IP logins
|
||||
|
||||
**Features:**
|
||||
- Automatic `risk_level` column creation in `auth_audit` table
|
||||
- Structured logging with metadata
|
||||
- Easy integration with external alerting systems
|
||||
|
||||
**Updated Files:**
|
||||
- `src/routes/authRoutes.js` - Uses enhanced audit logging throughout
|
||||
|
||||
**Logging Functions:**
|
||||
- `logAuthEvent()` - General auth event logging
|
||||
- `logSuspiciousOtpAttempt()` - Failed OTP attempts
|
||||
- `logBlockedIpLogin()` - Blocked IP logins
|
||||
- `logSuspiciousRefresh()` - Suspicious refresh events
|
||||
- `checkAnomalies()` - Pattern detection (for future alerting)
|
||||
|
||||
**TODO for Production:**
|
||||
- Integrate with external alerting (PagerDuty, Slack, email)
|
||||
- See comments in `src/services/auditLogger.js` for implementation example
|
||||
|
||||
## 10. Input Validation ✅
|
||||
|
||||
**Implementation:** `src/middleware/validation.js`
|
||||
|
||||
- **Centralized validation**: Reusable middleware for all endpoints
|
||||
- **Type checking**: Validates field types
|
||||
- **Length limits**: Prevents DoS via large payloads
|
||||
- **Format validation**: Validates phone numbers, OTP codes, etc.
|
||||
|
||||
**Validation Middleware:**
|
||||
- `validateRequestOtpBody()` - OTP request validation
|
||||
- `validateVerifyOtpBody()` - OTP verification validation
|
||||
- `validateRefreshTokenBody()` - Refresh token validation
|
||||
- `validateLogoutBody()` - Logout validation
|
||||
|
||||
**Features:**
|
||||
- Validates required fields
|
||||
- Validates field types
|
||||
- Validates string lengths
|
||||
- Validates formats (phone, OTP code)
|
||||
- Prevents oversized payloads
|
||||
|
||||
**Updated Files:**
|
||||
- `src/routes/authRoutes.js` - All endpoints use validation middleware
|
||||
|
||||
## Environment Variables Summary
|
||||
|
||||
### Required
|
||||
```bash
|
||||
DATABASE_URL=postgresql://...
|
||||
JWT_ACCESS_SECRET=... # Or use JWT_KEYS_JSON
|
||||
JWT_REFRESH_SECRET=... # Or use JWT_KEYS_JSON
|
||||
```
|
||||
|
||||
### Optional (with defaults)
|
||||
```bash
|
||||
# JWT Configuration
|
||||
JWT_ACTIVE_KEY_ID=1
|
||||
JWT_KEYS_JSON='{"1":"secret1","2":"secret2"}'
|
||||
JWT_ISSUER=farm-auth-service
|
||||
JWT_AUDIENCE=mobile-app
|
||||
JWT_REFRESH_KEY_ID=1
|
||||
|
||||
# Security Hardening
|
||||
BLOCKED_IP_RANGES=10.0.0.0/8,172.16.0.0/12
|
||||
REQUIRE_OTP_ON_SUSPICIOUS_REFRESH=false
|
||||
STEP_UP_OTP_WINDOW_MINUTES=5
|
||||
|
||||
# CORS
|
||||
CORS_ALLOWED_ORIGINS=https://app.example.com
|
||||
|
||||
# Rate Limiting (from previous implementation)
|
||||
OTP_REQ_PHONE_10MIN_LIMIT=3
|
||||
OTP_REQ_PHONE_DAY_LIMIT=10
|
||||
OTP_REQ_IP_10MIN_LIMIT=20
|
||||
OTP_REQ_IP_DAY_LIMIT=100
|
||||
OTP_VERIFY_MAX_ATTEMPTS=5
|
||||
OTP_VERIFY_FAILED_PER_HOUR_LIMIT=10
|
||||
OTP_TTL_SECONDS=120
|
||||
```
|
||||
|
||||
## Code Markers
|
||||
|
||||
All security hardening code is marked with comments:
|
||||
- `// === SECURITY HARDENING: OTP LOGGING ===`
|
||||
- `// === SECURITY HARDENING: IP/DEVICE RISK ===`
|
||||
- `// === SECURITY HARDENING: JWT KEY ROTATION ===`
|
||||
- `// === SECURITY HARDENING: ACCESS TOKEN REPLAY MITIGATION ===`
|
||||
- `// === SECURITY HARDENING: REFRESH TOKEN THEFT MITIGATION ===`
|
||||
- `// === SECURITY HARDENING: AUDIT LOGS & ANOMALY FLAGS ===`
|
||||
- `// === SECURITY HARDENING: INPUT VALIDATION ===`
|
||||
- `// === SECURITY HARDENING: CORS ===`
|
||||
- `// === SECURITY HARDENING: CSRF (FUTURE-PROOFING) ===`
|
||||
|
||||
## Testing Recommendations
|
||||
|
||||
1. **OTP Logging**: Verify OTPs are not logged in production
|
||||
2. **IP Blocking**: Test with blocked IP ranges
|
||||
3. **Risk Scoring**: Test with different IPs/devices
|
||||
4. **JWT Key Rotation**: Test token verification with multiple keys
|
||||
5. **CORS**: Test with allowed and blocked origins
|
||||
6. **Input Validation**: Test with invalid payloads
|
||||
7. **Audit Logging**: Verify risk levels are logged correctly
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Secrets Management**: Integrate with AWS Secrets Manager or HashiCorp Vault
|
||||
2. **Alerting**: Set up alerts for HIGH_RISK events
|
||||
3. **Monitoring**: Monitor audit logs for suspicious patterns
|
||||
4. **Key Rotation**: Implement automated key rotation process
|
||||
5. **CSRF**: If moving to cookies, implement CSRF protection
|
||||
|
||||
## Files Created
|
||||
|
||||
- `src/utils/otpLogger.js` - Safe OTP logging
|
||||
- `src/services/riskScoring.js` - IP/device risk scoring
|
||||
- `src/services/jwtKeys.js` - JWT key management
|
||||
- `src/middleware/validation.js` - Input validation
|
||||
- `src/services/auditLogger.js` - Enhanced audit logging
|
||||
- `src/middleware/stepUpAuth.js` - Step-up authentication
|
||||
- `CSRF_NOTES.md` - CSRF protection documentation
|
||||
- `SECURITY_HARDENING_SUMMARY.md` - This file
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `src/services/smsService.js` - Safe OTP logging
|
||||
- `src/services/tokenService.js` - JWT key rotation, claims validation
|
||||
- `src/middleware/authMiddleware.js` - JWT key rotation support
|
||||
- `src/routes/authRoutes.js` - All security features integrated
|
||||
- `src/index.js` - CORS hardening
|
||||
- `src/config.js` - New environment variables documented
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,299 +0,0 @@
|
|||
# Phone Number & Device Management - Security Scenarios
|
||||
|
||||
## Current System Behavior
|
||||
|
||||
### Scenario 1: Two Different Users with Same Phone Number
|
||||
|
||||
**Current Behavior:**
|
||||
- ❌ **SECURITY ISSUE**: Both users will access the **SAME account**
|
||||
- The system uses "find or create" logic:
|
||||
- If phone number exists → Logs into existing account
|
||||
- If phone number doesn't exist → Creates new account
|
||||
- The second person can:
|
||||
- See the first person's data
|
||||
- Modify the first person's profile
|
||||
- Access their listings/transactions
|
||||
- Change their account settings
|
||||
|
||||
**Why this happens:**
|
||||
```javascript
|
||||
// src/routes/authRoutes.js (lines 88-107)
|
||||
// find or create user
|
||||
const found = await db.query(`SELECT ... FROM users WHERE phone_number = $1`, [phone]);
|
||||
if (found.rows.length === 0) {
|
||||
// Create new user
|
||||
} else {
|
||||
user = found.rows[0]; // ← Uses existing account!
|
||||
}
|
||||
```
|
||||
|
||||
**Database Constraint:**
|
||||
- `phone_number` has UNIQUE constraint (one account per phone number)
|
||||
|
||||
---
|
||||
|
||||
### Scenario 2: Same User, Different Device
|
||||
|
||||
**Current Behavior:**
|
||||
- ✅ **Works correctly**: Same user can log in from multiple devices
|
||||
- Each device gets:
|
||||
- Its own refresh token (tracked by `device_id`)
|
||||
- Separate device record in `user_devices` table
|
||||
- Independent session
|
||||
- All devices can be logged in **simultaneously**
|
||||
- Each device's tokens work independently
|
||||
|
||||
**How it works:**
|
||||
```javascript
|
||||
// Device tracking (lines 117-142)
|
||||
INSERT INTO user_devices (user_id, device_identifier, ...)
|
||||
VALUES ($1, $2, ...)
|
||||
ON CONFLICT (user_id, device_identifier)
|
||||
DO UPDATE SET last_seen_at = NOW(), ...
|
||||
```
|
||||
|
||||
**Database Constraint:**
|
||||
- `UNIQUE (user_id, device_identifier)` → Same device can't be registered twice for same user
|
||||
|
||||
**Example:**
|
||||
1. User logs in on Phone A → Gets tokens, device_id = "phone-a"
|
||||
2. User logs in on Phone B → Gets different tokens, device_id = "phone-b"
|
||||
3. Both devices active simultaneously
|
||||
4. Logout on Phone A → Only revokes tokens for device_id = "phone-a"
|
||||
5. Phone B continues working
|
||||
|
||||
---
|
||||
|
||||
## Security Risks
|
||||
|
||||
### Risk 1: Phone Number Hijacking
|
||||
|
||||
**Problem:**
|
||||
- If someone gets access to your phone number (SIM swap, lost phone), they can log into your account
|
||||
- They receive the OTP and gain full account access
|
||||
|
||||
**Mitigation (Recommended):**
|
||||
1. Add additional verification (email, recovery questions)
|
||||
2. Implement device fingerprinting
|
||||
3. Alert user on new device login
|
||||
4. Allow user to see all active devices and revoke them
|
||||
|
||||
### Risk 2: Shared Phone Numbers
|
||||
|
||||
**Problem:**
|
||||
- Family members sharing a phone
|
||||
- Business phone used by multiple employees
|
||||
- Second-hand phone numbers
|
||||
|
||||
**Current Impact:**
|
||||
- Account confusion
|
||||
- Data privacy violations
|
||||
- Unauthorized access
|
||||
|
||||
---
|
||||
|
||||
## Recommended Solutions
|
||||
|
||||
### Solution 1: Warn User on First Login from New Device
|
||||
|
||||
**Implementation:**
|
||||
```javascript
|
||||
// In verify-otp endpoint
|
||||
const existingDevices = await db.query(
|
||||
`SELECT COUNT(*) FROM user_devices WHERE user_id = $1`,
|
||||
[user.id]
|
||||
);
|
||||
|
||||
if (existingDevices.rows[0].count > 0) {
|
||||
// This is a new device for existing account
|
||||
// Could send notification to all other devices
|
||||
// Or require additional verification
|
||||
}
|
||||
```
|
||||
|
||||
### Solution 2: Multi-Factor Authentication (MFA)
|
||||
|
||||
**Add options:**
|
||||
- Email verification for new device
|
||||
- SMS backup codes
|
||||
- Recovery questions
|
||||
- Authenticator app
|
||||
|
||||
### Solution 3: Account Ownership Verification
|
||||
|
||||
**Before creating account:**
|
||||
```javascript
|
||||
// Check if phone number recently used
|
||||
const recentLogins = await db.query(
|
||||
`SELECT user_id, last_login_at
|
||||
FROM users
|
||||
WHERE phone_number = $1
|
||||
AND last_login_at > NOW() - INTERVAL '7 days'`,
|
||||
[phoneNumber]
|
||||
);
|
||||
|
||||
if (recentLogins.rows.length > 0) {
|
||||
// Require additional verification or block
|
||||
}
|
||||
```
|
||||
|
||||
### Solution 4: Device Management Endpoint
|
||||
|
||||
**Add API endpoints:**
|
||||
```javascript
|
||||
// GET /users/me/devices - List all active devices
|
||||
// DELETE /users/me/devices/:device_id - Revoke specific device
|
||||
// POST /users/me/devices/:device_id/verify - Verify device ownership
|
||||
```
|
||||
|
||||
### Solution 5: Session Limits
|
||||
|
||||
**Limit concurrent sessions:**
|
||||
```javascript
|
||||
// Enforce maximum devices per user
|
||||
const MAX_DEVICES = 5;
|
||||
const deviceCount = await db.query(
|
||||
`SELECT COUNT(*) FROM user_devices WHERE user_id = $1 AND is_active = true`,
|
||||
[user.id]
|
||||
);
|
||||
|
||||
if (deviceCount >= MAX_DEVICES) {
|
||||
// Revoke oldest device or require user to choose which to revoke
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Immediate Actions Needed
|
||||
|
||||
### 1. Document Current Behavior
|
||||
- Update API documentation
|
||||
- Warn developers about phone number uniqueness
|
||||
- Add to user terms of service
|
||||
|
||||
### 2. Add Logging
|
||||
```javascript
|
||||
// Log all login attempts
|
||||
await db.query(
|
||||
`INSERT INTO auth_audit (user_id, action, status, device_id, ip_address)
|
||||
VALUES ($1, 'login', 'success', $2, $3)`,
|
||||
[user.id, devId, req.ip]
|
||||
);
|
||||
```
|
||||
|
||||
### 3. Add User Notifications
|
||||
- Email/SMS alert when login from new device
|
||||
- Show active devices in user profile
|
||||
- Allow device revocation
|
||||
|
||||
### 4. Consider Account Recovery Flow
|
||||
- Allow users to dispute account ownership
|
||||
- Support team can transfer ownership
|
||||
- Require additional verification for sensitive actions
|
||||
|
||||
---
|
||||
|
||||
## Testing Scenarios
|
||||
|
||||
### Test Case 1: Same Phone, Different Users
|
||||
```
|
||||
1. User A requests OTP for +919876543210
|
||||
2. User A verifies OTP → Account created
|
||||
3. User B requests OTP for +919876543210
|
||||
4. User B verifies OTP → Logs into User A's account ❌
|
||||
```
|
||||
|
||||
### Test Case 2: Same User, Multiple Devices
|
||||
```
|
||||
1. User logs in on Device A → Gets tokens
|
||||
2. User logs in on Device B → Gets different tokens
|
||||
3. Both tokens work simultaneously ✅
|
||||
4. User logs out on Device A → Device B still works ✅
|
||||
```
|
||||
|
||||
### Test Case 3: Device Re-login
|
||||
```
|
||||
1. User logs in on Device A
|
||||
2. User logs out
|
||||
3. User logs in again on Device A → New tokens issued ✅
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices for Mobile App
|
||||
|
||||
### 1. Inform Users
|
||||
```kotlin
|
||||
// Show warning in app
|
||||
if (response.needs_profile && existingAccount) {
|
||||
showDialog("This phone number is already registered.
|
||||
You will be logged into the existing account.")
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Show Active Devices
|
||||
```kotlin
|
||||
// Display all logged-in devices
|
||||
GET /users/me/devices → List devices with last_seen_at
|
||||
```
|
||||
|
||||
### 3. Allow Device Management
|
||||
```kotlin
|
||||
// Let users revoke devices
|
||||
DELETE /users/me/devices/{device_id}
|
||||
```
|
||||
|
||||
### 4. Handle Token Revocation
|
||||
```kotlin
|
||||
// If refresh returns 401, check if device was revoked
|
||||
if (error.code == 401) {
|
||||
checkActiveDevices()
|
||||
if (currentDeviceRevoked) {
|
||||
forceReLogin()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Queries for Analysis
|
||||
|
||||
### Check accounts by phone
|
||||
```sql
|
||||
SELECT phone_number, COUNT(*) as account_count
|
||||
FROM users
|
||||
GROUP BY phone_number
|
||||
HAVING COUNT(*) > 1;
|
||||
-- Should return 0 rows (UNIQUE constraint)
|
||||
```
|
||||
|
||||
### Check devices per user
|
||||
```sql
|
||||
SELECT u.phone_number, COUNT(ud.id) as device_count
|
||||
FROM users u
|
||||
LEFT JOIN user_devices ud ON u.id = ud.user_id
|
||||
WHERE ud.is_active = true
|
||||
GROUP BY u.id, u.phone_number
|
||||
ORDER BY device_count DESC;
|
||||
```
|
||||
|
||||
### Check concurrent sessions
|
||||
```sql
|
||||
SELECT u.phone_number, ud.device_identifier, ud.last_seen_at
|
||||
FROM users u
|
||||
JOIN user_devices ud ON u.id = ud.user_id
|
||||
WHERE ud.is_active = true
|
||||
AND ud.last_seen_at > NOW() - INTERVAL '1 hour'
|
||||
ORDER BY ud.last_seen_at DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Scenario | Current Behavior | Risk Level | Action Needed |
|
||||
|----------|-----------------|------------|---------------|
|
||||
| Same phone, different users | Share same account | 🔴 HIGH | Add verification/alert |
|
||||
| Same user, different devices | Multiple active sessions | 🟢 LOW | Add device management UI |
|
||||
| Same device, multiple logins | Works (token refresh) | 🟢 LOW | None |
|
||||
| Phone number hijacking | Full account access | 🔴 HIGH | Add MFA, alerts |
|
||||
100
SETUP.md
100
SETUP.md
|
|
@ -1,100 +0,0 @@
|
|||
# Environment Variables Setup
|
||||
|
||||
## Required Variables (MUST provide)
|
||||
|
||||
These are **mandatory** - the service will not start without them:
|
||||
|
||||
```env
|
||||
DATABASE_URL=postgres://username:password@localhost:5432/database_name
|
||||
JWT_ACCESS_SECRET=your-secret-here-minimum-32-characters
|
||||
JWT_REFRESH_SECRET=your-secret-here-minimum-32-characters
|
||||
```
|
||||
|
||||
### How to generate JWT secrets:
|
||||
|
||||
```bash
|
||||
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||
```
|
||||
|
||||
Run this twice to get two different secrets.
|
||||
|
||||
---
|
||||
|
||||
## Optional Variables (Can skip)
|
||||
|
||||
### Twilio SMS Configuration
|
||||
|
||||
**You DO NOT need to provide Twilio credentials** - the service will work without them!
|
||||
|
||||
If Twilio is **NOT configured**:
|
||||
- ✅ Service starts normally
|
||||
- ✅ OTP codes are logged to console for testing
|
||||
- ⚠️ SMS will not be sent (OTP shown in server logs)
|
||||
|
||||
If Twilio **IS configured**:
|
||||
- ✅ OTP codes sent via SMS automatically
|
||||
|
||||
```env
|
||||
# Twilio (Optional - only if you want SMS delivery)
|
||||
TWILIO_ACCOUNT_SID=your-twilio-account-sid
|
||||
TWILIO_AUTH_TOKEN=your-twilio-auth-token
|
||||
TWILIO_MESSAGING_SERVICE_SID=your-messaging-service-sid
|
||||
# OR
|
||||
TWILIO_FROM_NUMBER=+1234567890
|
||||
```
|
||||
|
||||
### Other Optional Variables
|
||||
|
||||
```env
|
||||
PORT=3000 # Server port (default: 3000)
|
||||
NODE_ENV=development # Environment (development/production)
|
||||
CORS_ALLOWED_ORIGINS= # Comma-separated origins (required in production)
|
||||
JWT_ACCESS_TTL=15m # Access token expiry (default: 15m)
|
||||
JWT_REFRESH_TTL=7d # Refresh token expiry (default: 7d)
|
||||
REFRESH_MAX_IDLE_MINUTES=4320 # Refresh token inactivity timeout (default: 3 days)
|
||||
OTP_MAX_ATTEMPTS=5 # Max OTP verification attempts (default: 5)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Setup
|
||||
|
||||
1. **Copy the example file:**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. **Fill in REQUIRED variables only:**
|
||||
```env
|
||||
DATABASE_URL=postgres://postgres:password123@localhost:5433/farmmarket
|
||||
JWT_ACCESS_SECRET=<generate-with-node-command>
|
||||
JWT_REFRESH_SECRET=<generate-with-node-command>
|
||||
```
|
||||
|
||||
3. **Skip Twilio** (optional - for development, OTP will show in console)
|
||||
|
||||
4. **Start the service:**
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Without Twilio
|
||||
|
||||
When Twilio is not configured:
|
||||
- Request OTP: `POST /auth/request-otp`
|
||||
- Check server console - OTP code will be logged: `📱 DEBUG OTP: +919876543210 Code: 123456`
|
||||
- Use that code to verify: `POST /auth/verify-otp`
|
||||
|
||||
This is perfect for local development!
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,294 +0,0 @@
|
|||
# 🛡️ Timing Attack Protection - Implementation Summary
|
||||
|
||||
## ✅ Status: **RESOLVED**
|
||||
|
||||
All timing attack vulnerabilities in OTP verification have been addressed with constant-time execution paths.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Problem Identified
|
||||
|
||||
### Original Vulnerabilities
|
||||
|
||||
The `verifyOtp()` function had **early returns** that leaked timing information:
|
||||
|
||||
1. **OTP Not Found** (Line 129-131)
|
||||
- ❌ Early return without `bcrypt.compare()`
|
||||
- ⚠️ Very fast response time
|
||||
- 🎯 Attackers could detect non-existent OTPs
|
||||
|
||||
2. **OTP Expired** (Line 136-139)
|
||||
- ❌ Early return without `bcrypt.compare()`
|
||||
- ⚠️ Medium response time (DB DELETE only)
|
||||
- 🎯 Attackers could detect expired OTPs
|
||||
|
||||
3. **Max Attempts Exceeded** (Line 143-146)
|
||||
- ❌ Early return without `bcrypt.compare()`
|
||||
- ⚠️ Medium response time (DB DELETE only)
|
||||
- 🎯 Attackers could detect max attempts state
|
||||
|
||||
4. **Invalid Code** (Line 148-159)
|
||||
- ✅ Performs `bcrypt.compare()` + UPDATE
|
||||
- ⚠️ Slow response time
|
||||
- 🎯 Different timing from other failure modes
|
||||
|
||||
5. **Valid Code** (Line 148-166)
|
||||
- ✅ Performs `bcrypt.compare()` + DELETE
|
||||
- ⚠️ Slow response time
|
||||
- 🎯 Different timing from other failure modes
|
||||
|
||||
### Attack Vector
|
||||
|
||||
Attackers could measure response times to determine:
|
||||
- ✅ Whether an OTP exists for a phone number
|
||||
- ✅ Whether an OTP is expired
|
||||
- ✅ Whether max attempts have been reached
|
||||
- ✅ Whether a code is invalid vs. expired
|
||||
|
||||
**Risk Level:** 🟡 **LOW-MEDIUM** → Now **🟢 LOW** (mitigated)
|
||||
|
||||
---
|
||||
|
||||
## ✅ Solution Implemented
|
||||
|
||||
### Constant-Time Execution Paths
|
||||
|
||||
**File:** `src/services/otpService.js`
|
||||
|
||||
#### Key Changes:
|
||||
|
||||
1. **Always Perform bcrypt.compare()**
|
||||
- ✅ `bcrypt.compare()` now executes for ALL code paths
|
||||
- ✅ Even when OTP is expired or max attempts exceeded
|
||||
- ✅ Even when OTP not found (uses dummy hash)
|
||||
|
||||
2. **Dummy Hash for "Not Found" Case**
|
||||
- ✅ Pre-computed dummy hash generated once at module load
|
||||
- ✅ Used when OTP not found to maintain constant time
|
||||
- ✅ `getDummyOtpHash()` function caches the hash
|
||||
|
||||
3. **Deferred Result Evaluation**
|
||||
- ✅ Check expiration/attempts status BEFORE `bcrypt.compare()`
|
||||
- ✅ Perform `bcrypt.compare()` regardless of status
|
||||
- ✅ Evaluate result AFTER constant-time comparison
|
||||
|
||||
4. **Timing Protection Wrapper**
|
||||
- ✅ `executeOtpVerifyWithTiming()` ensures minimum delay
|
||||
- ✅ Configurable via `OTP_VERIFY_MIN_DELAY` env var (default: 300ms)
|
||||
- ✅ Adds random jitter to prevent pattern detection
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Implementation Details
|
||||
|
||||
### Code Flow (New)
|
||||
|
||||
```
|
||||
1. Query database for OTP
|
||||
↓
|
||||
2. If not found:
|
||||
- Use dummy hash
|
||||
- Set isNotFound = true
|
||||
↓
|
||||
3. If found:
|
||||
- Check expiration (set isExpired flag)
|
||||
- Check max attempts (set isMaxAttempts flag)
|
||||
- Use actual hash
|
||||
↓
|
||||
4. ALWAYS perform bcrypt.compare() ← CONSTANT TIME
|
||||
↓
|
||||
5. Evaluate result based on flags:
|
||||
- not_found → return error
|
||||
- expired → delete + return error
|
||||
- max_attempts → delete + return error
|
||||
- invalid → update attempts + return error
|
||||
- valid → delete + return success
|
||||
```
|
||||
|
||||
### Key Functions
|
||||
|
||||
#### `getDummyOtpHash()`
|
||||
```javascript
|
||||
// Pre-computed dummy hash for constant-time comparison
|
||||
// Generated once at module load to avoid performance impact
|
||||
async function getDummyOtpHash() {
|
||||
if (!dummyOtpHash) {
|
||||
const dummyCode = 'DUMMY_OTP_' + Math.random().toString(36) + Date.now();
|
||||
dummyOtpHash = await bcrypt.hash(dummyCode, 10);
|
||||
}
|
||||
return dummyOtpHash;
|
||||
}
|
||||
```
|
||||
|
||||
#### `verifyOtp()` - Refactored
|
||||
```javascript
|
||||
// Always performs bcrypt.compare() regardless of outcome
|
||||
// Uses dummy hash for "not found" case
|
||||
// Defers result evaluation until after constant-time comparison
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Timing Normalization
|
||||
|
||||
### Before (Vulnerable)
|
||||
|
||||
| Scenario | Execution Time | bcrypt.compare() | Timing Leak |
|
||||
|----------|---------------|------------------|-------------|
|
||||
| Not Found | ~5ms | ❌ No | 🟡 High |
|
||||
| Expired | ~15ms | ❌ No | 🟡 Medium |
|
||||
| Max Attempts | ~15ms | ❌ No | 🟡 Medium |
|
||||
| Invalid Code | ~150ms | ✅ Yes | 🟢 Low |
|
||||
| Valid Code | ~150ms | ✅ Yes | 🟢 Low |
|
||||
|
||||
### After (Protected)
|
||||
|
||||
| Scenario | Execution Time | bcrypt.compare() | Timing Leak |
|
||||
|----------|---------------|------------------|-------------|
|
||||
| Not Found | ~150ms + delay | ✅ Yes (dummy) | 🟢 None |
|
||||
| Expired | ~150ms + delay | ✅ Yes | 🟢 None |
|
||||
| Max Attempts | ~150ms + delay | ✅ Yes | 🟢 None |
|
||||
| Invalid Code | ~150ms + delay | ✅ Yes | 🟢 None |
|
||||
| Valid Code | ~150ms + delay | ✅ Yes | 🟢 None |
|
||||
|
||||
**All paths now take similar time** (~150ms + configurable delay + jitter)
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# Minimum delay for OTP verification (ms)
|
||||
# Ensures all verification attempts take at least this long
|
||||
OTP_VERIFY_MIN_DELAY=300
|
||||
|
||||
# Maximum random jitter to add (ms)
|
||||
# Adds randomness to prevent pattern detection
|
||||
TIMING_MAX_JITTER=100
|
||||
```
|
||||
|
||||
### Default Values
|
||||
|
||||
- `OTP_VERIFY_MIN_DELAY`: 300ms
|
||||
- `TIMING_MAX_JITTER`: 100ms
|
||||
|
||||
**Total minimum time:** ~150ms (bcrypt) + 300ms (delay) + 0-100ms (jitter) = **450-550ms**
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Manual Timing Test
|
||||
|
||||
```bash
|
||||
# Test 1: Non-existent OTP
|
||||
time curl -X POST http://localhost:3000/auth/verify-otp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"phone_number": "+1234567890", "code": "000000"}'
|
||||
|
||||
# Test 2: Expired OTP (wait 2+ minutes after requesting)
|
||||
time curl -X POST http://localhost:3000/auth/verify-otp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"phone_number": "+1234567890", "code": "123456"}'
|
||||
|
||||
# Test 3: Invalid Code
|
||||
time curl -X POST http://localhost:3000/auth/verify-otp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"phone_number": "+1234567890", "code": "000000"}'
|
||||
|
||||
# All should take similar time (~450-550ms)
|
||||
```
|
||||
|
||||
### Expected Results
|
||||
|
||||
- ✅ All responses take similar time (within ~50ms variance)
|
||||
- ✅ No timing differences between failure modes
|
||||
- ✅ Consistent response times regardless of outcome
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Security Benefits
|
||||
|
||||
### Attack Prevention
|
||||
|
||||
1. **OTP Enumeration Prevention**
|
||||
- ✅ Attackers cannot determine if OTP exists
|
||||
- ✅ All responses take similar time
|
||||
|
||||
2. **State Leakage Prevention**
|
||||
- ✅ Attackers cannot detect expiration
|
||||
- ✅ Attackers cannot detect max attempts
|
||||
|
||||
3. **Pattern Detection Prevention**
|
||||
- ✅ Random jitter prevents pattern analysis
|
||||
- ✅ Consistent timing across all scenarios
|
||||
|
||||
### Defense in Depth
|
||||
|
||||
- ✅ **Layer 1:** Constant-time `bcrypt.compare()` execution
|
||||
- ✅ **Layer 2:** Minimum delay enforcement
|
||||
- ✅ **Layer 3:** Random jitter addition
|
||||
- ✅ **Layer 4:** Generic error messages (no information leakage)
|
||||
|
||||
---
|
||||
|
||||
## 📝 Files Modified
|
||||
|
||||
### `src/services/otpService.js`
|
||||
- ✅ Refactored `verifyOtp()` function
|
||||
- ✅ Added `getDummyOtpHash()` helper
|
||||
- ✅ Pre-computed dummy hash for constant-time comparison
|
||||
- ✅ Deferred result evaluation after `bcrypt.compare()`
|
||||
|
||||
### `src/utils/timingProtection.js`
|
||||
- ✅ Already implemented (no changes needed)
|
||||
- ✅ `executeOtpVerifyWithTiming()` wrapper
|
||||
- ✅ Configurable delays and jitter
|
||||
|
||||
### `src/routes/authRoutes.js`
|
||||
- ✅ Already using `executeOtpVerifyWithTiming()` wrapper
|
||||
- ✅ No changes needed
|
||||
|
||||
---
|
||||
|
||||
## ✅ Verification Checklist
|
||||
|
||||
- [x] `bcrypt.compare()` always executes
|
||||
- [x] Dummy hash used for "not found" case
|
||||
- [x] Expiration check deferred until after comparison
|
||||
- [x] Max attempts check deferred until after comparison
|
||||
- [x] All code paths take similar time
|
||||
- [x] Timing protection wrapper in place
|
||||
- [x] Configurable delays via env vars
|
||||
- [x] Random jitter added
|
||||
- [x] Generic error messages maintained
|
||||
- [x] No information leakage in responses
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Summary
|
||||
|
||||
**Status:** ✅ **RESOLVED**
|
||||
|
||||
All timing attack vulnerabilities have been mitigated through:
|
||||
|
||||
1. ✅ Constant-time `bcrypt.compare()` execution
|
||||
2. ✅ Dummy hash for "not found" cases
|
||||
3. ✅ Deferred result evaluation
|
||||
4. ✅ Minimum delay enforcement
|
||||
5. ✅ Random jitter addition
|
||||
|
||||
**Risk Level:** 🟡 **LOW-MEDIUM** → 🟢 **LOW** (mitigated)
|
||||
|
||||
The OTP verification system is now **resistant to timing-based attacks**.
|
||||
|
||||
---
|
||||
|
||||
## 📚 References
|
||||
|
||||
- [OWASP: Timing Attack](https://owasp.org/www-community/attacks/Timing_attack)
|
||||
- [bcrypt: Constant-Time Comparison](https://github.com/kelektiv/node.bcrypt.js)
|
||||
- [Node.js Security Best Practices](https://nodejs.org/en/docs/guides/security/)
|
||||
|
||||
201
TWILIO_SETUP.md
201
TWILIO_SETUP.md
|
|
@ -1,201 +0,0 @@
|
|||
# Twilio SMS Setup
|
||||
|
||||
## Required Twilio Variables
|
||||
|
||||
Add these to your `.env` file:
|
||||
|
||||
```env
|
||||
# Twilio SMS Configuration
|
||||
TWILIO_ACCOUNT_SID=your-account-sid-here
|
||||
TWILIO_AUTH_TOKEN=your-auth-token-here
|
||||
|
||||
# Use EITHER Messaging Service (recommended) OR From Number
|
||||
TWILIO_MESSAGING_SERVICE_SID=your-messaging-service-sid
|
||||
# OR
|
||||
TWILIO_FROM_NUMBER=+1234567890
|
||||
```
|
||||
|
||||
## Twilio Setup Options
|
||||
|
||||
### Option 1: Using Messaging Service (Recommended)
|
||||
```env
|
||||
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
TWILIO_AUTH_TOKEN=your_auth_token_here
|
||||
TWILIO_MESSAGING_SERVICE_SID=MGxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
### Option 2: Using Phone Number
|
||||
```env
|
||||
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
TWILIO_AUTH_TOKEN=your_auth_token_here
|
||||
TWILIO_FROM_NUMBER=+1234567890
|
||||
```
|
||||
|
||||
## How to Get Twilio Credentials
|
||||
|
||||
1. Sign up at https://www.twilio.com/
|
||||
2. Get your Account SID and Auth Token from the Twilio Console Dashboard
|
||||
3. For Messaging Service:
|
||||
- Go to Messaging > Services in Twilio Console
|
||||
- Create or select a Messaging Service
|
||||
- Copy the Service SID (starts with MG)
|
||||
4. For Phone Number:
|
||||
- Get a Twilio phone number from Phone Numbers section
|
||||
- Format: +1234567890 (E.164 format)
|
||||
|
||||
## Important Notes
|
||||
|
||||
- Use **Messaging Service** (Option 1) if you have one - it's recommended
|
||||
- Use **Phone Number** (Option 2) if you don't have a Messaging Service
|
||||
- You only need **ONE** of: `TWILIO_MESSAGING_SERVICE_SID` or `TWILIO_FROM_NUMBER`
|
||||
- The phone number must be in E.164 format: `+[country code][number]`
|
||||
- Example: `+919876543210` (India)
|
||||
|
||||
## Testing
|
||||
|
||||
After adding Twilio credentials:
|
||||
1. Restart your server: `npm run dev`
|
||||
2. Request OTP: `POST /auth/request-otp`
|
||||
3. Check server logs - should see: `✅ Twilio SMS sent, SID: SMxxxxx`
|
||||
4. User receives SMS with OTP code
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting Common Errors
|
||||
|
||||
### Error: "Short Code" Error
|
||||
|
||||
**Error Message:**
|
||||
```
|
||||
❌ Failed to send SMS via Twilio: 'To' number cannot be a Short Code: +9174114XXXX
|
||||
```
|
||||
|
||||
**Cause:**
|
||||
- You're trying to send SMS to a short code (5-6 digit number)
|
||||
- Twilio doesn't allow sending to short codes
|
||||
- Often happens with test numbers or invalid phone numbers
|
||||
|
||||
**Solution:**
|
||||
- Use valid full-length phone numbers in E.164 format (e.g., `+919876543210`)
|
||||
- Short codes are typically 5-6 digits total
|
||||
- The service now validates phone numbers and rejects short codes
|
||||
|
||||
**Fallback:**
|
||||
- OTP is automatically logged to console: `📱 DEBUG OTP (fallback): +91741147986 Code: 153502`
|
||||
- Check server logs for the OTP code during development/testing
|
||||
|
||||
---
|
||||
|
||||
### Error: "Unverified Number" Error (Trial Account)
|
||||
|
||||
**Error Message:**
|
||||
```
|
||||
❌ Failed to send SMS via Twilio: The number +91741114XXXX is unverified. Trial accounts cannot send messages to unverified numbers
|
||||
```
|
||||
|
||||
**Cause:**
|
||||
- You're using a **Twilio Trial Account**
|
||||
- Trial accounts can only send SMS to verified phone numbers
|
||||
- This is a security feature to prevent abuse
|
||||
|
||||
**Solutions:**
|
||||
|
||||
**Option 1: Verify the Phone Number (Free)**
|
||||
1. Go to [Twilio Console - Verified Numbers](https://console.twilio.com/us1/develop/phone-numbers/manage/verified)
|
||||
2. Click "Add a new number"
|
||||
3. Enter the phone number and verify via SMS/call
|
||||
4. You can verify up to 10 numbers on a trial account
|
||||
|
||||
**Option 2: Upgrade to Paid Account (Recommended for Production)**
|
||||
1. Add payment method in Twilio Console
|
||||
2. Upgrade your account (minimum $20 credit required)
|
||||
3. Once upgraded, you can send SMS to any valid phone number
|
||||
4. Pay only for messages sent (per SMS pricing)
|
||||
|
||||
**Option 3: Use Console Logs (Development Only)**
|
||||
- For testing/development, check server console logs
|
||||
- OTP codes are logged: `📱 DEBUG OTP (fallback): +919876543210 Code: 123456`
|
||||
- This allows testing without SMS delivery
|
||||
|
||||
---
|
||||
|
||||
### Error: "Failed to send OTP"
|
||||
|
||||
**Error Message:**
|
||||
```
|
||||
❌ Failed to send SMS via Twilio: [any error]
|
||||
```
|
||||
|
||||
**General Troubleshooting:**
|
||||
|
||||
1. **Check Twilio Credentials:**
|
||||
- Verify `TWILIO_ACCOUNT_SID` and `TWILIO_AUTH_TOKEN` in `.env`
|
||||
- Credentials should start with `AC` and be valid
|
||||
|
||||
2. **Check From Number/Service:**
|
||||
- Ensure `TWILIO_FROM_NUMBER` or `TWILIO_MESSAGING_SERVICE_SID` is set
|
||||
- Phone number must be in E.164 format: `+1234567890`
|
||||
|
||||
3. **Check Account Status:**
|
||||
- Login to [Twilio Console](https://console.twilio.com/)
|
||||
- Verify account is active (not suspended)
|
||||
- Check if you have credits/balance
|
||||
|
||||
4. **Check Phone Number Format:**
|
||||
- Must be E.164 format: `+[country code][number]`
|
||||
- Example: `+919876543210` (India), `+1234567890` (US)
|
||||
- No spaces or special characters
|
||||
|
||||
5. **Development Fallback:**
|
||||
- If SMS fails, OTP is always logged to console
|
||||
- Check server logs: `📱 DEBUG OTP (fallback): [phone] Code: [otp]`
|
||||
|
||||
---
|
||||
|
||||
## Understanding the Logs
|
||||
|
||||
**✅ Success:**
|
||||
```
|
||||
✅ Twilio SMS sent, SID: SMa7e2f23b3bdb05be1a275f43b71b2209
|
||||
```
|
||||
- SMS was successfully sent via Twilio
|
||||
- User should receive SMS with OTP
|
||||
|
||||
**❌ Failure (with fallback):**
|
||||
```
|
||||
❌ Failed to send SMS via Twilio: [error message]
|
||||
📱 DEBUG OTP (fallback): +919876543210 Code: 123456
|
||||
```
|
||||
- SMS sending failed, but OTP was generated
|
||||
- OTP code is logged to console for development/testing
|
||||
- User can still verify using the OTP code from logs
|
||||
|
||||
**⚠️ Warning (Twilio not configured):**
|
||||
```
|
||||
⚠️ Twilio credentials are not set. SMS sending will be disabled. OTP will be logged to console.
|
||||
📱 DEBUG OTP (Twilio not configured): +919876543210 Code: 123456
|
||||
```
|
||||
- Twilio credentials missing in `.env`
|
||||
- Service still works, but OTPs only appear in logs
|
||||
- Add Twilio credentials to enable SMS delivery
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Development:**
|
||||
- Use console logs for testing
|
||||
- Verify test numbers in Twilio Console
|
||||
|
||||
2. **Production:**
|
||||
- Upgrade to paid Twilio account
|
||||
- Use Messaging Service (recommended)
|
||||
- Monitor SMS delivery rates
|
||||
- Implement proper error handling in frontend
|
||||
|
||||
3. **Security:**
|
||||
- Never log OTPs in production
|
||||
- Use environment variables for credentials
|
||||
- Rotate Twilio credentials periodically
|
||||
|
||||
|
||||
|
|
@ -1,287 +0,0 @@
|
|||
# XSS Prevention Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Cross-Site Scripting (XSS) is a security vulnerability that allows attackers to inject malicious scripts into web pages viewed by other users. This guide provides best practices for preventing XSS attacks in frontend applications that interact with the Farm Auth Service.
|
||||
|
||||
## Server-Side Protection
|
||||
|
||||
The Farm Auth Service implements several server-side protections:
|
||||
|
||||
### 1. Content Security Policy (CSP)
|
||||
|
||||
The service sets a strict Content Security Policy header that:
|
||||
- Restricts script execution to same-origin and nonce-based inline scripts
|
||||
- Prevents unauthorized resource loading
|
||||
- Blocks inline event handlers and `javascript:` URLs
|
||||
|
||||
**CSP Header Example:**
|
||||
```
|
||||
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-...' 'unsafe-eval'; style-src 'self' 'nonce-...'; ...
|
||||
```
|
||||
|
||||
### 2. Security Headers
|
||||
|
||||
Additional security headers are set:
|
||||
- `X-XSS-Protection: 1; mode=block` - Legacy browser XSS filter
|
||||
- `X-Content-Type-Options: nosniff` - Prevents MIME type sniffing
|
||||
- `X-Frame-Options: DENY` - Prevents clickjacking
|
||||
|
||||
### 3. CORS Protection
|
||||
|
||||
Strict CORS origin whitelisting prevents unauthorized domains from making requests.
|
||||
|
||||
## Frontend Best Practices
|
||||
|
||||
### 1. Output Encoding
|
||||
|
||||
**Always encode user input before displaying it:**
|
||||
|
||||
```javascript
|
||||
// ❌ BAD - Vulnerable to XSS
|
||||
document.getElementById('username').innerHTML = userInput;
|
||||
|
||||
// ✅ GOOD - Safe
|
||||
document.getElementById('username').textContent = userInput;
|
||||
```
|
||||
|
||||
**For HTML content, use proper encoding:**
|
||||
```javascript
|
||||
function escapeHtml(text) {
|
||||
const map = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return text.replace(/[&<>"']/g, m => map[m]);
|
||||
}
|
||||
|
||||
// Use when you must set innerHTML
|
||||
element.innerHTML = escapeHtml(userInput);
|
||||
```
|
||||
|
||||
### 2. Avoid Dangerous APIs
|
||||
|
||||
**Never use these with user input:**
|
||||
- `innerHTML`
|
||||
- `outerHTML`
|
||||
- `document.write()`
|
||||
- `eval()`
|
||||
- `Function()` constructor
|
||||
- `setTimeout()` / `setInterval()` with string arguments
|
||||
|
||||
**Use safe alternatives:**
|
||||
- `textContent` instead of `innerHTML`
|
||||
- `setAttribute()` for attributes
|
||||
- `addEventListener()` instead of inline event handlers
|
||||
|
||||
### 3. Content Security Policy Nonce Support
|
||||
|
||||
If you need inline scripts or styles, use CSP nonces:
|
||||
|
||||
```javascript
|
||||
// Get nonce from meta tag (if server sets it)
|
||||
const nonce = document.querySelector('meta[name="csp-nonce"]')?.content;
|
||||
|
||||
// Use nonce in script tag
|
||||
const script = document.createElement('script');
|
||||
script.nonce = nonce;
|
||||
script.textContent = '// Your inline script';
|
||||
document.head.appendChild(script);
|
||||
```
|
||||
|
||||
**Note:** The current CSP configuration allows `'unsafe-inline'` for compatibility, but this should be tightened in production by using nonces exclusively.
|
||||
|
||||
### 4. Sanitize User Input
|
||||
|
||||
**For rich text content, use a sanitization library:**
|
||||
|
||||
```javascript
|
||||
// Using DOMPurify (recommended)
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
const clean = DOMPurify.sanitize(userInput, {
|
||||
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p'],
|
||||
ALLOWED_ATTR: ['href']
|
||||
});
|
||||
|
||||
element.innerHTML = clean;
|
||||
```
|
||||
|
||||
### 5. URL Validation
|
||||
|
||||
**Always validate and sanitize URLs:**
|
||||
|
||||
```javascript
|
||||
function isValidUrl(url) {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
// Only allow http/https
|
||||
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Use for links
|
||||
if (isValidUrl(userUrl)) {
|
||||
link.href = userUrl;
|
||||
} else {
|
||||
link.href = '#';
|
||||
link.onclick = (e) => { e.preventDefault(); alert('Invalid URL'); };
|
||||
}
|
||||
```
|
||||
|
||||
### 6. JSON Handling
|
||||
|
||||
**Always parse JSON safely:**
|
||||
|
||||
```javascript
|
||||
// ❌ BAD - eval() is dangerous
|
||||
const data = eval('(' + jsonString + ')');
|
||||
|
||||
// ✅ GOOD - Use JSON.parse()
|
||||
try {
|
||||
const data = JSON.parse(jsonString);
|
||||
} catch (e) {
|
||||
console.error('Invalid JSON');
|
||||
}
|
||||
```
|
||||
|
||||
### 7. React / Vue / Angular Specific
|
||||
|
||||
**React:**
|
||||
- React automatically escapes content in JSX
|
||||
- Use `dangerouslySetInnerHTML` only when necessary and sanitize first
|
||||
- Never use `dangerouslySetInnerHTML` with user input
|
||||
|
||||
```jsx
|
||||
// ✅ GOOD - React escapes automatically
|
||||
<div>{userInput}</div>
|
||||
|
||||
// ⚠️ CAUTION - Only if absolutely necessary
|
||||
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />
|
||||
```
|
||||
|
||||
**Vue:**
|
||||
- Use `v-text` instead of `v-html` when possible
|
||||
- Sanitize before using `v-html`
|
||||
|
||||
```vue
|
||||
<!-- ✅ GOOD -->
|
||||
<div v-text="userInput"></div>
|
||||
|
||||
<!-- ⚠️ CAUTION - Sanitize first -->
|
||||
<div v-html="sanitizedInput"></div>
|
||||
```
|
||||
|
||||
**Angular:**
|
||||
- Use interpolation `{{ }}` which automatically escapes
|
||||
- Use `[innerHTML]` only with sanitized content
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD - Angular escapes automatically
|
||||
<div>{{ userInput }}</div>
|
||||
|
||||
// ⚠️ CAUTION - Use DomSanitizer
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
const safe = this.sanitizer.sanitize(SecurityContext.HTML, userInput);
|
||||
```
|
||||
|
||||
### 8. API Response Handling
|
||||
|
||||
**Never trust API responses blindly:**
|
||||
|
||||
```javascript
|
||||
// ❌ BAD - Directly inserting API response
|
||||
fetch('/api/user')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
document.getElementById('profile').innerHTML = data.bio; // DANGEROUS!
|
||||
});
|
||||
|
||||
// ✅ GOOD - Sanitize or use textContent
|
||||
fetch('/api/user')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
document.getElementById('profile').textContent = data.bio; // Safe
|
||||
});
|
||||
```
|
||||
|
||||
### 9. Cookie Security
|
||||
|
||||
**Set secure cookie flags (server-side):**
|
||||
- `HttpOnly` - Prevents JavaScript access
|
||||
- `Secure` - Only sent over HTTPS
|
||||
- `SameSite=Strict` - Prevents CSRF
|
||||
|
||||
**Note:** The Farm Auth Service uses Bearer tokens, not cookies, which is more secure.
|
||||
|
||||
### 10. Third-Party Libraries
|
||||
|
||||
**Be cautious with third-party libraries:**
|
||||
- Only use well-maintained, trusted libraries
|
||||
- Keep dependencies updated
|
||||
- Review library code for XSS vulnerabilities
|
||||
- Use Content Security Policy to restrict external scripts
|
||||
|
||||
## Testing for XSS
|
||||
|
||||
### Manual Testing
|
||||
|
||||
Try these payloads in input fields:
|
||||
|
||||
```html
|
||||
<script>alert('XSS')</script>
|
||||
<img src=x onerror=alert('XSS')>
|
||||
<svg onload=alert('XSS')>
|
||||
javascript:alert('XSS')
|
||||
<iframe src="javascript:alert('XSS')">
|
||||
```
|
||||
|
||||
### Automated Testing
|
||||
|
||||
Use tools like:
|
||||
- OWASP ZAP
|
||||
- Burp Suite
|
||||
- XSSer
|
||||
- Browser DevTools Console
|
||||
|
||||
## Common XSS Attack Vectors
|
||||
|
||||
1. **Stored XSS:** Malicious script stored in database and executed when displayed
|
||||
2. **Reflected XSS:** Malicious script in URL parameters reflected in response
|
||||
3. **DOM-based XSS:** Malicious script in DOM manipulation without server interaction
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] All user input is encoded before display
|
||||
- [ ] No use of `innerHTML` with user input
|
||||
- [ ] URLs are validated before use
|
||||
- [ ] JSON is parsed safely (not with `eval()`)
|
||||
- [ ] Third-party libraries are vetted and updated
|
||||
- [ ] CSP nonces are used for inline scripts/styles
|
||||
- [ ] API responses are sanitized
|
||||
- [ ] Framework-specific best practices are followed
|
||||
- [ ] Regular security audits are performed
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [OWASP XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html)
|
||||
- [MDN Web Security: XSS](https://developer.mozilla.org/en-US/docs/Web/Security/Types_of_attacks#cross-site_scripting_xss)
|
||||
- [Content Security Policy Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)
|
||||
|
||||
## Reporting Security Issues
|
||||
|
||||
If you discover an XSS vulnerability in the Farm Auth Service, please report it responsibly:
|
||||
1. Do not disclose publicly
|
||||
2. Contact the security team
|
||||
3. Provide detailed reproduction steps
|
||||
4. Allow time for fix before disclosure
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2024
|
||||
**Version:** 1.0
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
-- Migration: Add token_version column to users table if it doesn't exist
|
||||
-- This column is used for global logout functionality
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Check if token_version column exists, if not add it
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'users'
|
||||
AND column_name = 'token_version'
|
||||
) THEN
|
||||
ALTER TABLE users
|
||||
ADD COLUMN token_version INT NOT NULL DEFAULT 1;
|
||||
|
||||
RAISE NOTICE 'Added token_version column to users table';
|
||||
ELSE
|
||||
RAISE NOTICE 'token_version column already exists in users table';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
|
|
@ -1,949 +0,0 @@
|
|||
# Farm Auth Service - Architecture Documentation
|
||||
|
||||
## 1. High-Level Overview
|
||||
|
||||
The Farm Auth Service is a Node.js + Express authentication and security service that provides phone-based authentication using OTP (One-Time Password) via SMS, JWT-based access and refresh tokens, comprehensive rate limiting, security hardening, and audit logging. The service is designed for a mobile application ecosystem where users authenticate using their phone numbers.
|
||||
|
||||
**Core Functionality:**
|
||||
- Phone number-based authentication with OTP verification via SMS (Twilio)
|
||||
- JWT access tokens (short-lived) and refresh tokens (long-lived) with rotation
|
||||
- Device tracking and multi-device session management
|
||||
- Comprehensive rate limiting at multiple levels (phone, IP, user)
|
||||
- Security hardening: CORS validation, security headers, field-level encryption, timing attack protection, enumeration detection
|
||||
- Audit logging with risk scoring and webhook alerting
|
||||
- Admin dashboard for security event monitoring
|
||||
|
||||
**External Systems:**
|
||||
- **PostgreSQL Database**: Stores users, OTP codes, refresh tokens, devices, and audit logs
|
||||
- **Redis** (optional): Used for rate limiting counters and OTP tracking (falls back to in-memory store)
|
||||
- **Twilio**: SMS provider for OTP delivery (optional - service works without it for development)
|
||||
- **Webhook Endpoints**: For security alerts (Slack, Discord, or custom webhooks)
|
||||
|
||||
---
|
||||
|
||||
## 2. Architecture & Components
|
||||
|
||||
### 2.1 HTTP/API Layer
|
||||
|
||||
**Files:**
|
||||
- `src/index.js` - Express server setup and middleware configuration
|
||||
- `src/routes/authRoutes.js` - Authentication endpoints
|
||||
- `src/routes/userRoutes.js` - User profile and device management endpoints
|
||||
- `src/routes/adminRoutes.js` - Admin security dashboard endpoints
|
||||
|
||||
**Responsibilities:**
|
||||
- Request routing and middleware orchestration
|
||||
- Input validation and sanitization
|
||||
- Response formatting
|
||||
- Error handling
|
||||
|
||||
**Middleware Order (Critical):**
|
||||
1. Trust proxy configuration (if behind reverse proxy)
|
||||
2. CORS validation (startup and runtime)
|
||||
3. JSON body parser
|
||||
4. Security headers (global)
|
||||
5. Route-specific middleware (validation, rate limiting, auth)
|
||||
|
||||
**Key Configuration:**
|
||||
- `TRUST_PROXY`: Set to `'true'` if behind reverse proxy (nginx, load balancer)
|
||||
- `CORS_ALLOWED_ORIGINS`: Comma-separated list of allowed origins (required in production)
|
||||
- `ENABLE_ADMIN_DASHBOARD`: Set to `'true'` to enable admin routes
|
||||
|
||||
### 2.2 Authentication Core
|
||||
|
||||
**Files:**
|
||||
- `src/services/otpService.js` - OTP generation, hashing (bcrypt), storage, and verification
|
||||
- `src/services/tokenService.js` - JWT access/refresh token issuance, rotation, and validation
|
||||
- `src/services/jwtKeys.js` - JWT key management with rotation support
|
||||
- `src/middleware/authMiddleware.js` - JWT access token validation
|
||||
- `src/middleware/stepUpAuth.js` - Step-up authentication for sensitive operations
|
||||
|
||||
**Responsibilities:**
|
||||
- OTP generation (6-digit random codes)
|
||||
- OTP hashing with bcrypt (10 rounds)
|
||||
- OTP storage in database with expiry and attempt tracking
|
||||
- JWT token signing with key rotation support
|
||||
- Refresh token rotation and reuse detection
|
||||
- Device fingerprinting and tracking
|
||||
|
||||
**Key Features:**
|
||||
- **OTP Security**: Hashed with bcrypt, constant-time verification to prevent timing attacks
|
||||
- **Token Rotation**: Refresh tokens rotate on each use, old tokens are revoked
|
||||
- **Reuse Detection**: Detects if a refresh token is reused (theft indicator)
|
||||
- **Step-Up Auth**: Requires recent OTP verification for sensitive operations
|
||||
|
||||
### 2.3 Security Layer
|
||||
|
||||
**Files:**
|
||||
- `src/middleware/rateLimitMiddleware.js` - OTP request/verification rate limiting
|
||||
- `src/middleware/userRateLimit.js` - User route rate limiting (read/write/sensitive)
|
||||
- `src/middleware/adminRateLimit.js` - Admin route rate limiting
|
||||
- `src/middleware/securityHeaders.js` - Security headers (CSP, HSTS, X-Frame-Options, etc.)
|
||||
- `src/utils/corsValidator.js` - CORS configuration validation
|
||||
- `src/utils/timingProtection.js` - Timing attack protection for OTP flows
|
||||
- `src/utils/enumerationDetection.js` - Phone number enumeration detection
|
||||
- `src/services/riskScoring.js` - Risk scoring for login/refresh attempts
|
||||
- `src/middleware/validation.js` - Input validation middleware
|
||||
|
||||
**Responsibilities:**
|
||||
- Rate limiting at multiple levels (phone, IP, user, admin)
|
||||
- Security headers enforcement
|
||||
- CORS origin validation (startup and runtime)
|
||||
- Timing attack mitigation (constant-time OTP verification)
|
||||
- Enumeration detection and IP blocking
|
||||
- Risk scoring based on IP/device changes
|
||||
- Input validation and sanitization
|
||||
|
||||
**Key Features:**
|
||||
- **Multi-Level Rate Limiting**: Phone-based, IP-based, and user-based limits
|
||||
- **Enumeration Protection**: Detects and blocks IPs attempting phone number enumeration
|
||||
- **Timing Attack Protection**: All OTP operations use constant-time execution
|
||||
- **Risk Scoring**: Calculates risk scores for suspicious login/refresh attempts
|
||||
|
||||
### 2.4 Persistence Layer
|
||||
|
||||
**Files:**
|
||||
- `src/db.js` - PostgreSQL connection pool and query wrapper
|
||||
- `src/middleware/dbAccessLogger.js` - Optional database access logging
|
||||
- `src/utils/fieldEncryption.js` - Field-level encryption for PII (phone numbers)
|
||||
- `src/utils/encryptedPhoneSearch.js` - Phone number search with encryption support
|
||||
|
||||
**Database Tables:**
|
||||
- `users` - User accounts (phone number, name, role, user_type)
|
||||
- `otp_codes` - OTP codes (hashed, with expiry and attempt tracking)
|
||||
- `refresh_tokens` - Refresh tokens (hashed, with rotation tracking)
|
||||
- `user_devices` - Device tracking (platform, model, OS, app version)
|
||||
- `auth_audit` - Security audit logs (all authentication events)
|
||||
|
||||
**Responsibilities:**
|
||||
- Database connection management
|
||||
- Query execution with optional logging
|
||||
- Field-level encryption for sensitive data (phone numbers)
|
||||
- Database schema management (auto-creates tables if missing)
|
||||
|
||||
**Key Features:**
|
||||
- **Field-Level Encryption**: Phone numbers encrypted at rest (AES-256-GCM)
|
||||
- **Database Access Logging**: Optional logging of all DB queries (for security auditing)
|
||||
- **Backward Compatibility**: Handles both encrypted and plaintext phone numbers during migration
|
||||
|
||||
### 2.5 Integration Layer
|
||||
|
||||
**Files:**
|
||||
- `src/services/smsService.js` - Twilio SMS integration
|
||||
- `src/services/auditLogger.js` - Audit logging with webhook alerting
|
||||
- `src/services/redisClient.js` - Redis client with graceful fallback
|
||||
|
||||
**Responsibilities:**
|
||||
- SMS delivery via Twilio (with fallback logging)
|
||||
- Security event logging to database
|
||||
- Webhook alerting for high-risk events
|
||||
- Redis connection management (optional, falls back to in-memory)
|
||||
|
||||
**Key Features:**
|
||||
- **Twilio Integration**: Sends OTP via SMS (optional - works without for development)
|
||||
- **Webhook Alerting**: Sends alerts to Slack/Discord/custom webhooks for SUSPICIOUS/HIGH_RISK events
|
||||
- **Redis Fallback**: Gracefully falls back to in-memory store if Redis unavailable
|
||||
|
||||
---
|
||||
|
||||
## 3. Request Flows
|
||||
|
||||
### 3.1 OTP Login Flow
|
||||
|
||||
**Step-by-Step:**
|
||||
|
||||
1. **Client requests OTP** (`POST /auth/request-otp`)
|
||||
- Input validation (phone number format)
|
||||
- Check for active OTP (2-minute no-resend rule)
|
||||
- Rate limit by phone number (3 per 10 min, 10 per day)
|
||||
- Rate limit by IP address (20 per 10 min, 100 per day)
|
||||
- Check if IP is blocked (enumeration or CIDR ranges)
|
||||
- Enumeration detection (if suspicious, apply stricter limits)
|
||||
- Timing protection wrapper (constant-time execution)
|
||||
- Normalize phone number (E.164 format)
|
||||
- Generate 6-digit OTP code
|
||||
- Hash OTP with bcrypt (10 rounds)
|
||||
- Encrypt phone number (if encryption enabled)
|
||||
- Store OTP in database (delete old OTPs for same phone)
|
||||
- Mark OTP as active in Redis/memory (2-minute TTL)
|
||||
- Send SMS via Twilio (or log to console if not configured)
|
||||
- Log audit event (otp_request, INFO risk level)
|
||||
- Return success (even if SMS fails - OTP is generated)
|
||||
|
||||
2. **Client verifies OTP** (`POST /auth/verify-otp`)
|
||||
- Input validation (phone number, 6-digit code, device_id, device_info)
|
||||
- Rate limit failed verifications (10 per hour per phone)
|
||||
- Check if IP is blocked
|
||||
- Timing protection wrapper (constant-time execution)
|
||||
- Normalize phone number
|
||||
- Encrypt phone number for search
|
||||
- Query OTP from database (with constant-time dummy hash if not found)
|
||||
- Check expiry, max attempts, and verify code (all with constant-time bcrypt.compare)
|
||||
- If invalid: increment attempt count, log suspicious event, return generic error
|
||||
- If valid: delete OTP, find or create user, decrypt phone number
|
||||
- Update user last_login_at
|
||||
- Upsert device record (track platform, model, OS, app version)
|
||||
- Calculate risk score (IP change, device change, user agent change)
|
||||
- Log audit event (login, risk level based on score)
|
||||
- Check for anomalies (multiple failed attempts, high-risk IPs)
|
||||
- Issue access token (with high_assurance flag) and refresh token
|
||||
- Return user data, tokens, and device info
|
||||
|
||||
**Mermaid Sequence Diagram:**
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant API
|
||||
participant RateLimiter
|
||||
participant OTPService
|
||||
participant DB
|
||||
participant Twilio
|
||||
participant AuditLogger
|
||||
|
||||
Client->>API: POST /auth/request-otp<br/>{phone_number}
|
||||
API->>API: Validate input
|
||||
API->>RateLimiter: Check active OTP (2-min rule)
|
||||
RateLimiter-->>API: No active OTP
|
||||
API->>RateLimiter: Rate limit by phone (3/10min)
|
||||
RateLimiter-->>API: Allowed
|
||||
API->>RateLimiter: Rate limit by IP (20/10min)
|
||||
RateLimiter-->>API: Allowed
|
||||
API->>API: Check IP blocking
|
||||
API->>OTPService: Generate OTP
|
||||
OTPService->>DB: Store hashed OTP
|
||||
OTPService->>RateLimiter: Mark active (2-min TTL)
|
||||
API->>Twilio: Send SMS
|
||||
Twilio-->>API: SMS sent (or error)
|
||||
API->>AuditLogger: Log otp_request event
|
||||
API-->>Client: {ok: true}
|
||||
|
||||
Client->>API: POST /auth/verify-otp<br/>{phone_number, code, device_id}
|
||||
API->>API: Validate input
|
||||
API->>RateLimiter: Check failed attempts (10/hour)
|
||||
RateLimiter-->>API: Allowed
|
||||
API->>OTPService: Verify OTP (constant-time)
|
||||
OTPService->>DB: Query OTP (with dummy hash if not found)
|
||||
OTPService->>OTPService: bcrypt.compare (constant-time)
|
||||
alt OTP Valid
|
||||
OTPService->>DB: Delete OTP
|
||||
API->>DB: Find or create user
|
||||
API->>DB: Upsert device
|
||||
API->>API: Calculate risk score
|
||||
API->>AuditLogger: Log login (with risk level)
|
||||
API->>API: Issue access + refresh tokens
|
||||
API-->>Client: {user, access_token, refresh_token}
|
||||
else OTP Invalid
|
||||
OTPService->>DB: Increment attempt count
|
||||
API->>AuditLogger: Log suspicious attempt
|
||||
API-->>Client: {error: "OTP invalid or expired"}
|
||||
end
|
||||
```
|
||||
|
||||
### 3.2 Token Refresh Flow
|
||||
|
||||
**Step-by-Step:**
|
||||
|
||||
1. **Client requests token refresh** (`POST /auth/refresh`)
|
||||
- Input validation (refresh_token)
|
||||
- Check if IP is blocked
|
||||
- Decode refresh token to get key ID
|
||||
- Verify refresh token signature (try all keys if key ID not found)
|
||||
- Validate JWT claims (iss, aud, exp, iat)
|
||||
- Query refresh token from database (by token_id)
|
||||
- Verify token hash matches (bcrypt.compare)
|
||||
- Check if token is revoked or expired
|
||||
- Check refresh token idle timeout (max idle minutes)
|
||||
- Calculate risk score (IP change, device change, user agent change)
|
||||
- If suspicious: log suspicious refresh event
|
||||
- If suspicious and REQUIRE_OTP_ON_SUSPICIOUS_REFRESH: return step_up_required error
|
||||
- Update token last_used_at
|
||||
- Revoke old refresh token
|
||||
- Issue new access token and new refresh token (rotation)
|
||||
- Update device last_seen_at
|
||||
- Log audit event (token_refresh, risk level based on score)
|
||||
- Return new tokens
|
||||
|
||||
**Mermaid Sequence Diagram:**
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant API
|
||||
participant TokenService
|
||||
participant JWTKeys
|
||||
participant DB
|
||||
participant RiskScoring
|
||||
participant AuditLogger
|
||||
|
||||
Client->>API: POST /auth/refresh<br/>{refresh_token}
|
||||
API->>API: Validate input
|
||||
API->>API: Check IP blocking
|
||||
API->>TokenService: Verify refresh token
|
||||
TokenService->>JWTKeys: Get key secret (by key ID)
|
||||
JWTKeys-->>TokenService: Key secret
|
||||
TokenService->>TokenService: Verify JWT signature
|
||||
TokenService->>TokenService: Validate claims (iss, aud, exp)
|
||||
TokenService->>DB: Query refresh token (by token_id)
|
||||
DB-->>TokenService: Token record
|
||||
TokenService->>TokenService: Verify token hash (bcrypt)
|
||||
alt Token Valid
|
||||
TokenService->>TokenService: Check expiry & idle timeout
|
||||
API->>RiskScoring: Calculate risk score
|
||||
RiskScoring->>DB: Get previous auth info
|
||||
RiskScoring-->>API: Risk score & reasons
|
||||
alt Suspicious Refresh
|
||||
API->>AuditLogger: Log suspicious refresh
|
||||
alt Require OTP
|
||||
API-->>Client: {error: "step_up_required"}
|
||||
else Allow with Risk
|
||||
API->>TokenService: Rotate refresh token
|
||||
TokenService->>DB: Revoke old token
|
||||
TokenService->>DB: Store new token
|
||||
API->>AuditLogger: Log refresh (SUSPICIOUS/HIGH_RISK)
|
||||
API-->>Client: {access_token, refresh_token}
|
||||
end
|
||||
else Normal Refresh
|
||||
API->>TokenService: Rotate refresh token
|
||||
TokenService->>DB: Revoke old token
|
||||
TokenService->>DB: Store new token
|
||||
API->>DB: Update device last_seen_at
|
||||
API->>AuditLogger: Log refresh (INFO)
|
||||
API-->>Client: {access_token, refresh_token}
|
||||
end
|
||||
else Token Invalid
|
||||
API-->>Client: {error: "Invalid refresh token"}
|
||||
end
|
||||
```
|
||||
|
||||
### 3.3 Logout Flow
|
||||
|
||||
**Step-by-Step:**
|
||||
|
||||
1. **Single-device logout** (`POST /auth/logout`)
|
||||
- Input validation (refresh_token)
|
||||
- Verify refresh token (same as refresh flow)
|
||||
- If token invalid/already revoked: return success (idempotent)
|
||||
- Revoke all refresh tokens for user + device
|
||||
- Log audit event (logout, INFO)
|
||||
- Return success
|
||||
|
||||
2. **Logout all other devices** (`POST /users/me/logout-all-other-devices`)
|
||||
- Requires authentication (access token)
|
||||
- Requires step-up auth (recent OTP or high_assurance token)
|
||||
- Rate limited (10 per hour per user)
|
||||
- Get current device_id from header or body
|
||||
- Mark all other devices as inactive
|
||||
- Revoke refresh tokens for all other devices
|
||||
- Log audit event (logout_all_other_devices, INFO)
|
||||
- Return count of revoked devices
|
||||
|
||||
3. **Logout from all devices** (`POST /users/me/logout-all-devices`)
|
||||
- Requires authentication (access token)
|
||||
- Requires step-up auth (recent OTP or high_assurance token)
|
||||
- Rate limited (10 per hour per user)
|
||||
- Revoke all refresh tokens for the user (all devices)
|
||||
- Mark all devices as inactive
|
||||
- Increment user's `token_version` to invalidate all existing access tokens
|
||||
- Log audit event (logout_all_devices, HIGH_RISK) - triggers security alert
|
||||
- Return success with revoked tokens count
|
||||
- **Security Note**: This is a critical security operation used when account compromise is suspected. All existing access tokens become invalid immediately, even if they haven't expired yet.
|
||||
|
||||
4. **Revoke specific device** (`DELETE /users/me/devices/:device_id`)
|
||||
- Requires authentication (access token)
|
||||
- Requires step-up auth (recent OTP or high_assurance token)
|
||||
- Rate limited (10 per hour per user)
|
||||
- Validate device_id parameter
|
||||
- Mark device as inactive
|
||||
- Revoke refresh tokens for device
|
||||
- Log audit event (device_revoked, INFO)
|
||||
- Return success
|
||||
|
||||
**Mermaid Sequence Diagram:**
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant API
|
||||
participant TokenService
|
||||
participant DB
|
||||
participant AuditLogger
|
||||
|
||||
Note over Client,AuditLogger: Single Device Logout
|
||||
Client->>API: POST /auth/logout<br/>{refresh_token}
|
||||
API->>TokenService: Verify refresh token
|
||||
TokenService-->>API: Token info
|
||||
API->>TokenService: Revoke refresh token
|
||||
TokenService->>DB: Mark token revoked
|
||||
API->>AuditLogger: Log logout event
|
||||
API-->>Client: {ok: true}
|
||||
|
||||
Note over Client,AuditLogger: Logout All Other Devices
|
||||
Client->>API: POST /users/me/logout-all-other-devices<br/>{current_device_id}
|
||||
API->>API: Verify access token
|
||||
API->>API: Check step-up auth
|
||||
API->>API: Rate limit check (10/hour)
|
||||
API->>DB: Mark other devices inactive
|
||||
API->>TokenService: Revoke tokens for other devices
|
||||
TokenService->>DB: Revoke tokens
|
||||
API->>AuditLogger: Log logout_all_other_devices
|
||||
API-->>Client: {ok: true, revoked_devices_count: N}
|
||||
|
||||
Note over Client,AuditLogger: Logout All Devices (Global Logout)
|
||||
Client->>API: POST /users/me/logout-all-devices
|
||||
API->>API: Verify access token
|
||||
API->>API: Check step-up auth
|
||||
API->>API: Rate limit check (10/hour)
|
||||
API->>TokenService: Revoke all user tokens
|
||||
TokenService->>DB: Revoke all refresh tokens
|
||||
TokenService->>DB: Mark all devices inactive
|
||||
TokenService->>DB: Increment token_version
|
||||
API->>AuditLogger: Log logout_all_devices (HIGH_RISK)
|
||||
AuditLogger->>AuditLogger: Trigger security alert
|
||||
API-->>Client: {ok: true, revoked_tokens_count: N}
|
||||
```
|
||||
|
||||
### 3.4 Admin Security Events Flow
|
||||
|
||||
**Step-by-Step:**
|
||||
|
||||
1. **Admin requests security events** (`GET /admin/security-events`)
|
||||
- Requires authentication (access token)
|
||||
- Requires admin role (security_admin)
|
||||
- Rate limited (100 per 15 minutes per admin)
|
||||
- Validate and sanitize query parameters (risk_level, limit, offset, search)
|
||||
- Build parameterized SQL query (prevent injection)
|
||||
- Query auth_audit table with filters
|
||||
- Mask phone numbers (keep last 4 digits)
|
||||
- Sanitize all output fields
|
||||
- Get total count for pagination
|
||||
- Get statistics (last 24 hours: total, high_risk, suspicious, info)
|
||||
- Log admin access event (admin_view_security_events, INFO)
|
||||
- Return events, pagination info, and statistics
|
||||
|
||||
**Mermaid Sequence Diagram:**
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Admin
|
||||
participant API
|
||||
participant AuthMiddleware
|
||||
participant AdminAuth
|
||||
participant AdminRateLimit
|
||||
participant DB
|
||||
participant AuditLogger
|
||||
|
||||
Admin->>API: GET /admin/security-events<br/>?risk_level=HIGH_RISK&limit=200
|
||||
API->>AuthMiddleware: Verify access token
|
||||
AuthMiddleware-->>API: User info
|
||||
API->>AdminAuth: Check admin role
|
||||
AdminAuth-->>API: Authorized
|
||||
API->>AdminRateLimit: Check rate limit (100/15min)
|
||||
AdminRateLimit-->>API: Allowed
|
||||
API->>API: Sanitize query params
|
||||
API->>DB: Query auth_audit (parameterized)
|
||||
DB-->>API: Events data
|
||||
API->>API: Mask phone numbers
|
||||
API->>API: Sanitize output
|
||||
API->>DB: Get total count
|
||||
API->>DB: Get statistics (24h)
|
||||
API->>AuditLogger: Log admin access
|
||||
API-->>Admin: {events, pagination, stats}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Timeouts, Expiry, and Limits
|
||||
|
||||
| Name | ENV Variable / Config | Default Value | Defined In | What It Affects |
|
||||
|------|----------------------|---------------|------------|-----------------|
|
||||
| **OTP Expiry** | `OTP_TTL_SECONDS` | `120` (2 minutes) | `src/services/otpService.js:10` | OTP validity period |
|
||||
| **OTP Resend Throttle** | (hardcoded) | `120` seconds | `src/middleware/rateLimitMiddleware.js:154` | Minimum time between OTP requests for same phone |
|
||||
| **Max OTP Verification Attempts** | `OTP_VERIFY_MAX_ATTEMPTS` | `5` | `src/services/otpService.js:12` | Maximum attempts to verify an OTP before it's invalidated |
|
||||
| **JWT Access Token Expiry** | `JWT_ACCESS_TTL` | `'15m'` (15 minutes) | `src/config.js:72` | Access token lifetime |
|
||||
| **JWT Refresh Token Expiry** | `JWT_REFRESH_TTL` | `'7d'` (7 days) | `src/config.js:73` | Refresh token lifetime |
|
||||
| **Refresh Token Max Idle** | `REFRESH_MAX_IDLE_MINUTES` | `4320` (3 days) | `src/config.js:58-60` | Maximum idle time before refresh token expires |
|
||||
| **Step-Up Auth Window** | `STEP_UP_OTP_WINDOW_MINUTES` | `5` minutes | `src/middleware/stepUpAuth.js:26` | Time window for "recent" OTP verification for step-up auth |
|
||||
| **OTP Request - Phone (10 min)** | `OTP_REQ_PHONE_10MIN_LIMIT` | `3` | `src/middleware/rateLimitMiddleware.js:24` | Max OTP requests per phone per 10 minutes |
|
||||
| **OTP Request - Phone (24h)** | `OTP_REQ_PHONE_DAY_LIMIT` | `10` | `src/middleware/rateLimitMiddleware.js:25` | Max OTP requests per phone per 24 hours |
|
||||
| **OTP Request - IP (10 min)** | `OTP_REQ_IP_10MIN_LIMIT` | `20` | `src/middleware/rateLimitMiddleware.js:26` | Max OTP requests per IP per 10 minutes |
|
||||
| **OTP Request - IP (24h)** | `OTP_REQ_IP_DAY_LIMIT` | `100` | `src/middleware/rateLimitMiddleware.js:27` | Max OTP requests per IP per 24 hours |
|
||||
| **OTP Verify Failed (1h)** | `OTP_VERIFY_FAILED_PER_HOUR_LIMIT` | `10` | `src/middleware/rateLimitMiddleware.js:31` | Max failed verification attempts per phone per hour |
|
||||
| **Enumeration IP Block Duration** | `ENUMERATION_BLOCK_DURATION` | `3600` (1 hour) | `src/middleware/rateLimitMiddleware.js:40` | Duration IP is blocked after enumeration detection |
|
||||
| **User Rate Limit - Read** | `USER_RATE_LIMIT_READ_MAX` | `100` | `src/middleware/userRateLimit.js:25` | Max read requests per user per 15 minutes |
|
||||
| **User Rate Limit - Read Window** | `USER_RATE_LIMIT_READ_WINDOW` | `900` (15 min) | `src/middleware/userRateLimit.js:26` | Time window for read rate limit |
|
||||
| **User Rate Limit - Write** | `USER_RATE_LIMIT_WRITE_MAX` | `20` | `src/middleware/userRateLimit.js:29` | Max write requests per user per 15 minutes |
|
||||
| **User Rate Limit - Write Window** | `USER_RATE_LIMIT_WRITE_WINDOW` | `900` (15 min) | `src/middleware/userRateLimit.js:30` | Time window for write rate limit |
|
||||
| **User Rate Limit - Sensitive** | `USER_RATE_LIMIT_SENSITIVE_MAX` | `10` | `src/middleware/userRateLimit.js:33` | Max sensitive requests per user per hour |
|
||||
| **User Rate Limit - Sensitive Window** | `USER_RATE_LIMIT_SENSITIVE_WINDOW` | `3600` (1 hour) | `src/middleware/userRateLimit.js:34` | Time window for sensitive rate limit |
|
||||
| **Admin Rate Limit** | `ADMIN_RATE_LIMIT_MAX` | `100` | `src/middleware/adminRateLimit.js:23` | Max admin requests per admin per 15 minutes |
|
||||
| **Admin Rate Limit Window** | `ADMIN_RATE_LIMIT_WINDOW` | `900` (15 min) | `src/middleware/adminRateLimit.js:24` | Time window for admin rate limit |
|
||||
| **Twilio HTTP Timeout** | (hardcoded) | `5000` ms | `src/services/auditLogger.js:459` | Webhook request timeout (also used for Twilio if configured) |
|
||||
| **Webhook Retry Delay** | (hardcoded) | `3000` ms | `src/services/auditLogger.js:498` | Delay before retrying failed webhook alerts |
|
||||
| **OTP Request Min Delay** | `OTP_REQUEST_MIN_DELAY` | `500` ms | `src/utils/timingProtection.js:26` | Minimum delay for OTP requests (timing attack protection) |
|
||||
| **OTP Verify Min Delay** | `OTP_VERIFY_MIN_DELAY` | `300` ms | `src/utils/timingProtection.js:30` | Minimum delay for OTP verification (timing attack protection) |
|
||||
| **Timing Max Jitter** | `TIMING_MAX_JITTER` | `100` ms | `src/utils/timingProtection.js:34` | Maximum random jitter added to delays |
|
||||
| **Enumeration Max Phones/IP (10min)** | `ENUMERATION_MAX_PHONES_PER_IP_10MIN` | `5` | `src/utils/enumerationDetection.js:32` | Max unique phone numbers per IP in 10 minutes |
|
||||
| **Enumeration Max Phones/IP (1h)** | `ENUMERATION_MAX_PHONES_PER_IP_HOUR` | `20` | `src/utils/enumerationDetection.js:33` | Max unique phone numbers per IP in 1 hour |
|
||||
| **Enumeration Alert Threshold (10min)** | `ENUMERATION_ALERT_THRESHOLD_10MIN` | `10` | `src/utils/enumerationDetection.js:40` | Unique phones threshold for alert (10 min) |
|
||||
| **Enumeration Alert Threshold (1h)** | `ENUMERATION_ALERT_THRESHOLD_HOUR` | `50` | `src/utils/enumerationDetection.js:41` | Unique phones threshold for alert (1 hour) |
|
||||
|
||||
---
|
||||
|
||||
## 5. Security Features
|
||||
|
||||
### 5.1 CORS Behavior
|
||||
|
||||
**Configuration:**
|
||||
- **Startup Validation**: CORS configuration is validated at startup (`src/index.js:29-34`)
|
||||
- **Runtime Monitoring**: Runtime CORS checks log warnings for suspicious patterns (`src/index.js:58-63`)
|
||||
- **Origin Whitelisting**: Only explicitly configured origins are allowed (never wildcard `*` when credentials are involved)
|
||||
- **No-Origin Requests**: Requests without origin (mobile apps, Postman) are allowed
|
||||
|
||||
**Implementation:**
|
||||
- `CORS_ALLOWED_ORIGINS`: Comma-separated list of allowed origins (required in production)
|
||||
- Development mode: Allows all origins if no origins configured (with warning)
|
||||
- Production mode: Throws error if `CORS_ALLOWED_ORIGINS` is empty
|
||||
|
||||
**Files:**
|
||||
- `src/index.js:36-86` - CORS middleware configuration
|
||||
- `src/utils/corsValidator.js` - CORS validation utilities
|
||||
|
||||
### 5.2 Security Headers
|
||||
|
||||
**Headers Set Globally:**
|
||||
- `X-Frame-Options: DENY` - Prevents clickjacking
|
||||
- `X-Content-Type-Options: nosniff` - Prevents MIME type sniffing
|
||||
- `X-XSS-Protection: 1; mode=block` - Enables XSS filter (legacy browsers)
|
||||
- `Strict-Transport-Security` - HSTS (only in production, max-age=31536000, includeSubDomains, preload)
|
||||
- `Content-Security-Policy` - CSP with nonce support for inline scripts/styles
|
||||
- `Referrer-Policy: strict-origin-when-cross-origin` - Controls referrer information
|
||||
- `Permissions-Policy` - Restricts browser features (geolocation, microphone, camera, etc.)
|
||||
|
||||
**Files:**
|
||||
- `src/middleware/securityHeaders.js` - Security headers middleware
|
||||
|
||||
### 5.3 Authentication & Authorization
|
||||
|
||||
**Authentication:**
|
||||
- **OTP-Based**: Phone number + 6-digit OTP code
|
||||
- **JWT Access Tokens**: Short-lived (15 minutes), signed with HS256, include `token_version` claim
|
||||
- **JWT Refresh Tokens**: Long-lived (7 days), stored hashed in database, rotated on each use
|
||||
- **Device Tracking**: Tracks device identifier, platform, model, OS version, app version
|
||||
- **Token Versioning**: Access tokens include `token_version` claim that is validated against user's current version in database. When user logs out from all devices, `token_version` is incremented, invalidating all existing access tokens immediately.
|
||||
|
||||
**Authorization:**
|
||||
- **Role-Based**: Admin routes require `role === 'security_admin'`
|
||||
- **Step-Up Auth**: Sensitive operations require recent OTP verification or `high_assurance` token flag
|
||||
- **Token Claims**: Validates `iss` (issuer), `aud` (audience), `exp` (expiration), `iat` (issued at), `token_version` (for access token invalidation)
|
||||
|
||||
**Files:**
|
||||
- `src/middleware/authMiddleware.js` - Access token validation
|
||||
- `src/middleware/adminAuth.js` - Admin role check
|
||||
- `src/middleware/stepUpAuth.js` - Step-up authentication
|
||||
|
||||
### 5.4 Audit Logging
|
||||
|
||||
**Events Logged:**
|
||||
- `otp_request` - OTP request (success/failed)
|
||||
- `otp_verify` - OTP verification (success/failed)
|
||||
- `login` - User login (success/blocked)
|
||||
- `token_refresh` - Token refresh (success, with risk level)
|
||||
- `logout` - User logout
|
||||
- `device_revoked` - Device revocation
|
||||
- `logout_all_other_devices` - Logout all other devices
|
||||
- `logout_all_devices` - Logout from all devices (HIGH_RISK, triggers security alert)
|
||||
- `admin_view_security_events` - Admin access to security dashboard
|
||||
|
||||
**Risk Levels:**
|
||||
- `INFO` - Normal operations
|
||||
- `SUSPICIOUS` - Unusual patterns (IP change, device change, multiple failures)
|
||||
- `HIGH_RISK` - Blocked IPs, high risk scores (>=50), enumeration attempts
|
||||
|
||||
**Alerting:**
|
||||
- **Webhook Integration**: Sends alerts to `SECURITY_ALERT_WEBHOOK_URL` for SUSPICIOUS/HIGH_RISK events
|
||||
- **Anomaly Detection**: Detects patterns (multiple failed OTPs, multiple high-risk events from same IP)
|
||||
- **Retry Logic**: Retries failed webhook alerts once after 3 seconds
|
||||
|
||||
**Files:**
|
||||
- `src/services/auditLogger.js` - Audit logging and webhook alerting
|
||||
- `src/services/riskScoring.js` - Risk score calculation
|
||||
|
||||
### 5.5 Data Protection
|
||||
|
||||
**Field-Level Encryption:**
|
||||
- **Algorithm**: AES-256-GCM (authenticated encryption)
|
||||
- **Fields Encrypted**: Phone numbers (before storing in database)
|
||||
- **Key Management**: 32-byte key from `ENCRYPTION_KEY` (base64 encoded)
|
||||
- **Backward Compatibility**: Handles both encrypted and plaintext data during migration
|
||||
|
||||
**Database Access Logging:**
|
||||
- **Optional Feature**: Enabled with `DB_ACCESS_LOGGING_ENABLED=true`
|
||||
- **Logs**: All database queries with context (user ID, IP, user agent)
|
||||
- **Use Case**: Security auditing, compliance
|
||||
|
||||
**Files:**
|
||||
- `src/utils/fieldEncryption.js` - Field-level encryption
|
||||
- `src/middleware/dbAccessLogger.js` - Database access logging
|
||||
|
||||
### 5.6 Protection Against Attacks
|
||||
|
||||
**Brute-Force / Enumeration:**
|
||||
- Rate limiting at multiple levels (phone, IP, user)
|
||||
- Enumeration detection (tracks unique phone numbers per IP)
|
||||
- IP blocking for enumeration attempts (1 hour block)
|
||||
- Stricter rate limits when enumeration detected
|
||||
|
||||
**Timing Attacks:**
|
||||
- Constant-time OTP verification (always performs bcrypt.compare, uses dummy hash if OTP not found)
|
||||
- Timing protection wrappers for OTP request and verification flows
|
||||
- Minimum delay enforcement to prevent timing leaks
|
||||
|
||||
**Man-in-the-Middle:**
|
||||
- HTTPS enforcement via HSTS header (production)
|
||||
- Security headers (CSP, X-Frame-Options) prevent various MITM attacks
|
||||
- JWT token validation with signature verification
|
||||
|
||||
**Token Replay:**
|
||||
- Refresh token rotation (new token issued, old token revoked)
|
||||
- Reuse detection (if old token is used, all tokens for device are revoked)
|
||||
- Access token short expiry (15 minutes) limits replay window
|
||||
- Token versioning: Access tokens include `token_version` claim that is validated on each request. When user logs out from all devices, version is incremented, immediately invalidating all existing access tokens (even if not expired)
|
||||
|
||||
**Files:**
|
||||
- `src/utils/timingProtection.js` - Timing attack protection
|
||||
- `src/utils/enumerationDetection.js` - Enumeration detection
|
||||
- `src/services/tokenService.js` - Token rotation and reuse detection
|
||||
|
||||
---
|
||||
|
||||
## 6. Error Handling & Failure Modes
|
||||
|
||||
### 6.1 OTP Sending Failures
|
||||
|
||||
**Behavior:**
|
||||
- If Twilio is not configured: OTP is logged to console, request still succeeds
|
||||
- If Twilio fails: Error is logged, OTP is still generated and stored, request succeeds
|
||||
- **Rationale**: OTP generation should not fail if SMS delivery fails (user can check logs in development)
|
||||
|
||||
**Error Response:**
|
||||
- Success response returned even if SMS fails (for development/testing)
|
||||
- Production recommendation: Return error if SMS fails (uncomment error return in `src/routes/authRoutes.js:213`)
|
||||
|
||||
**Files:**
|
||||
- `src/services/smsService.js` - SMS sending with fallback logging
|
||||
|
||||
### 6.2 Database Failures
|
||||
|
||||
**Behavior:**
|
||||
- Connection pool errors: Logged, process exits (`src/db.js:11-14`)
|
||||
- Query errors: Propagated to route handler, return 500 error
|
||||
- **No Retries**: Database queries are not retried automatically (application-level retries can be added)
|
||||
|
||||
**Error Response:**
|
||||
- `500 Internal Server Error` with generic message: `{error: 'Internal server error'}`
|
||||
|
||||
**Files:**
|
||||
- `src/db.js` - Database connection and query wrapper
|
||||
|
||||
### 6.3 JWT Validation Errors
|
||||
|
||||
**Behavior:**
|
||||
- Invalid token format: `401 Unauthorized` - `{error: 'Invalid token format'}`
|
||||
- Invalid/expired token: `401 Unauthorized` - `{error: 'Invalid or expired token'}`
|
||||
- Invalid claims: `401 Unauthorized` - `{error: 'Invalid token claims'}`
|
||||
- Missing Authorization header: `401 Unauthorized` - `{error: 'Missing Authorization header'}`
|
||||
|
||||
**Key Rotation:**
|
||||
- If key ID not found: Tries all available keys (for rotation support)
|
||||
- If no key matches: Returns `401 Unauthorized`
|
||||
|
||||
**Files:**
|
||||
- `src/middleware/authMiddleware.js` - JWT validation
|
||||
- `src/services/tokenService.js` - Refresh token validation
|
||||
|
||||
### 6.4 Rate Limit Exceeded
|
||||
|
||||
**Behavior:**
|
||||
- OTP request rate limit: `429 Too Many Requests` - `{success: false, message: 'Too many OTP requests...'}`
|
||||
- OTP verify rate limit: `429 Too Many Requests` - `{success: false, message: 'Too many attempts...'}`
|
||||
- User route rate limit: `429 Too Many Requests` - `{error: 'Too many requests', retry_after: seconds}`
|
||||
- Admin route rate limit: `429 Too Many Requests` - `{error: 'Too many requests', retry_after: seconds}`
|
||||
|
||||
**Headers:**
|
||||
- `X-RateLimit-Limit`: Maximum requests allowed
|
||||
- `X-RateLimit-Remaining`: Remaining requests in window
|
||||
- `X-RateLimit-Reset`: ISO timestamp when limit resets
|
||||
- `X-RateLimit-Type`: Type of rate limit (read/write/sensitive/admin)
|
||||
|
||||
**Files:**
|
||||
- `src/middleware/rateLimitMiddleware.js` - OTP rate limiting
|
||||
- `src/middleware/userRateLimit.js` - User route rate limiting
|
||||
- `src/middleware/adminRateLimit.js` - Admin rate limiting
|
||||
|
||||
### 6.5 Retries & Fallbacks
|
||||
|
||||
**Redis Fallback:**
|
||||
- If Redis unavailable: Falls back to in-memory store (per-process, not shared)
|
||||
- Rate limiting continues to work (with per-instance limits, not global)
|
||||
- Warning logged on first failure, then silent
|
||||
|
||||
**Webhook Alerting:**
|
||||
- If webhook fails: Retries once after 3 seconds
|
||||
- If retry fails: Error logged, but main request flow continues (non-blocking)
|
||||
|
||||
**Files:**
|
||||
- `src/services/redisClient.js` - Redis client with graceful fallback
|
||||
- `src/services/auditLogger.js:334-516` - Webhook alerting with retry
|
||||
|
||||
---
|
||||
|
||||
## 7. Configuration & Environment Variables
|
||||
|
||||
### 7.1 Required Variables
|
||||
|
||||
| Variable | Description | Example | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `DATABASE_URL` | PostgreSQL connection string | `postgres://user:pass@localhost:5432/dbname` | ✅ Yes |
|
||||
| `JWT_ACCESS_SECRET` | Secret for signing access tokens (min 32 chars) | `hex-string-32-chars-minimum` | ✅ Yes |
|
||||
| `JWT_REFRESH_SECRET` | Secret for signing refresh tokens (min 32 chars) | `hex-string-32-chars-minimum` | ✅ Yes |
|
||||
|
||||
### 7.2 Optional Variables - Timeouts & Expiry
|
||||
|
||||
| Variable | Description | Default | Example |
|
||||
|----------|-------------|---------|---------|
|
||||
| `JWT_ACCESS_TTL` | Access token expiry | `15m` | `15m`, `1h` |
|
||||
| `JWT_REFRESH_TTL` | Refresh token expiry | `7d` | `7d`, `30d` |
|
||||
| `REFRESH_MAX_IDLE_MINUTES` | Refresh token max idle time | `4320` (3 days) | `4320` |
|
||||
| `OTP_TTL_SECONDS` | OTP validity in seconds | `120` (2 min) | `120` |
|
||||
| `STEP_UP_OTP_WINDOW_MINUTES` | Step-up auth window | `5` | `5` |
|
||||
|
||||
### 7.3 Optional Variables - Rate Limits
|
||||
|
||||
| Variable | Description | Default | Example |
|
||||
|----------|-------------|---------|---------|
|
||||
| `OTP_REQ_PHONE_10MIN_LIMIT` | Max OTP requests per phone (10 min) | `3` | `3` |
|
||||
| `OTP_REQ_PHONE_DAY_LIMIT` | Max OTP requests per phone (24h) | `10` | `10` |
|
||||
| `OTP_REQ_IP_10MIN_LIMIT` | Max OTP requests per IP (10 min) | `20` | `20` |
|
||||
| `OTP_REQ_IP_DAY_LIMIT` | Max OTP requests per IP (24h) | `100` | `100` |
|
||||
| `OTP_VERIFY_MAX_ATTEMPTS` | Max OTP verification attempts | `5` | `5` |
|
||||
| `OTP_VERIFY_FAILED_PER_HOUR_LIMIT` | Max failed verifications per phone (1h) | `10` | `10` |
|
||||
| `USER_RATE_LIMIT_READ_MAX` | Max read requests per user (15 min) | `100` | `100` |
|
||||
| `USER_RATE_LIMIT_WRITE_MAX` | Max write requests per user (15 min) | `20` | `20` |
|
||||
| `USER_RATE_LIMIT_SENSITIVE_MAX` | Max sensitive requests per user (1h) | `10` | `10` |
|
||||
| `ADMIN_RATE_LIMIT_MAX` | Max admin requests per admin (15 min) | `100` | `100` |
|
||||
|
||||
### 7.4 Optional Variables - Security Features
|
||||
|
||||
| Variable | Description | Default | Example |
|
||||
|----------|-------------|---------|---------|
|
||||
| `ENCRYPTION_ENABLED` | Enable field-level encryption | `false` | `true` |
|
||||
| `ENCRYPTION_KEY` | 32-byte encryption key (base64) | - | `base64-encoded-32-byte-key` |
|
||||
| `DB_ACCESS_LOGGING_ENABLED` | Enable database access logging | `false` | `true` |
|
||||
| `DB_ACCESS_LOG_LEVEL` | DB access log level ('all' or 'sensitive') | `sensitive` | `all`, `sensitive` |
|
||||
| `CORS_ALLOWED_ORIGINS` | Comma-separated allowed origins | - | `https://app.example.com,https://api.example.com` |
|
||||
| `ENUMERATION_MAX_PHONES_PER_IP_10MIN` | Max unique phones per IP (10 min) | `5` | `5` |
|
||||
| `ENUMERATION_MAX_PHONES_PER_IP_HOUR` | Max unique phones per IP (1h) | `20` | `20` |
|
||||
| `ENUMERATION_ALERT_THRESHOLD_10MIN` | Alert threshold for enumeration (10 min) | `10` | `10` |
|
||||
| `ENUMERATION_ALERT_THRESHOLD_HOUR` | Alert threshold for enumeration (1h) | `50` | `50` |
|
||||
| `OTP_REQUEST_MIN_DELAY` | Min delay for OTP requests (ms) | `500` | `500` |
|
||||
| `OTP_VERIFY_MIN_DELAY` | Min delay for OTP verify (ms) | `300` | `300` |
|
||||
| `TIMING_MAX_JITTER` | Max jitter for timing protection (ms) | `100` | `100` |
|
||||
| `BLOCKED_IP_RANGES` | Comma-separated CIDR blocks | - | `10.0.0.0/8,172.16.0.0/12` |
|
||||
| `REQUIRE_OTP_ON_SUSPICIOUS_REFRESH` | Require OTP on suspicious refresh | `false` | `true` |
|
||||
| `SECURITY_ALERT_WEBHOOK_URL` | Webhook URL for security alerts | - | `https://hooks.slack.com/...` |
|
||||
| `SECURITY_ALERT_MIN_LEVEL` | Minimum risk level for alerts | `HIGH_RISK` | `SUSPICIOUS`, `HIGH_RISK` |
|
||||
|
||||
### 7.5 Optional Variables - JWT Key Rotation
|
||||
|
||||
| Variable | Description | Default | Example |
|
||||
|----------|-------------|---------|---------|
|
||||
| `JWT_ACTIVE_KEY_ID` | Key ID for signing new tokens | `1` | `1`, `2` |
|
||||
| `JWT_KEYS_JSON` | JSON mapping key IDs to secrets | - | `{"1":"secret1","2":"secret2"}` |
|
||||
| `JWT_REFRESH_KEY_ID` | Key ID for refresh tokens | Same as active | `1` |
|
||||
| `JWT_ISSUER` | JWT issuer claim | `farm-auth-service` | `farm-auth-service` |
|
||||
| `JWT_AUDIENCE` | JWT audience claim | `mobile-app` | `mobile-app` |
|
||||
|
||||
### 7.6 Optional Variables - External Services
|
||||
|
||||
| Variable | Description | Default | Example |
|
||||
|----------|-------------|---------|---------|
|
||||
| `TWILIO_ACCOUNT_SID` | Twilio account SID | - | `ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` |
|
||||
| `TWILIO_AUTH_TOKEN` | Twilio auth token | - | `your_auth_token` |
|
||||
| `TWILIO_MESSAGING_SERVICE_SID` | Twilio messaging service SID | - | `MGxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` |
|
||||
| `TWILIO_FROM_NUMBER` | Twilio phone number (E.164) | - | `+1234567890` |
|
||||
| `REDIS_URL` | Redis connection URL | - | `redis://localhost:6379` |
|
||||
| `REDIS_HOST` | Redis host | `localhost` | `localhost` |
|
||||
| `REDIS_PORT` | Redis port | `6379` | `6379` |
|
||||
| `REDIS_PASSWORD` | Redis password | - | `password` |
|
||||
|
||||
### 7.7 Optional Variables - Server Configuration
|
||||
|
||||
| Variable | Description | Default | Example |
|
||||
|----------|-------------|---------|---------|
|
||||
| `PORT` | Server port | `3000` | `3000` |
|
||||
| `NODE_ENV` | Environment | - | `development`, `production` |
|
||||
| `TRUST_PROXY` | Trust proxy headers | `false` | `true` |
|
||||
| `ENABLE_ADMIN_DASHBOARD` | Enable admin routes | `false` | `true` |
|
||||
|
||||
---
|
||||
|
||||
## 8. Future Improvements / Notes
|
||||
|
||||
### 8.1 Planned Improvements (from TODOs in code)
|
||||
|
||||
1. **Secrets Manager Integration**
|
||||
- Load JWT keys from AWS Secrets Manager / HashiCorp Vault (instead of environment variables)
|
||||
- Load encryption keys from secrets manager
|
||||
- **File**: `src/services/jwtKeys.js:161-174` (TODO comment)
|
||||
|
||||
2. **Automated Key Rotation**
|
||||
- Implement automated JWT key rotation without downtime
|
||||
- Re-encrypt existing data when encryption keys are rotated
|
||||
- **File**: `src/services/jwtKeys.js` (key rotation support exists, but automation needed)
|
||||
|
||||
3. **SIEM Integration**
|
||||
- Integrate with SIEM systems (Splunk, ELK, etc.) for centralized log aggregation
|
||||
- Export audit logs to SIEM for advanced threat detection
|
||||
- **File**: `src/services/auditLogger.js` (webhook exists, but SIEM integration needed)
|
||||
|
||||
4. **CSP Nonces**
|
||||
- Fully implement CSP nonces for inline scripts/styles (currently allows `unsafe-inline` for compatibility)
|
||||
- **File**: `src/middleware/securityHeaders.js:28-29` (nonce support exists but not fully utilized)
|
||||
|
||||
5. **Database Connection Pooling Tuning**
|
||||
- Add configuration for connection pool size, timeout, etc.
|
||||
- **File**: `src/db.js` (basic pool, no tuning options)
|
||||
|
||||
6. **Rate Limiting Improvements**
|
||||
- Implement distributed rate limiting (currently per-instance if Redis unavailable)
|
||||
- Add rate limit headers to all rate-limited endpoints
|
||||
- **File**: `src/middleware/rateLimitMiddleware.js` (Redis fallback exists, but distributed limiting needed)
|
||||
|
||||
7. **OTP Delivery Alternatives**
|
||||
- Support multiple SMS providers (fallback if Twilio fails)
|
||||
- Support email OTP delivery
|
||||
- Support push notification OTP delivery
|
||||
- **File**: `src/services/smsService.js` (only Twilio supported)
|
||||
|
||||
8. **Advanced Risk Scoring**
|
||||
- Machine learning-based risk scoring
|
||||
- Geographic anomaly detection (unusual locations)
|
||||
- Device fingerprinting improvements
|
||||
- **File**: `src/services/riskScoring.js` (basic scoring exists)
|
||||
|
||||
### 8.2 Potential Risks & Technical Debt
|
||||
|
||||
1. **In-Memory Rate Limiting**
|
||||
- If Redis is unavailable, rate limiting uses in-memory store (per-instance, not shared)
|
||||
- **Risk**: Rate limits are per-instance, not global (can be bypassed with multiple instances)
|
||||
- **Mitigation**: Always use Redis in production, or implement distributed rate limiting
|
||||
|
||||
2. **OTP Storage**
|
||||
- OTPs are stored in database (not just Redis)
|
||||
- **Risk**: Database can become a bottleneck for high-volume OTP requests
|
||||
- **Mitigation**: Consider moving OTP storage to Redis entirely (with DB backup for audit)
|
||||
|
||||
3. **Phone Number Encryption Migration**
|
||||
- Handles both encrypted and plaintext phone numbers (backward compatibility)
|
||||
- **Risk**: Plaintext phone numbers still in database if encryption was enabled after data existed
|
||||
- **Mitigation**: Implement migration script to encrypt all existing phone numbers
|
||||
|
||||
4. **Webhook Alerting**
|
||||
- Webhook failures are logged but don't block requests
|
||||
- **Risk**: Security alerts might be missed if webhook is down
|
||||
- **Mitigation**: Implement alert queue (Redis/RabbitMQ) with retry logic and dead-letter queue
|
||||
|
||||
5. **Database Access Logging**
|
||||
- Database access logging is optional and can impact performance
|
||||
- **Risk**: Performance degradation if enabled in high-traffic scenarios
|
||||
- **Mitigation**: Use async logging, batch writes, or separate logging database
|
||||
|
||||
6. **JWT Key Rotation**
|
||||
- Key rotation support exists, but manual process
|
||||
- **Risk**: Manual key rotation can cause downtime if not done correctly
|
||||
- **Mitigation**: Implement automated key rotation with gradual rollout
|
||||
|
||||
7. **CORS Configuration**
|
||||
- CORS validation at startup, but runtime checks are warnings only
|
||||
- **Risk**: Misconfiguration might not be caught until runtime
|
||||
- **Mitigation**: Add stricter runtime validation or fail-fast on suspicious patterns
|
||||
|
||||
8. **Error Messages**
|
||||
- Some error messages are generic to prevent information leakage
|
||||
- **Risk**: Generic errors can make debugging difficult
|
||||
- **Mitigation**: Log detailed errors server-side, return generic errors to clients
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Database Schema
|
||||
|
||||
### Key Tables
|
||||
|
||||
**users**
|
||||
- `id` (UUID, PK)
|
||||
- `phone_number` (VARCHAR(20), UNIQUE, encrypted if ENCRYPTION_ENABLED)
|
||||
- `name` (VARCHAR(255))
|
||||
- `role` (enum: 'user', 'admin', 'moderator')
|
||||
- `user_type` (enum: 'seller', 'buyer', 'service_provider')
|
||||
- `token_version` (INT, DEFAULT 1) - Incremented on logout-all-devices to invalidate all access tokens
|
||||
- `created_at`, `updated_at`, `last_login_at`
|
||||
|
||||
**otp_codes**
|
||||
- `id` (UUID, PK)
|
||||
- `phone_number` (VARCHAR(20), encrypted if ENCRYPTION_ENABLED)
|
||||
- `otp_hash` (VARCHAR(255), bcrypt hash)
|
||||
- `expires_at` (TIMESTAMPTZ)
|
||||
- `attempt_count` (INT)
|
||||
- `created_at` (TIMESTAMPTZ)
|
||||
|
||||
**refresh_tokens**
|
||||
- `id` (UUID, PK)
|
||||
- `user_id` (UUID, FK)
|
||||
- `token_id` (UUID, UNIQUE)
|
||||
- `token_hash` (VARCHAR(255), bcrypt hash)
|
||||
- `device_id` (VARCHAR(255))
|
||||
- `user_agent` (TEXT)
|
||||
- `ip_address` (VARCHAR(45))
|
||||
- `expires_at` (TIMESTAMPTZ)
|
||||
- `last_used_at` (TIMESTAMPTZ)
|
||||
- `revoked_at` (TIMESTAMPTZ, NULL = active)
|
||||
- `reuse_detected_at` (TIMESTAMPTZ)
|
||||
- `rotated_from_id` (UUID, FK to refresh_tokens)
|
||||
|
||||
**user_devices**
|
||||
- `id` (UUID, PK)
|
||||
- `user_id` (UUID, FK)
|
||||
- `device_identifier` (TEXT)
|
||||
- `device_platform` (TEXT)
|
||||
- `device_model` (TEXT)
|
||||
- `os_version` (TEXT)
|
||||
- `app_version` (TEXT)
|
||||
- `language_code` (TEXT)
|
||||
- `timezone` (TEXT)
|
||||
- `first_seen_at` (TIMESTAMPTZ)
|
||||
- `last_seen_at` (TIMESTAMPTZ)
|
||||
- `is_active` (BOOLEAN)
|
||||
- UNIQUE (user_id, device_identifier)
|
||||
|
||||
**auth_audit**
|
||||
- `id` (UUID, PK)
|
||||
- `user_id` (UUID, FK, nullable)
|
||||
- `action` (VARCHAR(100))
|
||||
- `status` (VARCHAR(50))
|
||||
- `risk_level` (VARCHAR(20): 'INFO', 'SUSPICIOUS', 'HIGH_RISK')
|
||||
- `ip_address` (VARCHAR(45))
|
||||
- `user_agent` (TEXT)
|
||||
- `device_id` (VARCHAR(255))
|
||||
- `meta` (JSONB)
|
||||
- `created_at` (TIMESTAMPTZ)
|
||||
|
||||
---
|
||||
|
||||
## Document Version
|
||||
|
||||
- **Version**: 1.0
|
||||
- **Last Updated**: 2024
|
||||
- **Author**: Architecture Documentation Generator
|
||||
- **Maintained By**: Development Team
|
||||
|
||||
116
public/test.html
116
public/test.html
|
|
@ -21,7 +21,7 @@
|
|||
<label>Phone Number (E.164, e.g. +91XXXXXXXXXX)</label>
|
||||
<input id="phone" type="text" placeholder="+91..." />
|
||||
|
||||
<button onclick="requestOtp()">Request OTP</button>
|
||||
<button id="requestOtpBtn">Request OTP</button>
|
||||
|
||||
<pre id="requestOtpResult"></pre>
|
||||
</div>
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
<label>Device ID</label>
|
||||
<input id="deviceId" type="text" value="web-test-device" />
|
||||
|
||||
<button onclick="verifyOtp()">Verify OTP</button>
|
||||
<button id="verifyOtpBtn">Verify OTP</button>
|
||||
|
||||
<pre id="verifyOtpResult"></pre>
|
||||
</div>
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
<option value="service_provider">Service Provider</option>
|
||||
</select>
|
||||
|
||||
<button onclick="updateProfile()">Update Profile</button>
|
||||
<button id="updateProfileBtn">Update Profile</button>
|
||||
|
||||
<pre id="updateProfileResult"></pre>
|
||||
</div>
|
||||
|
|
@ -63,7 +63,7 @@
|
|||
<p>Authenticated GET request to fetch user profile with JWT token</p>
|
||||
<p>Shows: phone number, name, profile type, location, last login, and all saved locations</p>
|
||||
|
||||
<button onclick="getUserDetails()">Get User Details</button>
|
||||
<button id="getUserDetailsBtn">Get User Details</button>
|
||||
|
||||
<pre id="getUserDetailsResult"></pre>
|
||||
</div>
|
||||
|
|
@ -72,10 +72,24 @@
|
|||
let accessToken = null;
|
||||
let refreshToken = null;
|
||||
|
||||
// Add error handler for uncaught errors
|
||||
window.addEventListener('error', (e) => {
|
||||
console.error('Global error:', e);
|
||||
alert('JavaScript error: ' + e.message);
|
||||
});
|
||||
|
||||
async function requestOtp() {
|
||||
const phone = document.getElementById('phone').value.trim();
|
||||
const out = document.getElementById('requestOtpResult');
|
||||
|
||||
if (!phone) {
|
||||
out.textContent = 'Error: Please enter a phone number';
|
||||
alert('Please enter a phone number');
|
||||
return;
|
||||
}
|
||||
|
||||
out.textContent = 'Sending...';
|
||||
console.log('Requesting OTP for:', phone);
|
||||
|
||||
try {
|
||||
const res = await fetch('/auth/request-otp', {
|
||||
|
|
@ -84,10 +98,20 @@
|
|||
body: JSON.stringify({ phone_number: phone })
|
||||
});
|
||||
|
||||
console.log('Response status:', res.status);
|
||||
const data = await res.json();
|
||||
console.log('Response data:', data);
|
||||
out.textContent = JSON.stringify(data, null, 2);
|
||||
|
||||
if (res.ok) {
|
||||
alert('OTP sent successfully! Check your phone or server logs.');
|
||||
} else {
|
||||
alert('Error: ' + (data.error || data.message || 'Failed to send OTP'));
|
||||
}
|
||||
} catch (err) {
|
||||
out.textContent = 'Error: ' + err;
|
||||
console.error('Request OTP error:', err);
|
||||
out.textContent = 'Error: ' + err.message;
|
||||
alert('Error: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -96,7 +120,21 @@
|
|||
const otp = document.getElementById('otp').value.trim();
|
||||
const deviceId = document.getElementById('deviceId').value.trim() || 'web-test-device';
|
||||
const out = document.getElementById('verifyOtpResult');
|
||||
|
||||
if (!phone) {
|
||||
out.textContent = 'Error: Please enter a phone number';
|
||||
alert('Please enter a phone number');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!otp) {
|
||||
out.textContent = 'Error: Please enter an OTP code';
|
||||
alert('Please enter an OTP code');
|
||||
return;
|
||||
}
|
||||
|
||||
out.textContent = 'Verifying...';
|
||||
console.log('Verifying OTP for:', phone, 'code:', otp);
|
||||
|
||||
try {
|
||||
const res = await fetch('/auth/verify-otp', {
|
||||
|
|
@ -117,7 +155,9 @@
|
|||
})
|
||||
});
|
||||
|
||||
console.log('Response status:', res.status);
|
||||
const data = await res.json();
|
||||
console.log('Response data:', data);
|
||||
out.textContent = JSON.stringify(data, null, 2);
|
||||
|
||||
if (res.ok) {
|
||||
|
|
@ -125,10 +165,14 @@
|
|||
refreshToken = data.refresh_token;
|
||||
console.log('Access token set:', accessToken);
|
||||
console.log('Refresh token set:', refreshToken);
|
||||
alert('OTP verified. needs_profile = ' + data.needs_profile);
|
||||
alert('OTP verified successfully! needs_profile = ' + data.needs_profile);
|
||||
} else {
|
||||
alert('Error: ' + (data.error || data.message || 'OTP verification failed'));
|
||||
}
|
||||
} catch (err) {
|
||||
out.textContent = 'Error: ' + err;
|
||||
console.error('Verify OTP error:', err);
|
||||
out.textContent = 'Error: ' + err.message;
|
||||
alert('Error: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -139,10 +183,18 @@
|
|||
|
||||
if (!accessToken) {
|
||||
alert('No access_token. Verify OTP first.');
|
||||
out.textContent = 'Error: No access token. Please verify OTP first.';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
alert('Please enter a name');
|
||||
out.textContent = 'Error: Please enter a name';
|
||||
return;
|
||||
}
|
||||
|
||||
out.textContent = 'Updating profile...';
|
||||
console.log('Updating profile:', { name, user_type: userType });
|
||||
|
||||
try {
|
||||
const res = await fetch('/users/me', {
|
||||
|
|
@ -154,10 +206,20 @@
|
|||
body: JSON.stringify({ name, user_type: userType })
|
||||
});
|
||||
|
||||
console.log('Response status:', res.status);
|
||||
const data = await res.json();
|
||||
console.log('Response data:', data);
|
||||
out.textContent = JSON.stringify(data, null, 2);
|
||||
|
||||
if (res.ok) {
|
||||
alert('Profile updated successfully!');
|
||||
} else {
|
||||
alert('Error: ' + (data.error || data.message || 'Failed to update profile'));
|
||||
}
|
||||
} catch (err) {
|
||||
out.textContent = 'Error: ' + err;
|
||||
console.error('Update profile error:', err);
|
||||
out.textContent = 'Error: ' + err.message;
|
||||
alert('Error: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -171,6 +233,7 @@
|
|||
}
|
||||
|
||||
out.textContent = 'Fetching user details...';
|
||||
console.log('Fetching user details with token:', accessToken.substring(0, 20) + '...');
|
||||
|
||||
try {
|
||||
const res = await fetch('/users/me', {
|
||||
|
|
@ -181,22 +244,57 @@
|
|||
}
|
||||
});
|
||||
|
||||
console.log('Response status:', res.status);
|
||||
const data = await res.json();
|
||||
console.log('Response data:', data);
|
||||
|
||||
if (res.ok) {
|
||||
out.textContent = JSON.stringify(data, null, 2);
|
||||
alert('User details fetched successfully!');
|
||||
} else {
|
||||
out.textContent = `Error ${res.status}: ${JSON.stringify(data, null, 2)}`;
|
||||
|
||||
// If token expired, try to refresh
|
||||
if (res.status === 401) {
|
||||
alert('Access token expired. Try refreshing the token or verify OTP again.');
|
||||
} else {
|
||||
alert('Error: ' + (data.error || data.message || 'Failed to fetch user details'));
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
out.textContent = 'Error: ' + err;
|
||||
console.error('Get user details error:', err);
|
||||
out.textContent = 'Error: ' + err.message;
|
||||
alert('Error: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Attach event listeners to buttons (replaces inline onclick handlers)
|
||||
// Since script is at end of body, DOM is already loaded
|
||||
(function() {
|
||||
const requestOtpBtn = document.getElementById('requestOtpBtn');
|
||||
const verifyOtpBtn = document.getElementById('verifyOtpBtn');
|
||||
const updateProfileBtn = document.getElementById('updateProfileBtn');
|
||||
const getUserDetailsBtn = document.getElementById('getUserDetailsBtn');
|
||||
|
||||
if (requestOtpBtn) {
|
||||
requestOtpBtn.addEventListener('click', requestOtp);
|
||||
console.log('Request OTP button listener attached');
|
||||
}
|
||||
if (verifyOtpBtn) {
|
||||
verifyOtpBtn.addEventListener('click', verifyOtp);
|
||||
console.log('Verify OTP button listener attached');
|
||||
}
|
||||
if (updateProfileBtn) {
|
||||
updateProfileBtn.addEventListener('click', updateProfile);
|
||||
console.log('Update Profile button listener attached');
|
||||
}
|
||||
if (getUserDetailsBtn) {
|
||||
getUserDetailsBtn.addEventListener('click', getUserDetails);
|
||||
console.log('Get User Details button listener attached');
|
||||
}
|
||||
|
||||
console.log('Test page loaded. All event listeners attached.');
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
// Quick script to run the token_version migration
|
||||
const { Pool } = require('pg');
|
||||
require('dotenv').config();
|
||||
|
||||
const db = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
});
|
||||
|
||||
async function runMigration() {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const migrationSQL = fs.readFileSync(
|
||||
path.join(__dirname, 'db/migrations/add_token_version.sql'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
try {
|
||||
console.log('Running migration: add_token_version...');
|
||||
await db.query(migrationSQL);
|
||||
console.log('✅ Migration completed successfully!');
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error('❌ Migration failed:', err.message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await db.end();
|
||||
}
|
||||
}
|
||||
|
||||
runMigration();
|
||||
|
||||
|
|
@ -93,6 +93,15 @@ app.use(securityHeaders);
|
|||
// 👇 This must be here to serve test.html
|
||||
app.use(express.static('public'));
|
||||
|
||||
// Routes for test page (multiple aliases for convenience)
|
||||
app.get('/test', (req, res) => {
|
||||
res.sendFile('test.html', { root: 'public' });
|
||||
});
|
||||
|
||||
app.get('/testpage', (req, res) => {
|
||||
res.sendFile('test.html', { root: 'public' });
|
||||
});
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -93,9 +93,16 @@ async function authMiddleware(req, res, next) {
|
|||
return res.status(401).json({ error: 'Invalid or expired token' });
|
||||
}
|
||||
} catch (dbErr) {
|
||||
// Handle missing token_version column gracefully
|
||||
if (dbErr.code === '42703' && dbErr.message && dbErr.message.includes('token_version')) {
|
||||
console.warn('token_version column not found in database, skipping version check');
|
||||
// Continue without token version validation (backward compatibility)
|
||||
// Token will still be validated for expiry and signature
|
||||
} else {
|
||||
console.error('Error validating token version:', dbErr);
|
||||
return res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
req.user = {
|
||||
id: payload.sub,
|
||||
|
|
|
|||
|
|
@ -23,9 +23,10 @@ function buildCSP(nonce = null) {
|
|||
const nonceValue = nonce ? `'nonce-${nonce}'` : '';
|
||||
|
||||
// Base CSP directives
|
||||
// Note: 'unsafe-inline' is kept for test.html and other static pages
|
||||
const directives = [
|
||||
"default-src 'self'",
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval'", // Allow inline for compatibility (can be tightened)
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval'", // Allow inline for compatibility (test.html needs this)
|
||||
"style-src 'self' 'unsafe-inline'", // Allow inline styles (can be tightened)
|
||||
"img-src 'self' data: https:", // Allow images from self, data URIs, and HTTPS
|
||||
"font-src 'self' data: https:", // Allow fonts from self, data URIs, and HTTPS
|
||||
|
|
@ -36,12 +37,11 @@ function buildCSP(nonce = null) {
|
|||
"upgrade-insecure-requests", // Upgrade HTTP to HTTPS
|
||||
];
|
||||
|
||||
// Add nonce if provided
|
||||
// Add nonce if provided (but keep unsafe-inline for compatibility)
|
||||
if (nonceValue) {
|
||||
// Replace unsafe-inline with nonce for scripts
|
||||
directives[1] = `script-src 'self' ${nonceValue} 'unsafe-eval'`;
|
||||
// Replace unsafe-inline with nonce for styles
|
||||
directives[2] = `style-src 'self' ${nonceValue}`;
|
||||
// Keep unsafe-inline for test.html compatibility, add nonce for additional security
|
||||
directives[1] = `script-src 'self' 'unsafe-inline' ${nonceValue} 'unsafe-eval'`;
|
||||
directives[2] = `style-src 'self' 'unsafe-inline' ${nonceValue}`;
|
||||
}
|
||||
|
||||
// Allow CORS origins for connect-src if configured
|
||||
|
|
@ -78,13 +78,22 @@ function securityHeaders(req, res, next) {
|
|||
}
|
||||
|
||||
// === SECURITY HARDENING: CONTENT SECURITY POLICY ===
|
||||
// Generate nonce for this request (stored in res.locals for use in templates)
|
||||
// Check if this is a test page that needs unsafe-inline (no nonce)
|
||||
const isTestPage = req.path === '/test' || req.path === '/testpage' || req.path === '/test.html';
|
||||
|
||||
if (isTestPage) {
|
||||
// For test pages, use CSP without nonce (allows unsafe-inline)
|
||||
// This is acceptable for development/test pages
|
||||
const csp = buildCSP(null); // null = no nonce, unsafe-inline will work
|
||||
res.setHeader('Content-Security-Policy', csp);
|
||||
res.locals.cspNonce = null; // No nonce for test pages
|
||||
} else {
|
||||
// For other pages, use nonce-based CSP
|
||||
const nonce = generateNonce();
|
||||
res.locals.cspNonce = nonce;
|
||||
|
||||
// Set CSP header
|
||||
const csp = buildCSP(nonce);
|
||||
res.setHeader('Content-Security-Policy', csp);
|
||||
}
|
||||
|
||||
// === SECURITY HARDENING: REFERRER POLICY ===
|
||||
// Control referrer information sent
|
||||
|
|
|
|||
|
|
@ -111,3 +111,4 @@ module.exports = {
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -314,21 +314,49 @@ router.post(
|
|||
const phoneSearchParams = preparePhoneSearchParams(normalizedPhone);
|
||||
|
||||
let user;
|
||||
// Check if token_version column exists, use it if available, otherwise default to 1
|
||||
const found = await db.query(
|
||||
`SELECT id, phone_number, name, role, NULL::user_type_enum as user_type, COALESCE(token_version, 1) as token_version
|
||||
`SELECT id, phone_number, name, role, NULL::user_type_enum as user_type,
|
||||
COALESCE(token_version, 1) as token_version
|
||||
FROM users
|
||||
WHERE phone_number = $1 OR phone_number = $2`,
|
||||
phoneSearchParams
|
||||
).catch(async (err) => {
|
||||
// If token_version column doesn't exist, try without it
|
||||
if (err.code === '42703' && err.message.includes('token_version')) {
|
||||
console.warn('token_version column not found, using default value of 1');
|
||||
return await db.query(
|
||||
`SELECT id, phone_number, name, role, NULL::user_type_enum as user_type, 1 as token_version
|
||||
FROM users
|
||||
WHERE phone_number = $1 OR phone_number = $2`,
|
||||
phoneSearchParams
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
|
||||
if (found.rows.length === 0) {
|
||||
// Insert with encrypted phone number
|
||||
const inserted = await db.query(
|
||||
`INSERT INTO users (phone_number)
|
||||
VALUES ($1)
|
||||
RETURNING id, phone_number, name, role, NULL::user_type_enum as user_type, COALESCE(token_version, 1) as token_version`,
|
||||
RETURNING id, phone_number, name, role, NULL::user_type_enum as user_type, 1 as token_version`,
|
||||
[encryptedPhone]
|
||||
).catch(async (err) => {
|
||||
// If token_version column doesn't exist, try without it in RETURNING
|
||||
if (err.code === '42703' && err.message.includes('token_version')) {
|
||||
const result = await db.query(
|
||||
`INSERT INTO users (phone_number)
|
||||
VALUES ($1)
|
||||
RETURNING id, phone_number, name, role, NULL::user_type_enum as user_type`,
|
||||
[encryptedPhone]
|
||||
);
|
||||
// Add token_version default
|
||||
result.rows[0].token_version = 1;
|
||||
return result;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
user = inserted.rows[0];
|
||||
} else {
|
||||
user = found.rows[0];
|
||||
|
|
|
|||
|
|
@ -164,7 +164,19 @@ router.put(
|
|||
return res.json(updatedUser);
|
||||
} catch (err) {
|
||||
console.error('update me error', err);
|
||||
return res.status(500).json({ error: 'Internal server error' });
|
||||
// Log more details for debugging
|
||||
if (err.code) {
|
||||
console.error('Database error code:', err.code);
|
||||
console.error('Database error message:', err.message);
|
||||
}
|
||||
return res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
// Include error details in development
|
||||
...(process.env.NODE_ENV !== 'production' && {
|
||||
details: err.message,
|
||||
code: err.code
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -187,3 +187,4 @@ module.exports = {
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -265,3 +265,4 @@ module.exports = {
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -107,12 +107,27 @@ async function addToSetAndGetCount(key, value, ttlSeconds) {
|
|||
if (isRedisReady() && redis) {
|
||||
try {
|
||||
// Use Redis set to track unique phone numbers
|
||||
// In Redis v4+, try camelCase first (sAdd, sCard), then fallback to lowercase
|
||||
if (typeof redis.sAdd === 'function') {
|
||||
// Redis v4+ uses camelCase method names
|
||||
await redis.sAdd(key, value);
|
||||
await redis.expire(key, ttlSeconds);
|
||||
const count = await redis.sCard(key);
|
||||
return count;
|
||||
} else if (typeof redis.sadd === 'function') {
|
||||
// Fallback for lowercase API
|
||||
await redis.sadd(key, value);
|
||||
await redis.expire(key, ttlSeconds);
|
||||
const count = await redis.scard(key);
|
||||
return count;
|
||||
} else {
|
||||
// If neither method exists, the client might not be properly initialized
|
||||
// This shouldn't happen, but handle gracefully
|
||||
console.warn('Redis set methods not available, using memory fallback');
|
||||
throw new Error('Redis client does not have set methods available');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Redis set operation error, falling back to memory:', err);
|
||||
console.error('Redis set operation error, falling back to memory:', err.message);
|
||||
// Fall through to memory store
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,3 +64,4 @@ module.exports = {
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue