Merge pull request #243 from the-luap/fix/security-session-invalidation
fix(security): token invalidation on password change, session timeout enforcement
This commit is contained in:
@@ -86,8 +86,8 @@ async function sessionTimeoutMiddleware(req, res, next) {
|
|||||||
const lastActivity = sessions.get(token);
|
const lastActivity = sessions.get(token);
|
||||||
const timeout = await getSessionTimeout();
|
const timeout = await getSessionTimeout();
|
||||||
|
|
||||||
// If session exists, check if it's expired
|
|
||||||
if (lastActivity) {
|
if (lastActivity) {
|
||||||
|
// Existing session — check if idle too long
|
||||||
if (now - lastActivity > timeout) {
|
if (now - lastActivity > timeout) {
|
||||||
sessions.delete(token);
|
sessions.delete(token);
|
||||||
return res.status(401).json({
|
return res.status(401).json({
|
||||||
@@ -95,6 +95,16 @@ async function sessionTimeoutMiddleware(req, res, next) {
|
|||||||
code: 'SESSION_TIMEOUT'
|
code: 'SESSION_TIMEOUT'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// First request with this token — check if token was issued longer ago than the timeout
|
||||||
|
// This prevents old/stolen tokens from bypassing session timeout after server restart
|
||||||
|
const tokenIssuedAt = (decoded.iat || 0) * 1000; // iat is in seconds
|
||||||
|
if (now - tokenIssuedAt > timeout) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: 'Session expired',
|
||||||
|
code: 'SESSION_TIMEOUT'
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update last activity
|
// Update last activity
|
||||||
|
|||||||
@@ -122,13 +122,15 @@ router.post('/change-password', [
|
|||||||
// Hash new password with more rounds
|
// Hash new password with more rounds
|
||||||
const newPasswordHash = await bcrypt.hash(newPassword, 12);
|
const newPasswordHash = await bcrypt.hash(newPassword, 12);
|
||||||
|
|
||||||
// Update password and clear must_change_password flag
|
// Update password, set password_changed_at to invalidate existing tokens, and clear must_change_password flag
|
||||||
|
const now = new Date();
|
||||||
await db('admin_users')
|
await db('admin_users')
|
||||||
.where('id', userId)
|
.where('id', userId)
|
||||||
.update({
|
.update({
|
||||||
password_hash: newPasswordHash,
|
password_hash: newPasswordHash,
|
||||||
|
password_changed_at: now,
|
||||||
must_change_password: false,
|
must_change_password: false,
|
||||||
updated_at: new Date()
|
updated_at: now
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log activity
|
// Log activity
|
||||||
|
|||||||
@@ -149,7 +149,11 @@ export const userManagementService = {
|
|||||||
* Update an admin user
|
* Update an admin user
|
||||||
*/
|
*/
|
||||||
async updateUser(id: number, data: UpdateUserData): Promise<AdminUser> {
|
async updateUser(id: number, data: UpdateUserData): Promise<AdminUser> {
|
||||||
const response = await api.put<UpdateUserResponse>(`/admin/users/${id}`, data);
|
// Convert camelCase to snake_case for backend API
|
||||||
|
const payload: Record<string, unknown> = {};
|
||||||
|
if (data.roleId !== undefined) payload.role_id = data.roleId;
|
||||||
|
if (data.isActive !== undefined) payload.is_active = data.isActive;
|
||||||
|
const response = await api.put<UpdateUserResponse>(`/admin/users/${id}`, payload);
|
||||||
return transformUser(response.data.user);
|
return transformUser(response.data.user);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user