Updates Goodtag

This commit is contained in:
Chandresh Kerkar 2025-12-14 19:39:24 +05:30
parent e38204c4e6
commit ea87d97364
37 changed files with 14016 additions and 11 deletions

73
Documentaion/README.md Normal file
View File

@ -0,0 +1,73 @@
# Documentation Index
This directory contains all documentation for the Farm Auth Service, organized by category.
## 📚 Documentation Structure
### 🚀 [Getting Started](./getting-started/)
Essential guides for setting up and running the service:
- **[QUICK_START.md](./getting-started/QUICK_START.md)** - Quick start guide
- **[SETUP.md](./getting-started/SETUP.md)** - Detailed setup instructions
- **[DOCKER_SETUP.md](./getting-started/DOCKER_SETUP.md)** - Docker deployment guide
### 👨‍💼 [Admin Dashboard](./admin/)
Documentation for the admin security dashboard:
- **[ADMIN_DASHBOARD_QUICK_START.md](./admin/ADMIN_DASHBOARD_QUICK_START.md)** - Quick start for admin dashboard
- **[ADMIN_DASHBOARD_SETUP.md](./admin/ADMIN_DASHBOARD_SETUP.md)** - Admin dashboard setup guide
- **[ADMIN_DASHBOARD_SECURITY.md](./admin/ADMIN_DASHBOARD_SECURITY.md)** - Security considerations for admin dashboard
### 🔒 [Security](./security/)
Security documentation, audits, and hardening guides:
- **[SECURITY_AUDIT_REPORT.md](./security/SECURITY_AUDIT_REPORT.md)** - Security audit findings
- **[SECURITY_HARDENING_SUMMARY.md](./security/SECURITY_HARDENING_SUMMARY.md)** - Summary of security hardening measures
- **[SECURITY_SCENARIOS.md](./security/SECURITY_SCENARIOS.md)** - Security threat scenarios
- **[REMAINING_SECURITY_GAPS.md](./security/REMAINING_SECURITY_GAPS.md)** - Known security gaps and recommendations
- **[CORS_XSS_IMPLEMENTATION.md](./security/CORS_XSS_IMPLEMENTATION.md)** - CORS and XSS protection implementation
- **[CSRF_NOTES.md](./security/CSRF_NOTES.md)** - CSRF protection notes
- **[XSS_PREVENTION_GUIDE.md](./security/XSS_PREVENTION_GUIDE.md)** - XSS prevention guide
- **[TIMING_ATTACK_PROTECTION.md](./security/TIMING_ATTACK_PROTECTION.md)** - Timing attack protection
- **[DATABASE_ENCRYPTION_SETUP.md](./security/DATABASE_ENCRYPTION_SETUP.md)** - Database encryption setup
### 🛠️ [Implementation](./implementation/)
Detailed implementation guides for specific features:
- **[RATE_LIMITING_IMPLEMENTATION.md](./implementation/RATE_LIMITING_IMPLEMENTATION.md)** - Rate limiting implementation details
- **[LOGOUT_ALL_DEVICES_IMPLEMENTATION.md](./implementation/LOGOUT_ALL_DEVICES_IMPLEMENTATION.md)** - Global logout implementation
- **[DEVICE_MANAGEMENT.md](./implementation/DEVICE_MANAGEMENT.md)** - Device management system
- **[CHANGELOG_DEVICE_MANAGEMENT.md](./implementation/CHANGELOG_DEVICE_MANAGEMENT.md)** - Device management changelog
### 🔌 [Integration](./integration/)
Integration guides for external services and client applications:
- **[API_INTEGRATION.md](./integration/API_INTEGRATION.md)** - API integration guide
- **[KOTLIN_INTEGRATION_GUIDE.md](./integration/KOTLIN_INTEGRATION_GUIDE.md)** - Kotlin client integration
- **[TWILIO_SETUP.md](./integration/TWILIO_SETUP.md)** - Twilio SMS service setup
### 🗄️ [Database](./database/)
Database documentation and analysis:
- **[DATABASE_OVERVIEW.md](./database/DATABASE_OVERVIEW.md)** - Database schema overview
- **[OTP_TABLE_ANALYSIS.md](./database/OTP_TABLE_ANALYSIS.md)** - OTP table structure analysis
### 🏗️ [Architecture](./architecture/)
System architecture and design documentation:
- **[ARCHITECTURE.md](./architecture/ARCHITECTURE.md)** - System architecture overview
### 📝 [Others](./others/)
Miscellaneous documentation:
- **[GEMINI_PROMPT_AUTH_IMPLEMENTATION.md](./others/GEMINI_PROMPT_AUTH_IMPLEMENTATION.md)** - Gemini AI prompt for auth implementation
- **[GEMINI_PROMPT_CONCISE.md](./others/GEMINI_PROMPT_CONCISE.md)** - Concise Gemini AI prompt
---
## Quick Links
- **Main README**: [../README.md](../README.md) - Project overview and main documentation
- **Database Migrations**: [../db/migrations/](../db/migrations/) - Database migration scripts
---
## Contributing
When adding new documentation:
1. Place files in the appropriate category folder
2. If unsure, place in `others/` folder
3. Update this index file with a link to the new documentation

