Updated For Verfy JWT

This commit is contained in:
Chandresh Kerkar 2025-12-21 02:18:37 +05:30
parent 35872ccee2
commit 4121b99775
12 changed files with 1672 additions and 28 deletions

188
AUTH_FLOW.md Normal file
View File

@ -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 <token>` 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 <token>"
// 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.

424
AUTH_IMPLEMENTATION.md Normal file
View File

@ -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 <token>
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 <token>` 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

132
SETUP_AUTH.md Normal file
View File

@ -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 <token>` 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 <token>`
- 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

View File

@ -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;

200
middleware/fineAuthorize.js Normal file
View File

@ -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',
});
}
};
}

View File

@ -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 <token>'
});
}
// 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;

205
middleware/rateLimiter.js Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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"
}
}

View File

@ -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;

View File

@ -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);

106
services/auditLogger.js Normal file
View File

@ -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;