forked from Nova-reward/Nova-Rewards
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathauthenticateUser.js
More file actions
148 lines (135 loc) · 4.32 KB
/
Copy pathauthenticateUser.js
File metadata and controls
148 lines (135 loc) · 4.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
const jwt = require('jsonwebtoken');
const logger = require('../lib/logger');
const { query } = require('../db/index');
const { verifyToken, isRevoked } = require('../services/tokenService');
const AuditService = require('../services/auditService');
const SecurityAlertService = require('../services/securityAlertService');
/**
* Middleware: validates RS256 JWT from Authorization header.
* Checks Redis blocklist before accepting the token.
* Attaches { userId, role, walletAddress } (plus full DB row) to req.user on success.
* Issue #857 — JWT authentication middleware.
* Issue #648 — JWT RS256 hardening.
*/
async function authenticateUser(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
code: 'TOKEN_MISSING',
message: 'Bearer token is required in Authorization header',
});
}
const token = authHeader.substring(7);
try {
const decoded = verifyToken(token);
// sub = wallet_address (RS256 payload uses sub, not userId)
if (!decoded || !decoded.sub) {
return res.status(401).json({
success: false,
code: 'TOKEN_INVALID',
message: 'Invalid token payload',
});
}
// Reject if jti is in the Redis blocklist
if (decoded.jti && await isRevoked(decoded.jti)) {
return res.status(401).json({
success: false,
code: 'TOKEN_INVALID',
message: 'Token has been revoked',
});
}
const result = await query(
`SELECT id, email, wallet_address, first_name, last_name, bio, stellar_public_key,
role, created_at, updated_at
FROM users
WHERE wallet_address = $1 AND is_deleted = FALSE`,
[decoded.sub]
);
if (!result.rows[0]) {
return res.status(401).json({
success: false,
code: 'TOKEN_INVALID',
message: 'User not found or account deleted',
});
}
const dbUser = result.rows[0];
req.user = {
...dbUser,
userId: dbUser.id,
walletAddress: dbUser.wallet_address,
};
next();
} catch (err) {
if (err instanceof jwt.TokenExpiredError) {
return res.status(401).json({
success: false,
code: 'TOKEN_EXPIRED',
message: 'Token has expired',
});
}
logger.error('Authentication error:', err);
return res.status(401).json({
success: false,
code: 'TOKEN_INVALID',
message: 'Invalid or malformed token',
});
}
}
/**
* Middleware: check if user is admin
* Requirements: 1.2, 1.3, 1.4, 3.1, 3.2, 3.3, 3.4, 3.5, 4.1, 4.5
*/
async function requireAdmin(req, res, next) {
if (req.user?.role !== 'admin') {
// Only log security event when we have an authenticated user
if (req.user) {
const event = {
action: 'PRIVILEGE_ESCALATION_ATTEMPT',
entityType: 'admin_endpoint',
entityId: null,
performedBy: req.user.id,
source: 'api',
details: {
method: req.method,
role: req.user.role,
ip: req.ip || req.headers['x-forwarded-for'],
entityId: req.path,
},
};
try {
await AuditService.log(event);
} catch (auditErr) {
logger.error('[requireAdmin] AuditService failed:', auditErr);
}
// Fire-and-forget alert (non-blocking)
SecurityAlertService.send({ ...event, timestamp: new Date().toISOString() })
.catch(err => logger.error('[requireAdmin] SecurityAlertService failed:', err));
}
return res.status(403).json({
success: false,
error: 'forbidden',
message: 'Admin access required',
});
}
next();
}
/**
* Middleware: check if user owns the resource or is admin
* Requirements: 183.1
*/
function requireOwnershipOrAdmin(req, res, next) {
// GET requests: allow all authenticated users — route decides public vs private data
if (req.method === 'GET') return next();
const resourceUserId = parseInt(req.params.id);
const currentUserId = req.user?.id;
if (currentUserId !== resourceUserId && req.user?.role !== 'admin') {
return res.status(403).json({
success: false,
error: 'forbidden',
message: 'You can only access your own profile',
});
}
next();
}
module.exports = { authenticateUser, requireAdmin, requireOwnershipOrAdmin };