-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Expand file tree
/
Copy pathAccountLockout.js
More file actions
187 lines (167 loc) · 5.17 KB
/
Copy pathAccountLockout.js
File metadata and controls
187 lines (167 loc) · 5.17 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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
// This class handles the Account Lockout Policy settings.
import Parse from 'parse/node';
export class AccountLockout {
constructor(user, config) {
this._user = user;
this._config = config;
}
/**
* set _failed_login_count to value
*/
_setFailedLoginCount(value) {
const query = {
username: this._user.username,
};
const updateFields = {
_failed_login_count: value,
};
return this._config.database.update('_User', query, updateFields);
}
/**
* increment _failed_login_count by 1 and return the updated document
*/
_incrementFailedLoginCount() {
const query = {
username: this._user.username,
};
const updateFields = {
_failed_login_count: { __op: 'Increment', amount: 1 },
};
return this._config.database.update('_User', query, updateFields);
}
/**
* if the failed login count is greater than the threshold
* then sets lockout expiration to 'currenttime + accountPolicy.duration', i.e., account is locked out for the next 'accountPolicy.duration' minutes
* else do nothing
*/
_setLockoutExpiration() {
const query = {
username: this._user.username,
_failed_login_count: { $gte: this._config.accountLockout.threshold },
};
const now = new Date();
const updateFields = {
_account_lockout_expires_at: Parse._encode(
new Date(now.getTime() + this._config.accountLockout.duration * 60 * 1000)
),
};
return this._config.database.update('_User', query, updateFields).catch(err => {
if (
err &&
err.code &&
err.message &&
err.code === Parse.Error.OBJECT_NOT_FOUND &&
err.message === 'Object not found.'
) {
return; // nothing to update so we are good
} else {
throw err; // unknown error
}
});
}
/**
* if _account_lockout_expires_at > current_time and _failed_login_count > threshold
* reject with account locked error
* else
* resolve
*/
_notLocked() {
const query = {
username: this._user.username,
_account_lockout_expires_at: { $gt: Parse._encode(new Date()) },
_failed_login_count: { $gte: this._config.accountLockout.threshold },
};
return this._config.database.find('_User', query).then(users => {
if (Array.isArray(users) && users.length > 0) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Your account is locked due to multiple failed login attempts. Please try again after ' +
this._config.accountLockout.duration +
' minute(s)'
);
}
});
}
/**
* Atomically increment _failed_login_count and enforce lockout threshold.
* Uses the atomic increment result to determine the exact post-increment
* count, eliminating the TOCTOU race between checking and updating.
*/
_handleFailedLoginAttempt() {
return this._incrementFailedLoginCount().then(result => {
const count = result._failed_login_count;
if (count >= this._config.accountLockout.threshold) {
return this._setLockoutExpiration().then(() => {
if (count > this._config.accountLockout.threshold) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Your account is locked due to multiple failed login attempts. Please try again after ' +
this._config.accountLockout.duration +
' minute(s)'
);
}
});
}
});
}
/**
* Reset the failed login count once a previous lockout has expired, so the user is
* granted a fresh set of attempts (threshold) instead of being re-locked on the first
* post-expiry failure.
*/
_resetFailedLoginCountIfExpired() {
const query = {
username: this._user.username,
_account_lockout_expires_at: { $lt: Parse._encode(new Date()) },
};
const updateFields = {
_failed_login_count: 0,
_account_lockout_expires_at: { __op: 'Delete' },
};
return this._config.database.update('_User', query, updateFields).catch(err => {
if (
err &&
err.code &&
err.message &&
err.code === Parse.Error.OBJECT_NOT_FOUND &&
err.message === 'Object not found.'
) {
return; // no expired lockout to reset
} else {
throw err; // unknown error
}
});
}
/**
* handle login attempt if the Account Lockout Policy is enabled
*/
async handleLoginAttempt(loginSuccessful) {
if (!this._config.accountLockout) {
return;
}
await this._notLocked();
if (loginSuccessful) {
await this._setFailedLoginCount(0);
} else {
await this._resetFailedLoginCountIfExpired();
await this._handleFailedLoginAttempt();
}
}
/**
* Removes the account lockout.
*/
unlockAccount() {
if (!this._config.accountLockout || !this._config.accountLockout.unlockOnPasswordReset) {
return Promise.resolve();
}
return this._config.database.update(
'_User',
{ username: this._user.username },
{
_failed_login_count: { __op: 'Delete' },
_account_lockout_expires_at: { __op: 'Delete' },
}
);
}
}
export default AccountLockout;