View File

@ -0,0 +1,69 @@
# 🚀 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.

View File

@ -0,0 +1,388 @@
# 🔒 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**

View File

@ -0,0 +1,362 @@
# 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

View File

@ -0,0 +1,949 @@
# 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

View File

@ -0,0 +1,342 @@
# 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.

View File

@ -0,0 +1,317 @@
# 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! ✅

View File

@ -0,0 +1,375 @@
# 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! 🎉

View File

@ -0,0 +1,348 @@
# 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()
```

View File

@ -0,0 +1,101 @@
# 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!

View File

@ -0,0 +1,270 @@
# 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.

View File

@ -0,0 +1,428 @@
# 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.

View File

@ -0,0 +1,210 @@
# 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).

View File

@ -0,0 +1,248 @@
# 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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,821 @@
# 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**
The backend may use AWS SSM Parameter Store for database credentials (server-side only). This doesn't affect your Kotlin client - you just need to configure the correct API base URL.
```kotlin
// Development/Local
val baseUrl = "http://localhost:3000"
// Test Environment
val baseUrl = "https://test-api.yourdomain.com"
// Production
val baseUrl = "https://api.yourdomain.com"
```
**Environment-based Configuration:**
```kotlin
// config/ApiConfig.kt
object ApiConfig {
val baseUrl: String
get() = when (BuildConfig.BUILD_TYPE) {
"debug" -> "http://localhost:3000" // Local development
"staging" -> "https://test-api.yourdomain.com" // Test environment
"release" -> "https://api.yourdomain.com" // Production
else -> "http://localhost:3000"
}
}
// Usage
val apiClient = AuthApiClient(ApiConfig.baseUrl, tokenManager)
```
### 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
---
## AWS SSM Parameter Store (Backend Configuration)
**Important:** AWS SSM Parameter Store is a **server-side feature only**. It's used by the backend to securely store database credentials. Your Kotlin application doesn't need to interact with AWS SSM directly.
### What AWS SSM Does (Backend)
- Stores database credentials securely in AWS
- Backend fetches credentials from SSM on startup
- No changes needed in your Kotlin client code
### What You Need to Know (Kotlin App)
1. **API Endpoints remain the same** - No changes to API calls
2. **Base URL configuration** - Use correct environment URL (see below)
3. **Authentication flow unchanged** - Same OTP and token flow
### Environment Configuration
The backend may be deployed to different environments (test/prod) which use different SSM parameter paths. Your Kotlin app should connect to the correct API endpoint:
```kotlin
// config/EnvironmentConfig.kt
enum class Environment {
DEVELOPMENT,
TEST,
PRODUCTION
}
object EnvironmentConfig {
val currentEnvironment: Environment
get() = when (BuildConfig.BUILD_TYPE) {
"debug" -> Environment.DEVELOPMENT
"staging" -> Environment.TEST
"release" -> Environment.PRODUCTION
else -> Environment.DEVELOPMENT
}
val apiBaseUrl: String
get() = when (currentEnvironment) {
Environment.DEVELOPMENT -> "http://localhost:3000"
Environment.TEST -> "https://test-api.livingai.app" // Test backend
Environment.PRODUCTION -> "https://api.livingai.app" // Production backend
}
}
// Usage in your app
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
val tokenManager = TokenManager(this)
val apiClient = AuthApiClient(
baseUrl = EnvironmentConfig.apiBaseUrl,
tokenManager = tokenManager
)
// Store in dependency injection container
// or use as singleton
}
}
```
### Build Variants Setup (build.gradle.kts)
```kotlin
android {
buildTypes {
debug {
buildConfigField("String", "API_BASE_URL", "\"http://localhost:3000\"")
}
create("staging") {
buildConfigField("String", "API_BASE_URL", "\"https://test-api.livingai.app\"")
isMinifyEnabled = false
}
release {
buildConfigField("String", "API_BASE_URL", "\"https://api.livingai.app\"")
isMinifyEnabled = true
isShrinkResources = true
}
}
}
// Then use in code
val baseUrl = BuildConfig.API_BASE_URL
```
## Production Considerations
1. **Base URL**: Use environment-based configuration (see above)
2. **Certificate Pinning**: For production, implement SSL pinning
```kotlin
// Add certificate pinning for security
val client = HttpClient(Android) {
engine {
// Configure certificate pinning
}
}
```
3. **Error Logging**: Log errors to crash reporting (Firebase Crashlytics, etc.)
4. **Network Timeout**: Set appropriate timeouts for network requests
```kotlin
val client = HttpClient(Android) {
install(HttpTimeout) {
requestTimeoutMillis = 30000 // 30 seconds
connectTimeoutMillis = 10000 // 10 seconds
}
}
```
5. **Token Refresh Strategy**: Consider proactive refresh (refresh before expiration)
```kotlin
// Refresh token proactively before expiration
suspend fun refreshTokenIfNeeded() {
val token = tokenManager.getAccessToken()
if (token != null && isTokenExpiringSoon(token)) {
apiClient.refreshTokens()
}
}
```
6. **Network Security Config**: For production, enforce HTTPS only
```xml
<!-- res/xml/network_security_config.xml -->
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>
```
---
## 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
---
## AWS SSM Integration Summary
### For Kotlin Developers
✅ **What you DON'T need to do:**
- No AWS SDK in your Kotlin app
- No SSM client code
- No changes to authentication flow
- No changes to API calls
✅ **What you DO need:**
- Configure correct base URL for each environment
- Use proper environment-based build variants
- Handle API responses as before
### Backend Configuration (Server-Side)
The backend uses AWS SSM Parameter Store to fetch database credentials:
- **Test Environment**: `/test/livingai/db/app`
- **Production Environment**: `/prod/livingai/db/app`
This is configured on the server with:
```env
USE_AWS_SSM=true
AWS_REGION=ap-south-1
```
Your Kotlin app just needs to connect to the correct API endpoint for each environment.
---
This guide provides everything you need to integrate the `/users/me` endpoint in your Kotlin application!

View File

@ -0,0 +1,201 @@
# 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

View File

@ -0,0 +1,429 @@
# 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.

View File

@ -0,0 +1,89 @@
# 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.
---

View File

@ -0,0 +1,229 @@
# 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

View File

@ -0,0 +1,85 @@
# 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)

View File

@ -0,0 +1,369 @@
# 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

View File

@ -0,0 +1,355 @@
# 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.

View File

@ -0,0 +1,675 @@
# 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.

View File

@ -0,0 +1,331 @@
# 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

View File

@ -0,0 +1,299 @@
# 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 |

View File

@ -0,0 +1,294 @@
# 🛡️ 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/)

View File

@ -0,0 +1,287 @@
# 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 = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
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

46
check-db-config.js Normal file
View File

@ -0,0 +1,46 @@
// Quick script to check current database configuration
require('dotenv').config();
console.log('🔍 Current Database Configuration:\n');
// Check if AWS SSM is enabled
const useAwsSsm = process.env.USE_AWS_SSM === 'true' || process.env.USE_AWS_SSM === '1';
console.log(`USE_AWS_SSM: ${useAwsSsm ? '✅ Enabled' : '❌ Disabled'}`);
if (useAwsSsm) {
console.log('\n📦 AWS SSM Configuration:');
console.log(` AWS_REGION: ${process.env.AWS_REGION || 'ap-south-1 (default)'}`);
console.log(` AWS_ACCESS_KEY_ID: ${process.env.AWS_ACCESS_KEY_ID ? '✅ Set' : '❌ Not set (using IAM role)'}`);
console.log(` AWS_SECRET_ACCESS_KEY: ${process.env.AWS_SECRET_ACCESS_KEY ? '✅ Set' : '❌ Not set (using IAM role)'}`);
const env = process.env.NODE_ENV || 'test';
const paramPath = (env === 'production' || env === 'prod')
? '/prod/livingai/db/app'
: '/test/livingai/db/app';
console.log(` Parameter Path: ${paramPath}`);
console.log(` NODE_ENV: ${env}`);
} else {
console.log('\n📝 DATABASE_URL Configuration:');
const dbUrl = process.env.DATABASE_URL;
if (dbUrl) {
// Parse and mask the URL for security
try {
const url = new URL(dbUrl);
const maskedUrl = `${url.protocol}//${url.username ? '***' : ''}@${url.hostname}${url.port ? ':' + url.port : ''}${url.pathname}`;
console.log(` Connection String: ${maskedUrl}`);
console.log(` Host: ${url.hostname}`);
console.log(` Port: ${url.port || '5432 (default)'}`);
console.log(` Database: ${url.pathname.replace('/', '')}`);
console.log(` User: ${url.username || 'Not specified'}`);
} catch (e) {
console.log(` Connection String: ${dbUrl.substring(0, 20)}... (masked)`);
}
} else {
console.log(' ❌ DATABASE_URL not set!');
}
}
console.log('\n💡 To switch:');
console.log(' - Use AWS SSM: Set USE_AWS_SSM=true');
console.log(' - Use DATABASE_URL: Set USE_AWS_SSM=false or unset it');

