Skip to content

Commit 611135c

Browse files
authored
Merge pull request #1027 from hpcc-systems/yadhap/lock-users-after-invalid-login-attemtps
Lock Account After 5 Invalid Login Attempt
2 parents 75a1cdc + 7832f6d commit 611135c

File tree

5 files changed

+186
-12
lines changed

5 files changed

+186
-12
lines changed

Tombolo/server/controllers/authController.js

+17-12
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const {
1919
generateAndSetCSRFToken,
2020
setPreviousPasswords,
2121
setLastLogin,
22+
handleInvalidLoginAttempt
2223
} = require("../utils/authUtil");
2324
const { blacklistToken } = require("../utils/tokenBlackListing");
2425
const sequelize = require("../models").sequelize;
@@ -662,6 +663,21 @@ const loginBasicUser = async (req, res, next) => {
662663
});
663664
}
664665

666+
// If the accountLocked.isLocked is true, return generic error
667+
if (user.accountLocked.isLocked) {
668+
logger.error(`Login : Login Attempt by user with locked account ${email}`);
669+
return res.status(403).json({
670+
success: false,
671+
message: genericError,
672+
});
673+
}
674+
675+
//Compare password
676+
if (!bcrypt.compareSync(password, user.hash)) {
677+
logger.error(`Login : Invalid password for user with email ${email}`);
678+
await handleInvalidLoginAttempt({user, errMessage: genericError})
679+
}
680+
665681
// If not verified user return error
666682
if (!user.verifiedUser) {
667683
logger.error(`Login : Login attempt by unverified user - ${user.id}`);
@@ -678,9 +694,6 @@ const loginBasicUser = async (req, res, next) => {
678694
`Login : Login attempt by user with expired password - ${user.id}`
679695
);
680696

681-
//send password expired email
682-
// await sendPasswordExpiredEmail(user);
683-
684697
res.status(401).json({
685698
success: false,
686699
message: "password-expired",
@@ -713,15 +726,7 @@ const loginBasicUser = async (req, res, next) => {
713726
azureError.status = 403;
714727
throw azureError;
715728
}
716-
//Compare password
717-
if (!bcrypt.compareSync(password, user.hash)) {
718-
logger.error(`Login : Invalid password for user with email ${email}`);
719729

720-
// Incorrect E-mail password combination error
721-
const invalidCredentialsErr = new Error(genericError);
722-
invalidCredentialsErr.status = 403;
723-
throw invalidCredentialsErr;
724-
}
725730
// Remove hash from use object
726731
const userObj = user.toJSON();
727732
delete userObj.hash;
@@ -772,7 +777,7 @@ const loginBasicUser = async (req, res, next) => {
772777
}
773778
res
774779
.status(err.status || 500)
775-
.json({ success: false, message: err.message });
780+
.json({ success: false, message: err.message});
776781
}
777782
};
778783

Tombolo/server/migrations/20220507024001-create-user.js

+12
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,18 @@ module.exports = {
6060
type: Sequelize.DATE,
6161
allowNull: true,
6262
},
63+
loginAttempts: {
64+
type: Sequelize.INTEGER,
65+
allowNull: false,
66+
defaultValue: 0,
67+
},
68+
accountLocked: {
69+
type: Sequelize.JSON,
70+
defaultValue: {
71+
isLocked: false,
72+
lockedReason: [],
73+
},
74+
},
6375
lastLoginAt: {
6476
type: Sequelize.DATE,
6577
allowNull: true,

Tombolo/server/models/user.js

+13
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,19 @@ module.exports = (sequelize, DataTypes) => {
6363
type: DataTypes.DATE,
6464
allowNull: true,
6565
},
66+
loginAttempts: {
67+
type: DataTypes.INTEGER,
68+
allowNull: false,
69+
defaultValue: 0,
70+
},
71+
accountLocked: {
72+
type: DataTypes.JSON,
73+
allowNull: true,
74+
defaultValue: {
75+
isLocked: false,
76+
lockedReason: [],
77+
},
78+
},
6679
lastLoginAt: {
6780
type: DataTypes.DATE,
6881
allowNull: true,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<!DOCTYPE html>
2+
<html>
3+
4+
<head>
5+
<style>
6+
.main_body {
7+
width: 100%;
8+
border-bottom: 1px solid lightgray !important;
9+
}
10+
11+
.section-footer-td {
12+
color: #808080;
13+
border-top: 1px solid #808080;
14+
}
15+
16+
.section-header-td {
17+
padding-top: 8px;
18+
padding-bottom: 8px;
19+
}
20+
21+
.list {
22+
padding: 10px;
23+
}
24+
25+
.tabbedText {
26+
padding-left: 40px;
27+
}
28+
29+
.bold-text {
30+
font-weight: bold;
31+
}
32+
33+
.important-text {
34+
color: #ff0000;
35+
}
36+
</style>
37+
</head>
38+
39+
<body>
40+
<table class="main_body">
41+
<tbody>
42+
<tr>
43+
<td>
44+
<table>
45+
<tbody>
46+
<tr>
47+
<td class="section-header-td">
48+
<div>Hello <%= userName %>,</div>
49+
<div>
50+
<p>
51+
Your <span class="important-text">account has been locked </span> due to exceeding the maximum number of <span class="important-text"> unsuccessful login attempts</span>. For security reasons, access to your account has been temporarily restricted.
52+
</p>
53+
<p>
54+
To restore access, please reach out to your administrator for assistance. You can contact the following for support: </p>
55+
<div class="tabbedText">
56+
<% if (typeof supportEmailRecipients !== 'undefined') { %>
57+
<% supportEmailRecipients.forEach(function(e) { %>
58+
<div class="tabbedText"><%= e %></div>
59+
<% }); %>
60+
</div>
61+
</div>
62+
<% } %>
63+
</td>
64+
</tr>
65+
66+
<tr>
67+
<td class="section-header-td">
68+
<div>Best Regards,</div>
69+
<div>Tombolo</div>
70+
</td>
71+
</tr>
72+
</tbody>
73+
</table>
74+
</td>
75+
</tr>
76+
</tbody>
77+
</table>
78+
</body>
79+
80+
</html>

Tombolo/server/utils/authUtil.js

+64
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
const jwt = require("jsonwebtoken");
33
const { v4: uuidv4 } = require("uuid");
44
const { Op } = require("sequelize");
5+
const moment = require("moment");
56

67
// Local Imports
78
const logger = require("../config/logger");
@@ -384,6 +385,7 @@ const setLastLogin = async (user) => {
384385

385386
const updatedUser = await User.update(
386387
{
388+
loginAttempts: 0,
387389
lastLoginAt: date,
388390
metaData: {
389391
...user.metaData,
@@ -409,6 +411,67 @@ const setLastLogin = async (user) => {
409411
return;
410412
};
411413

414+
// Record failed login attempt
415+
const handleInvalidLoginAttempt = async ({user, errMessage}) => {
416+
const loginAttempts = user.loginAttempts + 1;
417+
const accountLocked = user.accountLocked;
418+
419+
if (loginAttempts === 5) {
420+
const newAccLockedData = {
421+
isLocked: true,
422+
lockedReason: [...new Set([...accountLocked.lockedReason, "reachedMaxLoginAttempts"])],
423+
};
424+
425+
// Update user record
426+
await User.update(
427+
{
428+
loginAttempts: loginAttempts,
429+
accountLocked: newAccLockedData,
430+
},
431+
{ where: { id: user.id } }
432+
);
433+
434+
// Queue notification
435+
await sendAccountLockedEmail(user);
436+
}else{
437+
// Update user record
438+
await User.update({loginAttempts: loginAttempts},
439+
{ where: { id: user.id } }
440+
);
441+
}
442+
443+
// Incorrect E-mail password combination error
444+
const invalidCredentialsErr = new Error(errMessage);
445+
invalidCredentialsErr.status = 403;
446+
throw invalidCredentialsErr;
447+
};
448+
449+
// Function to send account locked email
450+
const sendAccountLockedEmail = async (user) => {
451+
// Get support email recipients
452+
const supportEmailRecipients = await getSupportContactEmails();
453+
454+
await NotificationQueue.create({
455+
type: "email",
456+
templateName: "accountLocked",
457+
notificationOrigin: "User Authentication",
458+
deliveryType: "immediate",
459+
metaData: {
460+
notificationId: `ACC_LOCKED_${moment().format("YYYYMMDD_HHmmss_SSS")}`,
461+
recipientName: user.firstName,
462+
notificationOrigin: "User Authentication",
463+
subject: "Your account has been locked",
464+
mainRecipients: [user.email],
465+
notificationDescription:
466+
"Account locked because of too many failed login attempts",
467+
userName: user.firstName,
468+
userEmail: user.email,
469+
supportEmailRecipients,
470+
},
471+
createdBy: user.id,
472+
});
473+
};
474+
412475
const deleteUser = async (id, reason) => {
413476
try {
414477
if (!reason || reason === "") {
@@ -472,5 +535,6 @@ module.exports = {
472535
generatePassword,
473536
setLastLogin,
474537
setLastLoginAndReturn,
538+
handleInvalidLoginAttempt,
475539
deleteUser,
476540
};

0 commit comments

Comments
 (0)