Skip to content

Commit a78d9b8

Browse files
author
Fatima Sanusi
committed
Security fixes: Implement comprehensive security enhancements (#232 #235 #237 #241)
- #232: Refactor webhook dispatcher with enhanced HMAC security - Add payload normalization for consistent signature generation - Implement replay protection with timestamps and nonces - Add timing-safe signature verification - Enhance webhookDispatcherService with similar security features - #237: Implement payload size limits for REST/GraphQL requests - Create PayloadSizeLimitMiddleware with configurable limits - Add GraphQL-specific payload complexity analysis - Implement early rejection via content-length headers - Add security violation logging for monitoring - Integrate middleware into main application with environment-based configuration - #241: Add anomaly detection for subscription cancellations/payment failures - Create AnomalyDetectionService with configurable thresholds - Implement baseline calculation and spike detection - Add real-time monitoring and alerting system - Create REST API endpoints for anomaly statistics and configuration - Integrate with subscription service for automatic monitoring - #235: Harden authentication flows with strict JWT expiration and rotation - Implement short-lived access tokens (15 minutes default) - Add refresh token rotation with automatic cleanup - Create token blacklist for immediate revocation - Add timing-safe signature verification - Implement token rotation endpoints and middleware - Add comprehensive token management and cleanup Security improvements: - Enhanced webhook signature verification with normalized payloads - Memory exhaustion attack prevention via payload limits - Real-time anomaly detection for business metrics - Strict JWT token lifecycle management with rotation - Comprehensive security monitoring and logging - Configurable security thresholds via environment variables
1 parent 492f89d commit a78d9b8

11 files changed

Lines changed: 1950 additions & 66 deletions

index.js

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,10 @@ const createPrivacyRoutes = require('./routes/privacy');
139139
const { setupApolloServer } = require('./src/graphql');
140140

141141

142+
// Initialize payload size limit middleware
143+
const { PayloadSizeLimitMiddleware } = require('./src/middleware/payloadSizeLimit');
144+
const { GraphQLPayloadLimitMiddleware } = require('./src/middleware/graphqlPayloadLimit');
145+
142146
// Tier middleware — attaches req.user.tier to every request
143147
const { attachTier } = require('./middleware/tierAuth');
144148
const { MerchantCorsMiddleware } = require('./src/middleware/merchantCorsMiddleware');
@@ -177,8 +181,37 @@ async function createApp(dependencies = {}) {
177181
// ── Global middleware ──────────────────────────────────────────────────────
178182
const merchantCors = new MerchantCorsMiddleware(database);
179183
app.use(cors(merchantCors.corsOptionsDelegate()));
180-
app.use(express.json({ limit: '10mb' }));
181-
app.use(express.urlencoded({ extended: true }));
184+
185+
// Initialize payload size limit middleware with security-focused configuration
186+
const payloadSizeLimit = new PayloadSizeLimitMiddleware({
187+
jsonLimit: process.env.PAYLOAD_JSON_LIMIT || 1024 * 1024, // 1MB for JSON
188+
urlencodedLimit: process.env.PAYLOAD_URLENCODED_LIMIT || 1024 * 1024, // 1MB for URL-encoded
189+
textLimit: process.env.PAYLOAD_TEXT_LIMIT || 1024 * 1024, // 1MB for text
190+
rawLimit: process.env.PAYLOAD_RAW_LIMIT || 10 * 1024 * 1024, // 10MB for raw/binary
191+
graphqlLimit: process.env.PAYLOAD_GRAPHQL_LIMIT || 2 * 1024 * 1024, // 2MB for GraphQL
192+
fileLimit: process.env.PAYLOAD_FILE_LIMIT || 50 * 1024 * 1024, // 50MB for file uploads
193+
strictMode: process.env.NODE_ENV === 'production', // Enable strict mode in production
194+
enableLogging: process.env.PAYLOAD_LOGGING !== 'false' // Enable logging by default
195+
});
196+
197+
// Initialize GraphQL payload limit middleware
198+
const graphqlPayloadLimit = new GraphQLPayloadLimitMiddleware({
199+
maxQueryLength: parseInt(process.env.GRAPHQL_MAX_QUERY_LENGTH) || 10000,
200+
maxVariablesSize: parseInt(process.env.GRAPHQL_MAX_VARIABLES_SIZE) || 1024 * 1024,
201+
maxQueryDepth: parseInt(process.env.GRAPHQL_MAX_QUERY_DEPTH) || 10,
202+
maxComplexity: parseInt(process.env.GRAPHQL_MAX_COMPLEXITY) || 1000,
203+
enableLogging: process.env.GRAPHQL_LOGGING !== 'false'
204+
});
205+
206+
// Apply payload size limits before body parsing
207+
app.use(payloadSizeLimit.middleware());
208+
209+
// Apply GraphQL-specific limits for GraphQL endpoints
210+
app.use('/graphql', graphqlPayloadLimit.middleware());
211+
212+
// Remove the default express.json() and express.urlencoded() as they are now handled by the payload limit middleware
213+
// app.use(express.json({ limit: '10mb' }));
214+
// app.use(express.urlencoded({ extended: true }));
182215

183216
// Request tracing middleware must be registered early for trace propagation
184217
app.use(requestTracingMiddleware);

middleware/auth.js

Lines changed: 104 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
const jwt = require('jsonwebtoken');
22
const { ethers } = require('ethers');
3+
const { AuthService } = require('../src/services/auth.service');
34

45
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
56

7+
// Initialize enhanced authentication service
8+
const authService = new AuthService();
9+
610
// Generate nonce for SIWE
711
const generateNonce = () => {
812
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
@@ -21,20 +25,25 @@ const verifySignature = (message, signature, address) => {
2125
}
2226
};
2327

24-
// Generate JWT token
28+
// Generate JWT token (using enhanced service)
2529
const generateToken = (address, tier = 'bronze') => {
26-
return jwt.sign(
27-
{
28-
address: address.toLowerCase(),
29-
tier,
30-
iat: Math.floor(Date.now() / 1000)
31-
},
32-
JWT_SECRET,
33-
{ expiresIn: '24h' }
34-
);
30+
const member = {
31+
id: address.toLowerCase(),
32+
email: `${address.toLowerCase()}@example.com`,
33+
organizationId: 'default',
34+
role: 'user',
35+
permissions: ['read']
36+
};
37+
38+
return authService.generateAccessToken(member);
3539
};
3640

37-
// Verify JWT middleware
41+
// Generate refresh token
42+
const generateRefreshToken = (address) => {
43+
return authService.generateRefreshToken(address.toLowerCase(), null);
44+
};
45+
46+
// Enhanced JWT verification middleware with rotation support
3847
const authenticateToken = (req, res, next) => {
3948
const authHeader = req.headers['authorization'];
4049
const token = authHeader && authHeader.split(' ')[1];
@@ -46,16 +55,32 @@ const authenticateToken = (req, res, next) => {
4655
});
4756
}
4857

49-
jwt.verify(token, JWT_SECRET, (err, user) => {
50-
if (err) {
51-
return res.status(403).json({
52-
success: false,
53-
error: 'Invalid or expired token'
54-
});
58+
try {
59+
const payload = authService.verifyAccessToken(token);
60+
61+
// Check if token needs rotation
62+
if (authService.shouldRotateToken(token)) {
63+
// Add rotation hint to response headers
64+
res.set('X-Token-Rotation-Required', 'true');
5565
}
56-
req.user = user;
66+
67+
req.user = {
68+
id: payload.sub,
69+
email: payload.email,
70+
organizationId: payload.organizationId,
71+
role: payload.role,
72+
permissions: payload.permissions,
73+
sessionId: payload.sessionId,
74+
jti: payload.jti
75+
};
76+
5777
next();
58-
});
78+
} catch (error) {
79+
return res.status(403).json({
80+
success: false,
81+
error: error.message || 'Invalid or expired token'
82+
});
83+
}
5984
};
6085

6186
// Tier-based access middleware
@@ -76,11 +101,70 @@ const requireTier = (requiredTier) => {
76101
};
77102
};
78103

