Updated For Verfy JWT
This commit is contained in:
parent
35872ccee2
commit
4121b99775
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
@ -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',
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
23
server.js
23
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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
Loading…
Reference in New Issue