From 4121b99775a2e2a53716a2dddc8a081cedfcb69a Mon Sep 17 00:00:00 2001 From: Chandresh Kerkar Date: Sun, 21 Dec 2025 02:18:37 +0530 Subject: [PATCH] Updated For Verfy JWT --- AUTH_FLOW.md | 188 +++++++++++++++ AUTH_IMPLEMENTATION.md | 424 ++++++++++++++++++++++++++++++++++ SETUP_AUTH.md | 132 +++++++++++ middleware/coarseAuthorize.js | 153 ++++++++++++ middleware/fineAuthorize.js | 200 ++++++++++++++++ middleware/jwtAuthenticate.js | 130 +++++++++++ middleware/rateLimiter.js | 205 ++++++++++++++++ middleware/requestContext.js | 33 +++ package.json | 2 + routes/userRoutes.js | 104 ++++++--- server.js | 23 ++ services/auditLogger.js | 106 +++++++++ 12 files changed, 1672 insertions(+), 28 deletions(-) create mode 100644 AUTH_FLOW.md create mode 100644 AUTH_IMPLEMENTATION.md create mode 100644 SETUP_AUTH.md create mode 100644 middleware/coarseAuthorize.js create mode 100644 middleware/fineAuthorize.js create mode 100644 middleware/jwtAuthenticate.js create mode 100644 middleware/rateLimiter.js create mode 100644 middleware/requestContext.js create mode 100644 services/auditLogger.js diff --git a/AUTH_FLOW.md b/AUTH_FLOW.md new file mode 100644 index 0000000..944e467 --- /dev/null +++ b/AUTH_FLOW.md @@ -0,0 +1,188 @@ +# Authentication Flow - BuySellService → Auth Service + +## Overview + +BuySellService **does NOT validate JWT tokens locally**. Instead, it calls the auth service API to validate and authorize every request. + +## Flow Diagram + +``` +Android App → BuySellService → Auth Service → BuySellService → Response + | | | | + | | | | + [Token] [Extract Token] [Validate Token] [User Info] + | | | | + | | | | + └──────────────┴────────────────┴───────────────┘ +``` + +## Detailed Flow + +### 1. Request Arrives at BuySellService + +**Android App sends:** +``` +GET http://localhost:3200/users/user-123 +Authorization: Bearer eyJhbGc... +``` + +### 2. BuySellService Middleware Chain + +**Step 1: requestContext** +- Extracts IP, User-Agent, generates Request-ID + +**Step 2: auditLogger** +- Attaches audit logger to request + +**Step 3: jwtAuthenticate** ⭐ **CALLS AUTH SERVICE** +- Extracts token from `Authorization: Bearer ` header +- **Calls auth service**: `POST http://localhost:3000/auth/validate-token` +- **Request body**: `{ token: "eyJhbGc..." }` +- **Waits for response** + +### 3. Auth Service Validates Token + +**Auth Service receives:** +```json +POST /auth/validate-token +Body: { "token": "eyJhbGc..." } +``` + +**Auth Service validates:** +- ✅ Token signature (using JWT secret) +- ✅ Token expiry (`exp` claim) +- ✅ Issuer (`iss` claim) +- ✅ Audience (`aud` claim) +- ✅ Token version (checks database for logout-all-devices) +- ✅ User exists in database + +**Auth Service responds (if valid):** +```json +{ + "valid": true, + "payload": { + "sub": "user-123", + "role": "user", + "user_type": "seller", + "phone_number": "+919876543210", + "token_version": 1, + "high_assurance": false + } +} +``` + +**Auth Service responds (if invalid):** +```json +{ + "valid": false, + "error": "Invalid or expired token" +} +``` + +### 4. BuySellService Receives Response + +**If valid:** +- Extracts user info from `payload` +- Sets `req.user = { userId: "user-123", role: "user", ... }` +- Logs successful authentication +- Continues to next middleware + +**If invalid:** +- Returns `401 Unauthorized` +- Logs failed authentication +- Stops request processing + +### 5. Continue with Authorization + +**After authentication:** +- Rate limiting (checks limits for userId) +- Coarse-grained authorization (checks role) +- Fine-grained authorization (checks resource ownership) +- Route handler (fetches user data) +- Response returned to Android app + +## Code Flow + +```javascript +// Backend/middleware/jwtAuthenticate.js + +async function jwtAuthenticate(req, res, next) { + // 1. Extract token + const token = req.headers.authorization?.slice(7); // "Bearer " + + // 2. Call auth service + const response = await axios.post( + 'http://localhost:3000/auth/validate-token', + { token } + ); + + // 3. Check response + if (response.data.valid === true) { + // 4. Extract user info + req.user = { + userId: response.data.payload.sub, + role: response.data.payload.role, + // ... other fields + }; + next(); // Continue to next middleware + } else { + // 5. Return 401 if invalid + return res.status(401).json({ error: 'Unauthorized' }); + } +} +``` + +## Benefits + +✅ **Centralized Validation**: All token validation logic in one place (auth service) +✅ **No Secret Sharing**: BuySellService doesn't need JWT secret +✅ **Token Version Checking**: Auth service checks database for logout-all-devices +✅ **Consistent**: All services use same validation logic +✅ **Easy Updates**: Update validation logic once in auth service + +## Error Handling + +**Auth Service Unavailable:** +- Returns `401 Unauthorized` with message: "Authentication service unavailable" +- Request is blocked (fail closed for security) + +**Token Invalid:** +- Returns `401 Unauthorized` with error details +- Request is blocked + +**Timeout:** +- 5 second timeout by default (configurable via `AUTH_SERVICE_TIMEOUT`) +- Returns `401 Unauthorized` if timeout exceeded + +## Configuration + +**Required Environment Variables:** +```env +AUTH_SERVICE_URL=http://localhost:3000 +``` + +**Optional:** +```env +AUTH_SERVICE_TIMEOUT=5000 # milliseconds +``` + +## Testing + +**Test auth service endpoint directly:** +```bash +# Valid token +curl -X POST http://localhost:3000/auth/validate-token \ + -H "Content-Type: application/json" \ + -d '{"token":"your_valid_token_here"}' + +# Response: +# {"valid":true,"payload":{"sub":"user-123","role":"user",...}} +``` + +**Test through BuySellService:** +```bash +curl -X GET http://localhost:3200/users/user-123 \ + -H "Authorization: Bearer your_valid_token_here" +``` + +The BuySellService will call the auth service internally to validate the token. diff --git a/AUTH_IMPLEMENTATION.md b/AUTH_IMPLEMENTATION.md new file mode 100644 index 0000000..1df2ed4 --- /dev/null +++ b/AUTH_IMPLEMENTATION.md @@ -0,0 +1,424 @@ +# Authentication & Authorization Implementation + +This document describes the complete authentication and authorization system implemented for BuySellService. + +## Overview + +The system implements a comprehensive security middleware chain that: +1. ✅ Validates JWT tokens from Authorization header +2. ✅ Applies rate limiting (per userId, fallback to IP) +3. ✅ Enforces coarse-grained authorization (route level) +4. ✅ Enforces fine-grained authorization (business logic) +5. ✅ Logs all API requests for audit + +## Middleware Chain Order + +The middleware is applied in this **critical order**: + +``` +1. requestContext → Extract IP, user agent, request ID +2. auditLogger → Attach audit logger to request +3. jwtAuthenticate → Validate JWT token, extract user info +4. rateLimiter → Apply rate limiting +5. coarseAuthorize → Route-level role checking +6. fineAuthorize → Business logic authorization (per route) +7. routeHandler → Actual business logic +``` + +## Implementation Details + +### 1. Request Context (`middleware/requestContext.js`) + +**Purpose**: Extract and attach request metadata + +**Extracts**: +- Client IP (considers proxies) +- User agent +- Request ID (for tracing) +- Request timestamp + +**Usage**: Applied globally first in middleware chain + +--- + +### 2. JWT Authentication (`middleware/jwtAuthenticate.js`) + +**Purpose**: Validate JWT tokens and extract user information + +**Validates**: +- ✅ Token signature +- ✅ Token expiry +- ✅ Issuer (iss) +- ✅ Audience (aud) + +**Token Validation**: +- **Calls auth service API**: All token validation is done by the auth service +- **Endpoint**: `POST /auth/validate-token` on auth service +- **Auth service validates**: Signature, expiry, issuer, audience, token_version (for logout-all) +- **Response**: Returns `{ valid: true, payload: {...} }` or `{ valid: false, error: "..." }` + +**Extracts to `req.user`**: +```javascript +{ + userId: payload.sub, // User ID + role: payload.role, // User role (USER, ADMIN) + userType: payload.user_type, + phoneNumber: payload.phone_number, + tokenVersion: payload.token_version, + highAssurance: payload.high_assurance, + tenantId: payload.tenant_id, // For multi-tenant apps +} +``` + +**Error Responses**: +- `401 Unauthorized` - Missing or invalid token + +**Environment Variables**: +- `AUTH_SERVICE_URL` (required) - URL of the auth service (default: 'http://localhost:3000') +- `AUTH_SERVICE_TIMEOUT` (optional) - Timeout for auth service calls in ms (default: 5000) + +--- + +### 3. Rate Limiting (`middleware/rateLimiter.js`) + +**Purpose**: Prevent API abuse by limiting requests per user/IP + +**Strategy**: +- **Preferred**: Rate limit per `userId` (if authenticated) +- **Fallback**: Rate limit per IP (if not authenticated) + +**Storage**: +- **Redis** (if available) - Distributed rate limiting +- **In-memory** (fallback) - Single-instance rate limiting + +**Pre-configured Limiters**: +- `rateLimiterRead` - 100 requests per 15 minutes (GET operations) +- `rateLimiterWrite` - 20 requests per 15 minutes (POST, PUT, DELETE) +- `rateLimiterSensitive` - 10 requests per hour (sensitive operations) + +**Response Headers**: +``` +X-RateLimit-Limit: 100 +X-RateLimit-Remaining: 95 +X-RateLimit-Reset: 2024-01-20T15:30:00Z +X-RateLimit-Type: read +``` + +**Error Responses**: +- `429 Too Many Requests` - Rate limit exceeded + +**Environment Variables**: +- `REDIS_URL` (optional) - Redis connection URL +- `RATE_LIMIT_MAX_REQUESTS` (optional, default: 100) +- `RATE_LIMIT_WINDOW_SECONDS` (optional, default: 900) + +--- + +### 4. Coarse-Grained Authorization (`middleware/coarseAuthorize.js`) + +**Purpose**: Route-level role-based access control + +**Route-to-Role Mapping**: +```javascript +'/admin/*' → ['ADMIN'] +'/users/*' → ['USER', 'ADMIN'] +'/orders/*' → ['USER', 'ADMIN'] +'/listings/*' → ['USER', 'ADMIN'] +'/locations/*' → ['USER', 'ADMIN'] +'/chat/*' → ['USER', 'ADMIN'] +'*' → ['USER', 'ADMIN'] // Default +``` + +**Usage**: +```javascript +// Use route mapping +const requireUserOrAdmin = createCoarseAuthorize(); + +// Or specify roles explicitly +const requireAdmin = createCoarseAuthorize(['ADMIN']); +``` + +**Error Responses**: +- `403 Forbidden` - User role doesn't match required roles + +--- + +### 5. Fine-Grained Authorization (`middleware/fineAuthorize.js`) + +**Purpose**: Business logic level authorization (resource ownership, action permissions) + +**Authorization Rules**: + +**Users**: +- ✅ Users can read their own profile +- ✅ Users can update their own profile +- ❌ Users cannot access other users' data (unless admin) + +**Orders**: +- ✅ Users can create orders +- ✅ Users can read/update/delete their own orders +- ❌ Users cannot access other users' orders + +**Listings**: +- ✅ Users can create listings +- ✅ Users can read all active listings (public) +- ✅ Users can update/delete their own listings + +**Usage**: +```javascript +router.get('/users/:id', + fineAuthorize({ + action: 'read', + resource: 'user', + getResourceOwnerId: (req) => req.params.id, + }), + handler +); +``` + +**Helper Function**: +```javascript +const result = authorizeAction({ + user: req.user, + action: 'read', + resource: 'user', + resourceOwnerId: userId, +}); + +if (!result.authorized) { + // Handle unauthorized +} +``` + +**Error Responses**: +- `403 Forbidden` - Action not authorized on resource + +--- + +### 6. Audit Logging (`services/auditLogger.js`) + +**Purpose**: Log all API requests for security auditing + +**Logged Information**: +- Timestamp +- Request ID +- User ID +- Action +- Route +- HTTP Method +- Status (success, failed, forbidden, etc.) +- Client IP +- User Agent +- Additional metadata + +**Usage**: +```javascript +// Automatic logging via middleware +req.auditLogger.logSuccess('get_user', { userId: id }); +req.auditLogger.logFailure('get_user', 'User not found', { userId: id }); +req.auditLogger.logForbidden('get_user', 'Access denied', { userId: id }); +``` + +**Storage**: +- Currently logs to console and in-memory store +- TODO: Integrate with external logging service (CloudWatch, Elasticsearch, etc.) + +--- + +## Example: GET /users/:userId Endpoint + +Here's how the complete flow works for `GET /users/:userId`: + +``` +1. Request arrives with: Authorization: Bearer + +2. requestContext + → Extracts IP: 192.168.1.1 + → Extracts User-Agent: Android App + → Generates Request-ID: abc123 + +3. auditLogger + → Attaches logger to req.auditLogger + +4. jwtAuthenticate + → Validates JWT token + → Extracts: userId = "user-123", role = "USER" + → Sets: req.user = { userId: "user-123", role: "USER", ... } + +5. rateLimiterRead + → Checks rate limit for user-123 + → Count: 5/100 (under limit) + → Sets headers: X-RateLimit-Remaining: 95 + +6. coarseAuthorize + → Checks route: /users/:id + → Required roles: ['USER', 'ADMIN'] + → User role: 'USER' ✅ + → Allows request + +7. fineAuthorize + → Action: 'read' + → Resource: 'user' + → Resource owner: req.params.id = "user-456" + → Checks: Can user-123 read user-456? + → Rule: Users can only read their own profile + → Result: user-123 !== user-456 → ❌ Unauthorized + → Returns: 403 Forbidden (unless user-123 === user-456 or user-123 is ADMIN) + +8. routeHandler + → Fetches user from database + → Returns user data + → Logs success via req.auditLogger +``` + +--- + +## Configuration + +### Environment Variables + +Create a `.env` file: + +```env +# Server +PORT=3200 +TRUST_PROXY=false + +# JWT (REQUIRED - must match auth service) +JWT_ACCESS_SECRET=your_jwt_secret_here +JWT_ISSUER=farm-auth-service +JWT_AUDIENCE=mobile-app + +# Rate Limiting (Optional) +REDIS_URL=redis://localhost:6379 +RATE_LIMIT_READ_MAX=100 +RATE_LIMIT_READ_WINDOW=900 + +# Auth Service (Optional - for centralized validation) +VALIDATE_VIA_AUTH_SERVICE=false +AUTH_SERVICE_URL=http://localhost:3000 +``` + +--- + +## Testing + +### Test with Valid Token + +```bash +# Get token from auth service (via login) +TOKEN="your_access_token_here" + +# Test GET /users/:userId +curl -X GET http://localhost:3200/users/user-123 \ + -H "Authorization: Bearer $TOKEN" +``` + +### Test without Token (Should return 401) + +```bash +curl -X GET http://localhost:3200/users/user-123 +# Returns: 401 Unauthorized +``` + +### Test with Invalid Token (Should return 401) + +```bash +curl -X GET http://localhost:3200/users/user-123 \ + -H "Authorization: Bearer invalid_token" +# Returns: 401 Unauthorized +``` + +### Test Rate Limiting (Should return 429 after limit) + +```bash +# Make many rapid requests +for i in {1..101}; do + curl -X GET http://localhost:3200/users/user-123 \ + -H "Authorization: Bearer $TOKEN" +done +# After 100 requests, returns: 429 Too Many Requests +``` + +--- + +## Security Features + +✅ **JWT Token Validation**: Signature, expiry, issuer, audience +✅ **Rate Limiting**: Per-user and per-IP +✅ **Role-Based Access Control**: Route-level and resource-level +✅ **Audit Logging**: All requests logged with context +✅ **No Cookies**: Uses Bearer tokens only +✅ **No Server-Side Token Storage**: Stateless JWT validation +✅ **HTTPS Assumed**: No certificate handling in app code + +--- + +## Files Structure + +``` +Backend/ +├── middleware/ +│ ├── requestContext.js # Request metadata extraction +│ ├── jwtAuthenticate.js # JWT token validation +│ ├── rateLimiter.js # Rate limiting +│ ├── coarseAuthorize.js # Route-level authorization +│ └── fineAuthorize.js # Business logic authorization +├── services/ +│ └── auditLogger.js # Audit logging service +├── routes/ +│ └── userRoutes.js # User routes with auth middleware +└── server.js # Server setup with middleware chain +``` + +--- + +## Next Steps + +1. **Install Dependencies**: + ```bash + cd Backend + npm install + ``` + +2. **Configure Environment**: + ```bash + cp .env.example .env + # Edit .env with your JWT_ACCESS_SECRET from auth service + ``` + +3. **Start Server**: + ```bash + npm start + ``` + +4. **Test Endpoint**: + - Use Android app to call GET /users/:userId + - Verify authentication and authorization work correctly + +--- + +## Integration with Auth Service + +The BuySellService calls the auth service API to validate JWT tokens. All token validation logic is centralized in the auth service. + +**Token Flow**: +1. User logs in via auth service → Receives JWT token +2. Android app stores token securely +3. Android app sends token in `Authorization: Bearer ` header to BuySellService +4. BuySellService extracts token and calls auth service: `POST /auth/validate-token` +5. Auth service validates token (signature, expiry, token_version, claims) and returns user info +6. BuySellService receives validated user info and attaches to `req.user` +7. BuySellService continues with authorization checks and processes request + +**Auth Service Endpoint**: +- `POST /auth/validate-token` +- Request: `{ token: "..." }` +- Response (valid): `{ valid: true, payload: { sub, role, user_type, ... } }` +- Response (invalid): `{ valid: false, error: "..." }` + +**Benefits of Centralized Validation**: +- ✅ Single source of truth for token validation +- ✅ Token version checking (for logout-all functionality) handled centrally +- ✅ No need to share JWT secrets between services +- ✅ Easier to update validation logic in one place diff --git a/SETUP_AUTH.md b/SETUP_AUTH.md new file mode 100644 index 0000000..05b5a49 --- /dev/null +++ b/SETUP_AUTH.md @@ -0,0 +1,132 @@ +# Quick Setup Guide - Authentication & Authorization + +## 1. Install Dependencies + +```bash +cd Backend +npm install +``` + +This will install: +- `jsonwebtoken` - For JWT token validation +- `axios` - For optional auth service API calls +- `redis` - For distributed rate limiting (optional) + +## 2. Configure Environment Variables + +The JWT secret **must match** the auth service secret: + +```env +# Copy from farm-auth-service/.env +JWT_ACCESS_SECRET=add74b258202057143382e8ee9ecc24a1114eddd3da5db79f3d29d24d7083043 +``` + +Create `.env` file in `Backend/` directory: + +```env +PORT=3200 +TRUST_PROXY=false + +# Auth Service Configuration (REQUIRED) +# BuySellService calls this to validate tokens +AUTH_SERVICE_URL=http://localhost:3000 +AUTH_SERVICE_TIMEOUT=5000 + +# Optional: Redis for rate limiting (if not set, uses in-memory) +# REDIS_URL=redis://localhost:6379 +``` + +## 3. Start the Server + +```bash +npm start +``` + +Server will start on `http://localhost:3200` + +## 4. Test the Implementation + +### Test from Android App + +The Android app's `getUserById` function will: +1. Send JWT token in `Authorization: Bearer ` header +2. Call `GET http://localhost:3200/users/:userId` +3. Backend validates token → applies rate limiting → checks authorization → returns user data + +### Test with cURL + +```bash +# 1. Get token from auth service (via login) +TOKEN="your_access_token_from_login" + +# 2. Test GET /users/:userId +curl -X GET http://localhost:3200/users/YOUR_USER_ID \ + -H "Authorization: Bearer $TOKEN" + +# Expected: Returns user data in JSON format +``` + +### Test Error Cases + +```bash +# No token (should return 401) +curl -X GET http://localhost:3200/users/YOUR_USER_ID +# Response: {"error":"Unauthorized","message":"Missing Authorization header..."} + +# Invalid token (should return 401) +curl -X GET http://localhost:3200/users/YOUR_USER_ID \ + -H "Authorization: Bearer invalid_token" +# Response: {"error":"Unauthorized","message":"Invalid or expired token"} + +# Access other user's profile (should return 403 if not admin) +curl -X GET http://localhost:3200/users/OTHER_USER_ID \ + -H "Authorization: Bearer $TOKEN" +# Response: {"error":"Forbidden","message":"Cannot access other users' data"} +``` + +## 5. Verify End-to-End Flow + +1. **Login via Auth Service** → Get JWT token +2. **Android app calls** `GET /users/:userId` with token +3. **Backend validates** token → ✅ Authenticated +4. **Backend checks rate limit** → ✅ Under limit +5. **Backend checks authorization** → ✅ User can access their own profile +6. **Backend fetches user** from database +7. **Backend returns** JSON response +8. **Android app displays** JSON in dialog + +## 6. Monitor Logs + +All requests are logged with: +- User ID +- Action +- Route +- Status (success/failed/forbidden) +- Timestamp + +Check console output for audit logs: +``` +[AUDIT] {"timestamp":"...","userId":"...","action":"get_user","route":"/users/...","status":"success",...} +``` + +## Troubleshooting + +### Issue: 401 Unauthorized +- Check that `AUTH_SERVICE_URL` is correct and auth service is running +- Verify token is valid and not expired +- Check token is sent in correct format: `Authorization: Bearer ` +- Check auth service logs for validation errors + +### Issue: 403 Forbidden +- User is trying to access another user's data +- Check user role matches required roles for route +- Admins can access any user data + +### Issue: 429 Too Many Requests +- Rate limit exceeded +- Wait for rate limit window to reset +- Check `X-RateLimit-Reset` header for reset time + +### Issue: Redis Connection Errors +- Redis is optional - rate limiting will use in-memory store if Redis unavailable +- Check `REDIS_URL` in `.env` if you want to use Redis diff --git a/middleware/coarseAuthorize.js b/middleware/coarseAuthorize.js new file mode 100644 index 0000000..ac08516 --- /dev/null +++ b/middleware/coarseAuthorize.js @@ -0,0 +1,153 @@ +// middleware/coarseAuthorize.js +/** + * Coarse-Grained Authorization Middleware (Route Level) + * + * Blocks access if user's role doesn't match required roles for the route + * Returns 403 Forbidden if unauthorized + * + * Route-to-role mapping: + * /admin/* → ADMIN + * /users/* → USER, ADMIN + * /orders/* → USER, ADMIN + * etc. + */ + +/** + * Route-to-Role Mapping Configuration + * Maps route patterns to allowed roles + */ +const ROUTE_ROLE_MAP = { + // Admin routes require ADMIN role + '/admin': ['ADMIN'], + '/admin/*': ['ADMIN'], + + // User routes allow USER and ADMIN + '/users': ['USER', 'ADMIN'], + '/users/*': ['USER', 'ADMIN'], + + // Order routes allow USER and ADMIN + '/orders': ['USER', 'ADMIN'], + '/orders/*': ['USER', 'ADMIN'], + + // Listing routes allow USER and ADMIN + '/listings': ['USER', 'ADMIN'], + '/listings/*': ['USER', 'ADMIN'], + + // Location routes allow USER and ADMIN + '/locations': ['USER', 'ADMIN'], + '/locations/*': ['USER', 'ADMIN'], + + // Chat routes allow USER and ADMIN + '/chat': ['USER', 'ADMIN'], + '/chat/*': ['USER', 'ADMIN'], + + // Default: Allow all authenticated users + '*': ['USER', 'ADMIN'], +}; + +/** + * Get required roles for a route + */ +function getRequiredRoles(path) { + // Check exact matches first + if (ROUTE_ROLE_MAP[path]) { + return ROUTE_ROLE_MAP[path]; + } + + // Check wildcard patterns + for (const [pattern, roles] of Object.entries(ROUTE_ROLE_MAP)) { + if (pattern.includes('*')) { + const regexPattern = pattern.replace(/\*/g, '.*'); + const regex = new RegExp(`^${regexPattern}$`); + if (regex.test(path)) { + return roles; + } + } + } + + // Default: allow all authenticated users + return ROUTE_ROLE_MAP['*'] || ['USER', 'ADMIN']; +} + +/** + * Normalize role to standard format + */ +function normalizeRole(role) { + if (!role) return null; + + // Map common role variations to standard roles + const roleMap = { + 'user': 'USER', + 'admin': 'ADMIN', + 'USER': 'USER', + 'ADMIN': 'ADMIN', + 'security_admin': 'ADMIN', + 'moderator': 'ADMIN', + }; + + return roleMap[role.toUpperCase()] || role.toUpperCase(); +} + +/** + * Coarse-Grained Authorization Middleware Factory + * + * @param {string[]} allowedRoles - Roles allowed to access this route (optional, uses route mapping if not provided) + * @returns {Function} Express middleware + */ +function createCoarseAuthorize(allowedRoles = null) { + return function coarseAuthorize(req, res, next) { + try { + // User should be set by jwtAuthenticate middleware + if (!req.user || !req.user.userId) { + return res.status(401).json({ + error: 'Unauthorized', + message: 'Authentication required', + }); + } + + const userRole = normalizeRole(req.user.role); + + // Determine required roles for this route + const requiredRoles = allowedRoles || getRequiredRoles(req.path); + const normalizedRequiredRoles = requiredRoles.map(normalizeRole); + + // Check if user's role is in the allowed roles list + if (!normalizedRequiredRoles.includes(userRole)) { + // Log authorization failure + if (req.auditLogger) { + req.auditLogger.log({ + userId: req.user.userId, + action: 'authorization_failed', + route: req.path, + status: 'forbidden', + meta: { + userRole, + requiredRoles: normalizedRequiredRoles, + }, + }); + } + + return res.status(403).json({ + error: 'Forbidden', + message: `Access denied. Required roles: ${normalizedRequiredRoles.join(', ')}. Your role: ${userRole}`, + }); + } + + next(); + } catch (err) { + console.error('Authorization error:', err); + return res.status(500).json({ + error: 'Internal server error', + message: 'Authorization check failed', + }); + } + }; +} + +/** + * Pre-configured authorization middlewares for common cases + */ +export const requireUser = createCoarseAuthorize(['USER', 'ADMIN']); +export const requireAdmin = createCoarseAuthorize(['ADMIN']); + +export default createCoarseAuthorize; diff --git a/middleware/fineAuthorize.js b/middleware/fineAuthorize.js new file mode 100644 index 0000000..cb00ce4 --- /dev/null +++ b/middleware/fineAuthorize.js @@ -0,0 +1,200 @@ +// middleware/fineAuthorize.js +/** + * Fine-Grained Authorization Helpers + * + * Provides reusable authorization functions for business logic + * Checks if user can perform specific actions on resources + */ + +/** + * Authorize action on resource + * + * @param {Object} params + * @param {Object} params.user - User object from req.user + * @param {string} params.action - Action being performed (e.g., 'read', 'write', 'delete', 'create') + * @param {string} params.resource - Resource type (e.g., 'order', 'user', 'listing') + * @param {string} params.resourceOwnerId - ID of the resource owner (optional) + * @param {Object} params.resourceData - Additional resource data for context (optional) + * @returns {Object} { authorized: boolean, reason?: string } + */ +export function authorizeAction({ user, action, resource, resourceOwnerId = null, resourceData = {} }) { + if (!user || !user.userId) { + return { authorized: false, reason: 'User not authenticated' }; + } + + const userRole = (user.role || '').toUpperCase(); + const isAdmin = userRole === 'ADMIN'; + + // Admin can do everything + if (isAdmin) { + return { authorized: true }; + } + + // Resource-specific authorization rules + switch (resource.toLowerCase()) { + case 'user': + return authorizeUserAction({ user, action, resourceOwnerId }); + + case 'order': + return authorizeOrderAction({ user, action, resourceOwnerId, resourceData }); + + case 'listing': + return authorizeListingAction({ user, action, resourceOwnerId, resourceData }); + + case 'location': + return authorizeLocationAction({ user, action, resourceOwnerId }); + + default: + // Default: Users can access their own resources, admins can access all + if (resourceOwnerId && resourceOwnerId === user.userId) { + return { authorized: true }; + } + return { authorized: false, reason: `Access denied for ${resource}` }; + } +} + +/** + * Authorize user-related actions + */ +function authorizeUserAction({ user, action, resourceOwnerId }) { + // Users can read their own profile + if (action === 'read' && resourceOwnerId === user.userId) { + return { authorized: true }; + } + + // Users can update their own profile + if (action === 'write' && resourceOwnerId === user.userId) { + return { authorized: true }; + } + + // Users cannot delete accounts (should go through special endpoint) + if (action === 'delete') { + return { authorized: false, reason: 'Account deletion requires special process' }; + } + + // Users cannot access other users' data + if (resourceOwnerId && resourceOwnerId !== user.userId) { + return { authorized: false, reason: 'Cannot access other users\' data' }; + } + + return { authorized: true }; +} + +/** + * Authorize order-related actions + */ +function authorizeOrderAction({ user, action, resourceOwnerId, resourceData }) { + // Users can create orders + if (action === 'create') { + // Verify user can create order for their tenantId if multi-tenant + if (resourceData.tenantId && resourceData.tenantId !== user.tenantId) { + return { authorized: false, reason: 'Cannot create order for different tenant' }; + } + return { authorized: true }; + } + + // Users can read/update/delete their own orders + if (resourceOwnerId === user.userId) { + return { authorized: true }; + } + + // Users cannot access other users' orders + return { authorized: false, reason: 'Cannot access other users\' orders' }; +} + +/** + * Authorize listing-related actions + */ +function authorizeListingAction({ user, action, resourceOwnerId, resourceData }) { + // Users can create listings + if (action === 'create') { + return { authorized: true }; + } + + // Users can read all active listings (public) + if (action === 'read' && resourceData.isActive !== false) { + return { authorized: true }; + } + + // Users can update/delete their own listings + if ((action === 'write' || action === 'delete') && resourceOwnerId === user.userId) { + return { authorized: true }; + } + + // Users cannot modify other users' listings + if (resourceOwnerId && resourceOwnerId !== user.userId) { + return { authorized: false, reason: 'Cannot modify other users\' listings' }; + } + + return { authorized: true }; +} + +/** + * Authorize location-related actions + */ +function authorizeLocationAction({ user, action, resourceOwnerId }) { + // Users can manage their own locations + if (resourceOwnerId === user.userId) { + return { authorized: true }; + } + + // Users cannot access other users' locations + return { authorized: false, reason: 'Cannot access other users\' locations' }; +} + +/** + * Express middleware wrapper for fine-grained authorization + * + * Usage: + * app.get('/orders/:orderId', + * fineAuthorize({ action: 'read', resource: 'order', getResourceOwnerId: (req) => req.params.orderId }), + * handler + * ) + */ +export function fineAuthorize({ action, resource, getResourceOwnerId, getResourceData }) { + return async function(req, res, next) { + try { + const resourceOwnerId = getResourceOwnerId ? await getResourceOwnerId(req) : null; + const resourceData = getResourceData ? await getResourceData(req) : {}; + + const result = authorizeAction({ + user: req.user, + action, + resource, + resourceOwnerId, + resourceData, + }); + + if (!result.authorized) { + // Log authorization failure + if (req.auditLogger) { + req.auditLogger.log({ + userId: req.user.userId, + action: 'fine_authorization_failed', + route: req.path, + status: 'forbidden', + meta: { + action, + resource, + resourceOwnerId, + reason: result.reason, + }, + }); + } + + return res.status(403).json({ + error: 'Forbidden', + message: result.reason || 'Access denied', + }); + } + + next(); + } catch (err) { + console.error('Fine authorization error:', err); + return res.status(500).json({ + error: 'Internal server error', + message: 'Authorization check failed', + }); + } + }; +} diff --git a/middleware/jwtAuthenticate.js b/middleware/jwtAuthenticate.js new file mode 100644 index 0000000..15b5bac --- /dev/null +++ b/middleware/jwtAuthenticate.js @@ -0,0 +1,130 @@ +// middleware/jwtAuthenticate.js +/** + * JWT Authentication Middleware + * + * Validates JWT tokens by calling the auth service API + * Auth service handles all token validation (signature, expiry, issuer, audience, token_version) + * Extracts and attaches user information to req.user + * + * Flow: + * 1. Extract token from Authorization header + * 2. Call auth service /auth/validate-token endpoint + * 3. Auth service validates token and returns user info + * 4. Attach user info to req.user and continue + */ + +import axios from 'axios'; + +const AUTH_SERVICE_URL = process.env.AUTH_SERVICE_URL || 'http://localhost:3000'; +const AUTH_SERVICE_TIMEOUT = parseInt(process.env.AUTH_SERVICE_TIMEOUT || '5000', 10); // 5 seconds default + +/** + * Validate JWT token via auth service API + */ +async function validateTokenViaAuthService(token) { + try { + const response = await axios.post( + `${AUTH_SERVICE_URL}/auth/validate-token`, + { token }, + { + timeout: AUTH_SERVICE_TIMEOUT, + headers: { + 'Content-Type': 'application/json', + }, + } + ); + + if (response.data.valid === true) { + return { + valid: true, + payload: response.data.payload, + }; + } else { + return { + valid: false, + error: response.data.error || 'Token validation failed' + }; + } + } catch (err) { + // Handle different error types + if (err.response) { + // Auth service returned an error response + console.error('Auth service validation error:', err.response.status, err.response.data); + return { + valid: false, + error: err.response.data?.error || 'Token validation failed' + }; + } else if (err.request) { + // Request was made but no response received + console.error('Auth service unavailable:', err.message); + return { + valid: false, + error: 'Authentication service unavailable' + }; + } else { + // Error setting up request + console.error('Error calling auth service:', err.message); + return { + valid: false, + error: 'Failed to validate token' + }; + } + } +} + +/** + * JWT Authentication Middleware + * + * Calls auth service to validate token and authorize the request + */ +async function jwtAuthenticate(req, res, next) { + // Extract token from Authorization header + const authHeader = req.headers.authorization || ''; + const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null; + + if (!token) { + return res.status(401).json({ + error: 'Unauthorized', + message: 'Missing Authorization header. Expected format: Authorization: Bearer ' + }); + } + + // Validate token via auth service API + const validationResult = await validateTokenViaAuthService(token); + + if (!validationResult.valid) { + // Log failed authentication attempt + if (req.auditLogger) { + req.auditLogger.logFailure('authenticate', validationResult.error || 'Token validation failed'); + } + + return res.status(401).json({ + error: 'Unauthorized', + message: 'Invalid or expired token', + details: validationResult.error + }); + } + + const payload = validationResult.payload; + + // Extract user information from auth service response + req.user = { + userId: payload.sub, // Subject (user ID) + role: payload.role || 'user', + userType: payload.user_type || null, + phoneNumber: payload.phone_number || null, + tokenVersion: payload.token_version || 1, + highAssurance: payload.high_assurance || false, + // Add tenantId if present in payload (for multi-tenant apps) + tenantId: payload.tenant_id || null, + }; + + // Log successful authentication + if (req.auditLogger) { + req.auditLogger.logSuccess('authenticate', { userId: req.user.userId }); + } + + next(); +} + +export default jwtAuthenticate; diff --git a/middleware/rateLimiter.js b/middleware/rateLimiter.js new file mode 100644 index 0000000..2441ea1 --- /dev/null +++ b/middleware/rateLimiter.js @@ -0,0 +1,205 @@ +// middleware/rateLimiter.js +/** + * Rate Limiting Middleware + * + * Rate limits requests per userId (preferred) or IP (fallback) + * Returns 429 Too Many Requests when limit exceeded + * + * Uses Redis if available, falls back to in-memory store + */ + +import { createClient } from 'redis'; + +// In-memory fallback store +const memoryStore = new Map(); + +// Clean up expired entries periodically +setInterval(() => { + const now = Date.now(); + for (const [key, value] of memoryStore.entries()) { + if (value.expiresAt < now) { + memoryStore.delete(key); + } + } +}, 60000); // Clean up every minute + +// Redis client (lazy initialization) +let redisClient = null; + +async function getRedisClient() { + if (redisClient) return redisClient; + + const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379'; + + try { + redisClient = createClient({ url: REDIS_URL }); + redisClient.on('error', (err) => { + console.error('Redis Client Error:', err); + redisClient = null; + }); + await redisClient.connect(); + console.log('Redis connected for rate limiting'); + return redisClient; + } catch (err) { + console.error('Failed to connect to Redis, using in-memory store:', err.message); + redisClient = null; + return null; + } +} + +// Rate limit configuration +const RATE_LIMIT_CONFIG = { + // Default limits + DEFAULT_MAX_REQUESTS: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '100', 10), + DEFAULT_WINDOW_SECONDS: parseInt(process.env.RATE_LIMIT_WINDOW_SECONDS || '900', 10), // 15 minutes + + // Per-route custom limits can be specified when creating middleware +}; + +/** + * Increment counter in Redis or memory store + */ +async function incrementCounter(key, ttlSeconds) { + const redis = await getRedisClient(); + + if (redis) { + try { + const count = await redis.incr(key); + if (count === 1) { + // First increment, set TTL + await redis.expire(key, ttlSeconds); + } + return count; + } catch (err) { + console.error('Redis increment error, falling back to memory:', err.message); + // Fall through to memory store + } + } + + // Memory store fallback + const now = Date.now(); + const stored = memoryStore.get(key); + + if (stored && stored.expiresAt > now) { + stored.count++; + return stored.count; + } else { + memoryStore.set(key, { + count: 1, + expiresAt: now + (ttlSeconds * 1000), + }); + return 1; + } +} + +/** + * Get counter value + */ +async function getCounter(key) { + const redis = await getRedisClient(); + + if (redis) { + try { + const count = await redis.get(key); + return count ? parseInt(count, 10) : 0; + } catch (err) { + // Fall through to memory store + } + } + + // Memory store fallback + const stored = memoryStore.get(key); + const now = Date.now(); + if (stored && stored.expiresAt > now) { + return stored.count || 0; + } + return 0; +} + +/** + * Rate Limiting Middleware Factory + * + * @param {Object} options - Rate limit options + * @param {number} options.maxRequests - Maximum requests per window (default: 100) + * @param {number} options.windowSeconds - Time window in seconds (default: 900 = 15 minutes) + * @param {string} options.type - Rate limit type for logging (default: 'default') + * @returns {Function} Express middleware + */ +function createRateLimiter(options = {}) { + const maxRequests = options.maxRequests || RATE_LIMIT_CONFIG.DEFAULT_MAX_REQUESTS; + const windowSeconds = options.windowSeconds || RATE_LIMIT_CONFIG.DEFAULT_WINDOW_SECONDS; + const type = options.type || 'default'; + + return async function rateLimiter(req, res, next) { + try { + // Determine rate limit key (prefer userId, fallback to IP) + const userId = req.user?.userId; + const clientIp = req.clientIp || 'unknown'; + + const key = userId + ? `rate_limit:${type}:user:${userId}` + : `rate_limit:${type}:ip:${clientIp}`; + + // Increment counter + const count = await incrementCounter(key, windowSeconds); + + // Check if limit exceeded + if (count > maxRequests) { + // Log rate limit exceeded + if (req.auditLogger) { + req.auditLogger.log({ + userId, + action: 'rate_limit_exceeded', + route: req.path, + status: 'blocked', + meta: { count, maxRequests, windowSeconds, type, key }, + }); + } + + return res.status(429).json({ + error: 'Too Many Requests', + message: `Rate limit exceeded. Maximum ${maxRequests} requests per ${windowSeconds} seconds allowed.`, + retry_after: windowSeconds, + limit_type: type, + }); + } + + // Add rate limit headers + const remaining = Math.max(0, maxRequests - count); + const resetTime = new Date(Date.now() + (windowSeconds * 1000)); + + res.setHeader('X-RateLimit-Limit', maxRequests); + res.setHeader('X-RateLimit-Remaining', remaining); + res.setHeader('X-RateLimit-Reset', resetTime.toISOString()); + res.setHeader('X-RateLimit-Type', type); + + next(); + } catch (err) { + console.error('Rate limiter error:', err); + // On error, allow the request to proceed (fail open) + // This ensures legitimate users aren't blocked if rate limiting fails + next(); + } + }; +} + +// Export pre-configured rate limiters +export const rateLimiterRead = createRateLimiter({ + maxRequests: parseInt(process.env.RATE_LIMIT_READ_MAX || '100', 10), + windowSeconds: parseInt(process.env.RATE_LIMIT_READ_WINDOW || '900', 10), + type: 'read', +}); + +export const rateLimiterWrite = createRateLimiter({ + maxRequests: parseInt(process.env.RATE_LIMIT_WRITE_MAX || '20', 10), + windowSeconds: parseInt(process.env.RATE_LIMIT_WRITE_WINDOW || '900', 10), + type: 'write', +}); + +export const rateLimiterSensitive = createRateLimiter({ + maxRequests: parseInt(process.env.RATE_LIMIT_SENSITIVE_MAX || '10', 10), + windowSeconds: parseInt(process.env.RATE_LIMIT_SENSITIVE_WINDOW || '3600', 10), + type: 'sensitive', +}); + +export default createRateLimiter; diff --git a/middleware/requestContext.js b/middleware/requestContext.js new file mode 100644 index 0000000..bf16c41 --- /dev/null +++ b/middleware/requestContext.js @@ -0,0 +1,33 @@ +// middleware/requestContext.js +/** + * Request Context Middleware + * + * Extracts and attaches request context information (IP, user agent, etc.) + * This should be the FIRST middleware in the chain + */ + +function requestContext(req, res, next) { + // Extract client IP (considering proxies) + req.clientIp = req.ip || + req.headers['x-forwarded-for']?.split(',')[0]?.trim() || + req.headers['x-real-ip'] || + req.connection?.remoteAddress || + 'unknown'; + + // Extract user agent + req.userAgent = req.headers['user-agent'] || 'unknown'; + + // Extract request ID for tracing (if not already set) + req.requestId = req.headers['x-request-id'] || + `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + + // Set request ID in response header + res.setHeader('X-Request-Id', req.requestId); + + // Attach timestamp + req.requestTimestamp = new Date(); + + next(); +} + +export default requestContext; diff --git a/package.json b/package.json index 53b5020..dbf000b 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ }, "homepage": "https://github.com/schari2509/BuySellService_LivingAI#readme", "dependencies": { + "axios": "^1.7.9", "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.1.0", @@ -27,6 +28,7 @@ "knex": "^3.1.0", "node-cron": "^4.2.1", "pg": "^8.16.3", + "redis": "^4.7.0", "socket.io": "^4.8.1" } } diff --git a/routes/userRoutes.js b/routes/userRoutes.js index 55d44e1..d7b6e18 100644 --- a/routes/userRoutes.js +++ b/routes/userRoutes.js @@ -1,10 +1,22 @@ import express from "express"; import { insert, select, update } from "../db/queryHelper/index.js"; +import jwtAuthenticate from "../middleware/jwtAuthenticate.js"; +import { rateLimiterRead, rateLimiterWrite } from "../middleware/rateLimiter.js"; +import createCoarseAuthorize from "../middleware/coarseAuthorize.js"; +import { fineAuthorize, authorizeAction } from "../middleware/fineAuthorize.js"; const router = express.Router(); -// 1. CREATE User -router.post("/", async (req, res) => { +// Apply authentication and rate limiting to all user routes +router.use(jwtAuthenticate); +router.use(rateLimiterRead); // Use read rate limiter for user routes + +// Apply coarse-grained authorization (USER and ADMIN can access) +const requireUserOrAdmin = createCoarseAuthorize(['USER', 'ADMIN']); +router.use(requireUserOrAdmin); + +// 1. CREATE User (Requires write rate limiter) +router.post("/", rateLimiterWrite, async (req, res) => { try { // Parse and extract user data from request body const { @@ -108,34 +120,64 @@ router.get("/", async (req, res) => { }); // 3. GET Single User -router.get("/:id", async (req, res) => { - try { - const { id } = req.params; - - // Use queryHelper select with JSON-based where conditions - const user = await select({ - table: 'users', - columns: ['id', 'name', 'phone_number', 'avatar_url', 'language', 'timezone', 'country_code', 'is_active', 'created_at', 'updated_at'], - where: { - id, - deleted: false, - }, - limit: 1, - }); +// Fine-grained authorization: Users can only read their own profile, admins can read any profile +router.get("/:id", + fineAuthorize({ + action: 'read', + resource: 'user', + getResourceOwnerId: (req) => req.params.id, + }), + async (req, res) => { + try { + const { id } = req.params; + + // Use queryHelper select with JSON-based where conditions + const user = await select({ + table: 'users', + columns: ['id', 'name', 'phone_number', 'avatar_url', 'language', 'timezone', 'country_code', 'is_active', 'created_at', 'updated_at'], + where: { + id, + deleted: false, + }, + limit: 1, + }); - if (user.length === 0) { - return res.status(404).json({ error: "User not found" }); + if (user.length === 0) { + // Log not found + if (req.auditLogger) { + req.auditLogger.logFailure('get_user', 'User not found', { userId: id }); + } + return res.status(404).json({ error: "User not found" }); + } + + // Log success + if (req.auditLogger) { + req.auditLogger.logSuccess('get_user', { userId: id }); + } + + res.json(user[0]); + } catch (err) { + console.error("Error fetching user:", err); + + // Log error + if (req.auditLogger) { + req.auditLogger.logFailure('get_user', err.message, { userId: req.params.id }); + } + + res.status(500).json({ error: "Internal server error" }); } - - res.json(user[0]); - } catch (err) { - console.error("Error fetching user:", err); - res.status(500).json({ error: "Internal server error" }); } -}); +); -// 4. UPDATE User -router.put("/:id", async (req, res) => { +// 4. UPDATE User (Requires write rate limiter and fine-grained authorization) +router.put("/:id", + rateLimiterWrite, + fineAuthorize({ + action: 'write', + resource: 'user', + getResourceOwnerId: (req) => req.params.id, + }), + async (req, res) => { try { const { id } = req.params; // Parse and extract update data from request body @@ -186,8 +228,14 @@ router.put("/:id", async (req, res) => { } }); -// 5. DELETE User (Soft Delete) -router.delete("/:id", async (req, res) => { +// 5. DELETE User (Soft Delete) - Requires fine-grained authorization +router.delete("/:id", + fineAuthorize({ + action: 'delete', + resource: 'user', + getResourceOwnerId: (req) => req.params.id, + }), + async (req, res) => { try { const { id } = req.params; diff --git a/server.js b/server.js index a91f5f1..b681ccc 100644 --- a/server.js +++ b/server.js @@ -3,6 +3,7 @@ import cors from "cors"; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import http from "http"; +import dotenv from "dotenv"; import listingRoutes from "./routes/listingRoutes.js"; import locationRoutes from "./routes/locationRoutes.js"; import chatRoutes from "./routes/chatRoutes.js"; @@ -10,10 +11,32 @@ import userRoutes from "./routes/userRoutes.js"; import { initSocket } from "./socket.js"; import { startExpirationJob } from "./jobs/expirationJob.js"; +// Import middleware (in correct order) +import requestContext from "./middleware/requestContext.js"; +import { auditLoggerMiddleware } from "./services/auditLogger.js"; + +// Load environment variables +dotenv.config(); + const app = express(); + +// Trust proxy for correct IP addresses (important for rate limiting) +if (process.env.TRUST_PROXY === 'true' || process.env.TRUST_PROXY === '1') { + app.set('trust proxy', true); +} + app.use(cors()); app.use(express.json()); +// ===================================================== +// MIDDLEWARE CHAIN (IMPORTANT ORDER) +// ===================================================== +// 1. Request Context (FIRST - extracts IP, user agent, etc.) +app.use(requestContext); + +// 2. Audit Logger (attach logger to request) +app.use(auditLoggerMiddleware); + // Serve static files from public directory const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); diff --git a/services/auditLogger.js b/services/auditLogger.js new file mode 100644 index 0000000..a4c6f91 --- /dev/null +++ b/services/auditLogger.js @@ -0,0 +1,106 @@ +// services/auditLogger.js +/** + * Audit Logging Service + * + * Logs all API requests with user context, action, route, status, and timestamp + * Logs both success and failure cases + */ + +// In-memory log store (for development/testing) +// In production, you should integrate with a logging service (e.g., Winston, Bunyan, CloudWatch, etc.) +const auditLogs = []; + +/** + * Audit Logger Class + */ +class AuditLogger { + constructor(req) { + this.req = req; + this.userId = req.user?.userId || null; + this.clientIp = req.clientIp || 'unknown'; + this.userAgent = req.userAgent || 'unknown'; + this.requestId = req.requestId; + this.route = req.path; + this.method = req.method; + } + + /** + * Log an audit event + * + * @param {Object} params + * @param {string} params.action - Action being performed + * @param {string} params.status - Status ('success', 'failed', 'forbidden', etc.) + * @param {Object} params.meta - Additional metadata + */ + log({ action, status = 'success', meta = {} }) { + const auditEntry = { + timestamp: new Date().toISOString(), + requestId: this.requestId, + userId: this.userId, + action, + route: this.route, + method: this.method, + status, + clientIp: this.clientIp, + userAgent: this.userAgent, + meta, + }; + + // In production, send to logging service (e.g., CloudWatch, Elasticsearch, etc.) + // For now, log to console and store in memory + console.log('[AUDIT]', JSON.stringify(auditEntry)); + + auditLogs.push(auditEntry); + + // Keep only last 1000 logs in memory + if (auditLogs.length > 1000) { + auditLogs.shift(); + } + + // TODO: Integrate with external logging service + // Example: + // if (process.env.LOGGING_SERVICE_URL) { + // axios.post(process.env.LOGGING_SERVICE_URL + '/audit', auditEntry).catch(err => { + // console.error('Failed to send audit log:', err); + // }); + // } + } + + /** + * Log successful request + */ + logSuccess(action, meta = {}) { + this.log({ action, status: 'success', meta }); + } + + /** + * Log failed request + */ + logFailure(action, reason, meta = {}) { + this.log({ action, status: 'failed', meta: { ...meta, reason } }); + } + + /** + * Log forbidden access + */ + logForbidden(action, reason, meta = {}) { + this.log({ action, status: 'forbidden', meta: { ...meta, reason } }); + } +} + +/** + * Middleware to attach audit logger to request + */ +export function auditLoggerMiddleware(req, res, next) { + req.auditLogger = new AuditLogger(req); + next(); +} + +/** + * Get audit logs (for debugging/admin purposes) + */ +export function getAuditLogs(limit = 100) { + return auditLogs.slice(-limit); +} + +export default AuditLogger;