104+
// Token rotation endpoint handler
105+
const rotateTokens = async (req, res) => {
106+
const { refreshToken } = req.body;
107+
108+
if (!refreshToken) {
109+
return res.status(400).json({
110+
success: false,
111+
error: 'Refresh token required'
112+
});
113+
}
114+
115+
try {
116+
const tokens = await authService.rotateTokens(refreshToken);
117+
118+
res.json({
119+
success: true,
120+
data: tokens,
121+
message: 'Tokens rotated successfully'
122+
});
123+
} catch (error) {
124+
res.status(403).json({
125+
success: false,
126+
error: error.message || 'Token rotation failed'
127+
});
128+
}
129+
};
130+
131+
// Token revocation endpoint
132+
const revokeToken = (req, res) => {
133+
const authHeader = req.headers['authorization'];
134+
const token = authHeader && authHeader.split(' ')[1];
135+
136+
if (!token) {
137+
return res.status(400).json({
138+
success: false,
139+
error: 'Token required'
140+
});
141+
}
142+
143+
try {
144+
const payload = authService.verifyAccessToken(token);
145+
authService.blacklistToken(payload.jti);
146+
147+
res.json({
148+
success: true,
149+
message: 'Token revoked successfully'
150+
});
151+
} catch (error) {
152+
res.status(403).json({
153+
success: false,
154+
error: error.message || 'Token revocation failed'
155+
});
156+
}
157+
};
158+
79159
module.exports = {
80160
generateNonce,
81161
nonces,
82162
verifySignature,
83163
generateToken,
164+
generateRefreshToken,
84165
authenticateToken,
85-
requireTier
166+
rotateTokens,
167+
revokeToken,
168+
requireTier,
169+
authService // Export service for direct access
86170
};

0 commit comments

Comments
 (0)