View File

@ -0,0 +1,197 @@
# AWS Systems Manager (SSM) Parameter Store Setup
This guide explains how to configure the auth service to use AWS Systems Manager Parameter Store for database credentials.
## Overview
Instead of storing database credentials in environment variables, you can store them securely in AWS SSM Parameter Store. The service will automatically fetch credentials from SSM when enabled.
## Prerequisites
1. AWS Account with SSM Parameter Store access
2. Database credentials stored in SSM Parameter Store
3. AWS credentials configured (IAM role, environment variables, or credentials file)
## SSM Parameter Structure
Store your database credentials as a JSON string in SSM Parameter Store:
**Parameter Path:**
- Test/Development: `/test/livingai/db/app`
- Production: `/prod/livingai/db/app`
**Parameter Value (JSON):**
```json
{
"user": "read_write_user",
"password": "your_password_here",
"host": "db.livingai.app",
"dbname": "livingai_test_db"
}
```
**Parameter Type:** `SecureString` (encrypted)
## Configuration
### 1. Environment Variables
Add these to your `.env` file or environment:
```env
# Enable AWS SSM integration
USE_AWS_SSM=true
# AWS Region (default: ap-south-1)
AWS_REGION=ap-south-1
# Optional: Explicit AWS credentials (only for local/testing)
# In production, use IAM roles instead
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
```
### 2. AWS Credentials Setup
#### Option A: IAM Role (Recommended for EC2/ECS/Lambda)
- Attach an IAM role to your EC2 instance, ECS task, or Lambda function
- The role should have permission to access SSM Parameter Store
**Required IAM Policy:**
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ssm:GetParameter",
"ssm:GetParameters"
],
"Resource": [
"arn:aws:ssm:ap-south-1:*:parameter/test/livingai/db/*",
"arn:aws:ssm:ap-south-1:*:parameter/prod/livingai/db/*"
]
},
{
"Effect": "Allow",
"Action": [
"kms:Decrypt"
],
"Resource": "arn:aws:kms:ap-south-1:*:key/*"
}
]
}
```
#### Option B: Environment Variables (Local/Testing)
```env
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
AWS_REGION=ap-south-1
```
#### Option C: AWS Credentials File
```bash
aws configure
```
### 3. Fallback to DATABASE_URL
If SSM is not available or `USE_AWS_SSM` is not set, the service will fall back to using `DATABASE_URL` from environment variables. This is useful for:
- Local development
- Testing environments without AWS access
- Backward compatibility
## Environment Detection
The service automatically selects the correct parameter path based on `NODE_ENV`:
- `NODE_ENV=production` or `NODE_ENV=prod``/prod/livingai/db/app`
- Any other value (including `test`, `development`, etc.) → `/test/livingai/db/app`
You can also override this by setting the environment explicitly in code.
## Usage
### Enable SSM Integration
```env
USE_AWS_SSM=true
AWS_REGION=ap-south-1
```
### Disable SSM (Use DATABASE_URL)
```env
USE_AWS_SSM=false
# or simply don't set it
DATABASE_URL=postgresql://user:password@host:5432/dbname
```
## Testing
1. **Test SSM Connection:**
```bash
# Set environment variables
export USE_AWS_SSM=true
export AWS_REGION=ap-south-1
# Start the service
npm start
```
2. **Check Logs:**
- Success: `✅ Successfully fetched DB credentials from SSM: /test/livingai/db/app`
- Fallback: `⚠️ AWS SSM not available, falling back to DATABASE_URL`
## Troubleshooting
### Error: "Failed to fetch DB credentials from SSM"
**Possible causes:**
1. **Missing IAM permissions** - Check IAM role/policy
2. **Wrong parameter path** - Verify parameter exists in SSM console
3. **Wrong region** - Ensure `AWS_REGION` matches parameter region
4. **Invalid credentials** - Check AWS credentials configuration
**Solution:**
- Verify parameter exists: `aws ssm get-parameter --name "/test/livingai/db/app" --with-decryption`
- Check IAM permissions
- Verify AWS credentials are configured correctly
### Error: "UnrecognizedClientException"
**Cause:** AWS SDK cannot find credentials
**Solution:**
- Set `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` for local testing
- Or configure IAM role for AWS services
### Service Falls Back to DATABASE_URL
**Cause:** SSM not available or not enabled
**Solution:**
- This is expected behavior for local development
- Ensure `USE_AWS_SSM=true` is set
- Verify AWS credentials are configured
## Security Best Practices
1. **Never commit AWS credentials to version control**
2. **Use IAM roles in production** (not access keys)
3. **Store parameters as SecureString** (encrypted)
4. **Use least privilege IAM policies**
5. **Rotate credentials regularly**
6. **Monitor SSM access in CloudTrail**
## Migration from DATABASE_URL
1. Store credentials in SSM Parameter Store
2. Set `USE_AWS_SSM=true`
3. Remove `DATABASE_URL` from environment (optional, service will ignore it)
4. Restart the service
The service will automatically use SSM credentials when available.

View File

@ -0,0 +1,821 @@
# 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**
The backend may use AWS SSM Parameter Store for database credentials (server-side only). This doesn't affect your Kotlin client - you just need to configure the correct API base URL.
```kotlin
// Development/Local
val baseUrl = "http://localhost:3000"
// Test Environment
val baseUrl = "https://test-api.yourdomain.com"
// Production
val baseUrl = "https://api.yourdomain.com"
```
**Environment-based Configuration:**
```kotlin
// config/ApiConfig.kt
object ApiConfig {
val baseUrl: String
get() = when (BuildConfig.BUILD_TYPE) {
"debug" -> "http://localhost:3000" // Local development
"staging" -> "https://test-api.yourdomain.com" // Test environment
"release" -> "https://api.yourdomain.com" // Production
else -> "http://localhost:3000"
}
}
// Usage
val apiClient = AuthApiClient(ApiConfig.baseUrl, tokenManager)
```
### 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
---
## AWS SSM Parameter Store (Backend Configuration)
**Important:** AWS SSM Parameter Store is a **server-side feature only**. It's used by the backend to securely store database credentials. Your Kotlin application doesn't need to interact with AWS SSM directly.
### What AWS SSM Does (Backend)
- Stores database credentials securely in AWS
- Backend fetches credentials from SSM on startup
- No changes needed in your Kotlin client code
### What You Need to Know (Kotlin App)
1. **API Endpoints remain the same** - No changes to API calls
2. **Base URL configuration** - Use correct environment URL (see below)
3. **Authentication flow unchanged** - Same OTP and token flow
### Environment Configuration
The backend may be deployed to different environments (test/prod) which use different SSM parameter paths. Your Kotlin app should connect to the correct API endpoint:
```kotlin
// config/EnvironmentConfig.kt
enum class Environment {
DEVELOPMENT,
TEST,
PRODUCTION
}
object EnvironmentConfig {
val currentEnvironment: Environment
get() = when (BuildConfig.BUILD_TYPE) {
"debug" -> Environment.DEVELOPMENT
"staging" -> Environment.TEST
"release" -> Environment.PRODUCTION
else -> Environment.DEVELOPMENT
}
val apiBaseUrl: String
get() = when (currentEnvironment) {
Environment.DEVELOPMENT -> "http://localhost:3000"
Environment.TEST -> "https://test-api.livingai.app" // Test backend
Environment.PRODUCTION -> "https://api.livingai.app" // Production backend
}
}
// Usage in your app
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
val tokenManager = TokenManager(this)
val apiClient = AuthApiClient(
baseUrl = EnvironmentConfig.apiBaseUrl,
tokenManager = tokenManager
)
// Store in dependency injection container
// or use as singleton
}
}
```
### Build Variants Setup (build.gradle.kts)
```kotlin
android {
buildTypes {
debug {
buildConfigField("String", "API_BASE_URL", "\"http://localhost:3000\"")
}
create("staging") {
buildConfigField("String", "API_BASE_URL", "\"https://test-api.livingai.app\"")
isMinifyEnabled = false
}
release {
buildConfigField("String", "API_BASE_URL", "\"https://api.livingai.app\"")
isMinifyEnabled = true
isShrinkResources = true
}
}
}
// Then use in code
val baseUrl = BuildConfig.API_BASE_URL
```
## Production Considerations
1. **Base URL**: Use environment-based configuration (see above)
2. **Certificate Pinning**: For production, implement SSL pinning
```kotlin
// Add certificate pinning for security
val client = HttpClient(Android) {
engine {
// Configure certificate pinning
}
}
```
3. **Error Logging**: Log errors to crash reporting (Firebase Crashlytics, etc.)
4. **Network Timeout**: Set appropriate timeouts for network requests
```kotlin
val client = HttpClient(Android) {
install(HttpTimeout) {
requestTimeoutMillis = 30000 // 30 seconds
connectTimeoutMillis = 10000 // 10 seconds
}
}
```
5. **Token Refresh Strategy**: Consider proactive refresh (refresh before expiration)
```kotlin
// Refresh token proactively before expiration
suspend fun refreshTokenIfNeeded() {
val token = tokenManager.getAccessToken()
if (token != null && isTokenExpiringSoon(token)) {
apiClient.refreshTokens()
}
}
```
6. **Network Security Config**: For production, enforce HTTPS only
```xml
<!-- res/xml/network_security_config.xml -->
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>
```
---
## 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
---
## AWS SSM Integration Summary
### For Kotlin Developers
✅ **What you DON'T need to do:**
- No AWS SDK in your Kotlin app
- No SSM client code
- No changes to authentication flow
- No changes to API calls
✅ **What you DO need:**
- Configure correct base URL for each environment
- Use proper environment-based build variants
- Handle API responses as before
### Backend Configuration (Server-Side)
The backend uses AWS SSM Parameter Store to fetch database credentials:
- **Test Environment**: `/test/livingai/db/app`
- **Production Environment**: `/prod/livingai/db/app`
This is configured on the server with:
```env
USE_AWS_SSM=true
AWS_REGION=ap-south-1
```
Your Kotlin app just needs to connect to the correct API endpoint for each environment.
---
This guide provides everything you need to integrate the `/users/me` endpoint in your Kotlin application!

1289
node_modules/.package-lock.json generated vendored

File diff suppressed because it is too large Load Diff

1290
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"@aws-sdk/client-ssm": "^3.948.0",
"bcrypt": "^6.0.0",
"cors": "^2.8.5",
"dotenv": "^17.2.3",

View File

@ -35,12 +35,17 @@
// WARNING: Never use '*' when credentials or tokens are involved
require('dotenv').config();
// DATABASE_URL is only required if not using AWS SSM Parameter Store
const REQUIRED_ENV = [
'DATABASE_URL',
'JWT_ACCESS_SECRET',
'JWT_REFRESH_SECRET',
];
// If using AWS SSM, DATABASE_URL is optional (credentials come from SSM)
if (process.env.USE_AWS_SSM !== 'true' && process.env.USE_AWS_SSM !== '1') {
REQUIRED_ENV.push('DATABASE_URL');
}
const missing = REQUIRED_ENV.filter((key) => !process.env[key]);
if (missing.length > 0) {
throw new Error(

117
src/db.js
View File

@ -1,17 +1,104 @@
// src/db.js
// === SECURITY HARDENING: DATABASE ACCESS LOGGING ===
// === AWS SSM INTEGRATION ===
const { Pool } = require('pg');
const config = require('./config');
const { loggedQuery } = require('./middleware/dbAccessLogger');
const { getDbCredentials, buildDatabaseUrl } = require('./utils/awsSsm');
const pool = new Pool({
connectionString: config.databaseUrl,
});
// Initialize database connection
let pool = null;
let dbInitPromise = null;
pool.on('error', (err) => {
/**
* Initialize database connection
* Tries AWS SSM Parameter Store first, falls back to DATABASE_URL
*/
async function initializeDb() {
if (pool) {
return pool; // Already initialized
}
if (dbInitPromise) {
return dbInitPromise; // Initialization in progress
}
dbInitPromise = (async () => {
let connectionString = config.databaseUrl;
// Try to get credentials from AWS SSM Parameter Store
// Only if USE_AWS_SSM is explicitly set to 'true'
if (process.env.USE_AWS_SSM === 'true' || process.env.USE_AWS_SSM === '1') {
try {
const credentials = await getDbCredentials();
if (credentials) {
connectionString = buildDatabaseUrl(credentials);
console.log('✅ Using database credentials from AWS SSM Parameter Store');
} else {
console.log('⚠️ AWS SSM not available, using DATABASE_URL from environment');
}
} catch (error) {
console.error('❌ Error fetching credentials from SSM, using DATABASE_URL:', error.message);
// Fall back to DATABASE_URL
}
} else {
console.log(' Using DATABASE_URL from environment (USE_AWS_SSM not enabled)');
}
pool = new Pool({
connectionString,
});
pool.on('error', (err) => {
console.error('Unexpected PG client error', err);
process.exit(-1);
});
});
// Test connection
try {
await pool.query('SELECT 1');
console.log('✅ Database connection established');
} catch (err) {
console.error('❌ Database connection failed:', err.message);
throw err;
}
return pool;
})();
return dbInitPromise;
}
// Auto-initialize on module load
if (process.env.USE_AWS_SSM === 'true' || process.env.USE_AWS_SSM === '1') {
initializeDb().catch((err) => {
console.error('Failed to initialize database:', err);
process.exit(1);
});
} else {
// For backward compatibility, create pool immediately if not using SSM
pool = new Pool({
connectionString: config.databaseUrl,
});
pool.on('error', (err) => {
console.error('Unexpected PG client error', err);
process.exit(-1);
});
}
/**
* Get database pool (ensures initialization if using SSM)
* @returns {Promise<Pool>} Database pool
*/
async function getPool() {
if (process.env.USE_AWS_SSM === 'true' || process.env.USE_AWS_SSM === '1') {
if (!pool) {
await initializeDb();
}
}
return pool;
}
/**
* Execute database query with optional logging and context
@ -23,7 +110,10 @@ pool.on('error', (err) => {
* - userAgent: User agent string
* @returns {Promise} - Query result
*/
function query(text, params = [], context = {}) {
async function query(text, params = [], context = {}) {
// Ensure pool is initialized
const dbPool = await getPool();
// Use logged query if logging is enabled, otherwise use direct query
const DB_ACCESS_LOGGING_ENABLED = process.env.DB_ACCESS_LOGGING_ENABLED === 'true' || process.env.DB_ACCESS_LOGGING_ENABLED === '1';
@ -31,7 +121,7 @@ function query(text, params = [], context = {}) {
return loggedQuery(text, params, context);
}
return pool.query(text, params);
return dbPool.query(text, params);
}
/**
@ -49,6 +139,17 @@ function createContextFromRequest(req) {
module.exports = {
query,
pool,
get pool() {
// For backward compatibility, return pool synchronously if available
// Otherwise, return a promise that resolves to the pool
if (pool) {
return pool;
}
if (process.env.USE_AWS_SSM === 'true' || process.env.USE_AWS_SSM === '1') {
return initializeDb();
}
return pool;
},
createContextFromRequest,
initializeDb, // Export for explicit initialization if needed
};

80
src/utils/awsSsm.js Normal file
View File

@ -0,0 +1,80 @@
// src/utils/awsSsm.js
// AWS Systems Manager Parameter Store integration for fetching secrets
const { SSMClient, GetParameterCommand } = require('@aws-sdk/client-ssm');
/**
* Get AWS SSM client
* Uses IAM role credentials in AWS, or explicit credentials from env vars for local/testing
*/
function getSsmClient() {
const region = process.env.AWS_REGION || 'ap-south-1';
// If explicit credentials provided (for local/testing), use them
if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
return new SSMClient({
region,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});
}
// Otherwise, use default credential chain (IAM role, env vars, etc.)
return new SSMClient({ region });
}
/**
* Get database credentials from AWS SSM Parameter Store
* @param {string} environment - 'test' or 'prod' (defaults to NODE_ENV or 'test')
* @returns {Promise<Object>} Database credentials {user, password, host, dbname}
*/
async function getDbCredentials(environment = null) {
const env = environment || process.env.NODE_ENV || 'test';
const paramPath = env === 'production' || env === 'prod'
? '/prod/livingai/db/app'
: '/test/livingai/db/app';
try {
const client = getSsmClient();
const command = new GetParameterCommand({
Name: paramPath,
WithDecryption: true, // Crucial: decrypts the SecureString
});
const response = await client.send(command);
const credentials = JSON.parse(response.Parameter.Value);
console.log(`✅ Successfully fetched DB credentials from SSM: ${paramPath}`);
return credentials;
} catch (error) {
console.error(`❌ Failed to fetch DB credentials from SSM (${paramPath}):`, error.message);
// If SSM is not available (local dev), return null to use DATABASE_URL fallback
if (error.name === 'UnrecognizedClientException' ||
error.name === 'AccessDeniedException' ||
error.code === 'CredentialsError') {
console.warn('⚠️ AWS SSM not available, falling back to DATABASE_URL');
return null;
}
throw error;
}
}
/**
* Build database connection string from credentials
* @param {Object} credentials - {user, password, host, dbname}
* @returns {string} PostgreSQL connection string
*/
function buildDatabaseUrl(credentials) {
const { user, password, host, dbname } = credentials;
return `postgresql://${user}:${encodeURIComponent(password)}@${host}/${dbname}`;
}
module.exports = {
getDbCredentials,
buildDatabaseUrl,
getSsmClient,
};