mirror of
https://github.com/wekan/wekan.git
synced 2025-04-22 04:57:07 -04:00
Reduced Wekan bundle size from 636 MB to 467 MB by deleting all
dependencies of lucasantoniassi:accounts-lockout and including only required 10 files. Thank to xet7 !
This commit is contained in:
parent
8f19f043a1
commit
23e5e1e3bd
13 changed files with 908 additions and 22 deletions
|
@ -83,7 +83,6 @@ mquandalle:moment
|
|||
msavin:usercache
|
||||
# Keep stylus in 1.1.0, because building v2 takes extra 52 minutes.
|
||||
coagmano:stylus@1.1.0!
|
||||
lucasantoniassi:accounts-lockout
|
||||
meteorhacks:subs-manager
|
||||
meteorhacks:picker
|
||||
lamhieu:unblock
|
||||
|
@ -145,3 +144,4 @@ staringatlights:fast-render
|
|||
spacebars
|
||||
easylogic:summernote
|
||||
pascoual:pdfkit
|
||||
wekan-accounts-lockout
|
||||
|
|
|
@ -71,7 +71,6 @@ launch-screen@1.2.1
|
|||
livedata@1.0.18
|
||||
localstorage@1.2.0
|
||||
logging@1.2.0
|
||||
lucasantoniassi:accounts-lockout@1.0.0
|
||||
matb33:collection-hooks@0.9.1
|
||||
matteodem:easy-search@1.6.4
|
||||
mdg:validation-error@0.5.1
|
||||
|
@ -220,6 +219,7 @@ verron:autosize@3.0.8
|
|||
webapp@1.10.1
|
||||
webapp-hashing@1.1.0
|
||||
wekan-accounts-cas@0.1.0
|
||||
wekan-accounts-lockout@1.0.0
|
||||
wekan-accounts-oidc@1.0.10
|
||||
wekan-cfs-access-point@0.1.50
|
||||
wekan-cfs-base-package@0.0.30
|
||||
|
|
25
packages/wekan-accounts-lockout/CONTRIBUTING.md
Normal file
25
packages/wekan-accounts-lockout/CONTRIBUTING.md
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Contributing guide
|
||||
|
||||
Want to contribute to Accounts-Lockout? Awesome!
|
||||
There are many ways you can contribute, see below.
|
||||
|
||||
## Opening issues
|
||||
|
||||
Open an issue to report bugs or to propose new features.
|
||||
|
||||
- Reporting bugs: describe the bug as clearly as you can, including steps to reproduce, what happened and what you were expecting to happen. Also include browser version, OS and other related software's (npm, Node.js, etc) versions when applicable.
|
||||
|
||||
- Proposing features: explain the proposed feature, what it should do, why it is useful, how users should use it. Give us as much info as possible so it will be easier to discuss, access and implement the proposed feature. When you're unsure about a certain aspect of the feature, feel free to leave it open for others to discuss and find an appropriate solution.
|
||||
|
||||
## Proposing pull requests
|
||||
|
||||
Pull requests are very welcome. Note that if you are going to propose drastic changes, be sure to open an issue for discussion first, to make sure that your PR will be accepted before you spend effort coding it.
|
||||
|
||||
Fork the Accounts-Lockout repository, clone it locally and create a branch for your proposed bug fix or new feature. Avoid working directly on the master branch.
|
||||
|
||||
Implement your bug fix or feature, write tests to cover it and make sure all tests are passing (run a final `npm test` to make sure everything is correct). Then commit your changes, push your bug fix/feature branch to the origin (your forked repo) and open a pull request to the upstream (the repository you originally forked)'s master branch.
|
||||
|
||||
## Documentation
|
||||
|
||||
Documentation is extremely important and takes a fair deal of time and effort to write and keep updated.
|
||||
Please submit any and all improvements you can make to the repository's docs.
|
21
packages/wekan-accounts-lockout/LICENSE
Normal file
21
packages/wekan-accounts-lockout/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Lucas Antoniassi de Paiva
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
126
packages/wekan-accounts-lockout/README.md
Normal file
126
packages/wekan-accounts-lockout/README.md
Normal file
|
@ -0,0 +1,126 @@
|
|||
# Meteor - Accounts - Lockout
|
||||
|
||||
[](https://travis-ci.org/LucasAntoniassi/meteor-accounts-lockout)
|
||||
[](https://www.codacy.com/app/lucasantoniassi/meteor-accounts-lockout?utm_source=github.com&utm_medium=referral&utm_content=LucasAntoniassi/meteor-accounts-lockout&utm_campaign=Badge_Grade)
|
||||
[](https://codeclimate.com/github/LucasAntoniassi/meteor-accounts-lockout)
|
||||
|
||||
## What it is
|
||||
|
||||
Seamless Meteor apps accounts protection from password brute-force attacks.
|
||||
Users won't notice it. Hackers shall not pass.
|
||||
|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
meteor add lucasantoniassi:accounts-lockout
|
||||
```
|
||||
|
||||
## Usage via ES6 import
|
||||
|
||||
```javascript
|
||||
// server
|
||||
import { AccountsLockout } from 'meteor/lucasantoniassi:accounts-lockout';
|
||||
```
|
||||
|
||||
## How to use
|
||||
|
||||
Default settings:
|
||||
|
||||
```javascript
|
||||
"knownUsers": {
|
||||
"failuresBeforeLockout": 3, // positive integer greater than 0
|
||||
"lockoutPeriod": 60, // in seconds
|
||||
"failureWindow": 10 // in seconds
|
||||
},
|
||||
"unknownUsers": {
|
||||
"failuresBeforeLockout": 3, // positive integer greater than 0
|
||||
"lockoutPeriod": 60, // in seconds
|
||||
"failureWindow": 10 // in seconds
|
||||
}
|
||||
```
|
||||
|
||||
`knownUsers` are users where already belongs to your `Meteor.users` collections,
|
||||
these rules are applied if they attempt to login with an incorrect password but a know email.
|
||||
|
||||
`unknownUsers` are users where **not** belongs to your `Meteor.users` collections,
|
||||
these rules are applied if they attempt to login with a unknown email.
|
||||
|
||||
`failuresBeforeLockout` should be a positive integer greater than 0.
|
||||
|
||||
`lockoutPeriod` should be in seconds.
|
||||
|
||||
`failureWindow` should be in seconds.
|
||||
|
||||
If the `default` is nice to you, you can do that.
|
||||
|
||||
```javascript
|
||||
(new AccountsLockout()).startup();
|
||||
```
|
||||
|
||||
You can overwrite passing an `object` as argument.
|
||||
|
||||
```javascript
|
||||
(new AccountsLockout({
|
||||
knownUsers: {
|
||||
failuresBeforeLockout: 3,
|
||||
lockoutPeriod: 60,
|
||||
failureWindow: 15,
|
||||
},
|
||||
unknownUsers: {
|
||||
failuresBeforeLockout: 3,
|
||||
lockoutPeriod: 60,
|
||||
failureWindow: 15,
|
||||
},
|
||||
})).startup();
|
||||
```
|
||||
|
||||
If you prefer, you can pass a `function` as argument.
|
||||
|
||||
```javascript
|
||||
const knownUsersRules = (user) => {
|
||||
// apply some logic with this user
|
||||
return {
|
||||
failuresBeforeLockout,
|
||||
lockoutPeriod,
|
||||
failureWindow,
|
||||
};
|
||||
};
|
||||
|
||||
const unknownUsersRules = (connection) => {
|
||||
// apply some logic with this connection
|
||||
return {
|
||||
failuresBeforeLockout,
|
||||
lockoutPeriod,
|
||||
failureWindow,
|
||||
};
|
||||
};
|
||||
|
||||
(new AccountsLockout({
|
||||
knownUsers: knownUsersRules,
|
||||
unknownUsers: unknownUsersRules,
|
||||
})).startup();
|
||||
```
|
||||
|
||||
If you prefer, you can use `Meteor.settings`. It will overwrite any previous case.
|
||||
|
||||
```javascript
|
||||
"accounts-lockout": {
|
||||
"knownUsers": {
|
||||
"failuresBeforeLockout": 3,
|
||||
"lockoutPeriod": 60,
|
||||
"failureWindow": 10
|
||||
},
|
||||
"unknownUsers": {
|
||||
"failuresBeforeLockout": 3,
|
||||
"lockoutPeriod": 60,
|
||||
"failureWindow": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This package is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT).
|
||||
|
5
packages/wekan-accounts-lockout/accounts-lockout.js
Normal file
5
packages/wekan-accounts-lockout/accounts-lockout.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import AccountsLockout from './src/accountsLockout';
|
||||
|
||||
const Name = 'wekan-accounts-lockout';
|
||||
|
||||
export { Name, AccountsLockout };
|
18
packages/wekan-accounts-lockout/package.js
Normal file
18
packages/wekan-accounts-lockout/package.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
/* global Package */
|
||||
|
||||
Package.describe({
|
||||
name: 'wekan-accounts-lockout',
|
||||
version: '1.0.0',
|
||||
summary: 'Meteor package for locking user accounts and stopping brute force attacks',
|
||||
git: 'https://github.com/lucasantoniassi/meteor-accounts-lockout.git',
|
||||
documentation: 'README.md',
|
||||
});
|
||||
|
||||
Package.onUse((api) => {
|
||||
api.versionsFrom('1.4.2.3');
|
||||
api.use([
|
||||
'ecmascript',
|
||||
'accounts-password',
|
||||
]);
|
||||
api.mainModule('accounts-lockout.js');
|
||||
});
|
4
packages/wekan-accounts-lockout/package.json
Normal file
4
packages/wekan-accounts-lockout/package.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "wekan-accounts-lockout",
|
||||
"private": true
|
||||
}
|
29
packages/wekan-accounts-lockout/src/accountsLockout.js
Normal file
29
packages/wekan-accounts-lockout/src/accountsLockout.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
import KnownUser from './knownUser';
|
||||
import UnknownUser from './unknownUser';
|
||||
|
||||
class AccountsLockout {
|
||||
constructor({
|
||||
knownUsers = {
|
||||
failuresBeforeLockout: 3,
|
||||
lockoutPeriod: 60,
|
||||
failureWindow: 15,
|
||||
},
|
||||
unknownUsers = {
|
||||
failuresBeforeLockout: 3,
|
||||
lockoutPeriod: 60,
|
||||
failureWindow: 15,
|
||||
},
|
||||
}) {
|
||||
this.settings = {
|
||||
knownUsers,
|
||||
unknownUsers,
|
||||
};
|
||||
}
|
||||
|
||||
startup() {
|
||||
(new KnownUser(this.settings.knownUsers)).startup();
|
||||
(new UnknownUser(this.settings.unknownUsers)).startup();
|
||||
}
|
||||
}
|
||||
|
||||
export default AccountsLockout;
|
|
@ -0,0 +1,3 @@
|
|||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
export default new Meteor.Collection('AccountsLockout.Connections');
|
321
packages/wekan-accounts-lockout/src/knownUser.js
Normal file
321
packages/wekan-accounts-lockout/src/knownUser.js
Normal file
|
@ -0,0 +1,321 @@
|
|||
/* eslint-disable no-underscore-dangle */
|
||||
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
|
||||
class KnownUser {
|
||||
constructor(settings) {
|
||||
this.unchangedSettings = settings;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
startup() {
|
||||
if (!(this.unchangedSettings instanceof Function)) {
|
||||
this.updateSettings();
|
||||
}
|
||||
this.scheduleUnlocksForLockedAccounts();
|
||||
KnownUser.unlockAccountsIfLockoutAlreadyExpired();
|
||||
this.hookIntoAccounts();
|
||||
}
|
||||
|
||||
updateSettings() {
|
||||
const settings = KnownUser.knownUsers();
|
||||
if (settings) {
|
||||
settings.forEach(function updateSetting({ key, value }) {
|
||||
this.settings[key] = value;
|
||||
});
|
||||
}
|
||||
this.validateSettings();
|
||||
}
|
||||
|
||||
validateSettings() {
|
||||
if (
|
||||
!this.settings.failuresBeforeLockout ||
|
||||
this.settings.failuresBeforeLockout < 0
|
||||
) {
|
||||
throw new Error('"failuresBeforeLockout" is not positive integer');
|
||||
}
|
||||
if (
|
||||
!this.settings.lockoutPeriod ||
|
||||
this.settings.lockoutPeriod < 0
|
||||
) {
|
||||
throw new Error('"lockoutPeriod" is not positive integer');
|
||||
}
|
||||
if (
|
||||
!this.settings.failureWindow ||
|
||||
this.settings.failureWindow < 0
|
||||
) {
|
||||
throw new Error('"failureWindow" is not positive integer');
|
||||
}
|
||||
}
|
||||
|
||||
scheduleUnlocksForLockedAccounts() {
|
||||
const lockedAccountsCursor = Meteor.users.find(
|
||||
{
|
||||
'services.accounts-lockout.unlockTime': {
|
||||
$gt: Number(new Date()),
|
||||
},
|
||||
},
|
||||
{
|
||||
fields: {
|
||||
'services.accounts-lockout.unlockTime': 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
const currentTime = Number(new Date());
|
||||
lockedAccountsCursor.forEach((user) => {
|
||||
let lockDuration = KnownUser.unlockTime(user) - currentTime;
|
||||
if (lockDuration >= this.settings.lockoutPeriod) {
|
||||
lockDuration = this.settings.lockoutPeriod * 1000;
|
||||
}
|
||||
if (lockDuration <= 1) {
|
||||
lockDuration = 1;
|
||||
}
|
||||
Meteor.setTimeout(
|
||||
KnownUser.unlockAccount.bind(null, user._id),
|
||||
lockDuration,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
static unlockAccountsIfLockoutAlreadyExpired() {
|
||||
const currentTime = Number(new Date());
|
||||
const query = {
|
||||
'services.accounts-lockout.unlockTime': {
|
||||
$lt: currentTime,
|
||||
},
|
||||
};
|
||||
const data = {
|
||||
$unset: {
|
||||
'services.accounts-lockout.unlockTime': 0,
|
||||
'services.accounts-lockout.failedAttempts': 0,
|
||||
},
|
||||
};
|
||||
Meteor.users.update(query, data);
|
||||
}
|
||||
|
||||
hookIntoAccounts() {
|
||||
Accounts.validateLoginAttempt(this.validateLoginAttempt.bind(this));
|
||||
Accounts.onLogin(KnownUser.onLogin);
|
||||
}
|
||||
|
||||
|
||||
validateLoginAttempt(loginInfo) {
|
||||
if (
|
||||
// don't interrupt non-password logins
|
||||
loginInfo.type !== 'password' ||
|
||||
loginInfo.user === undefined ||
|
||||
// Don't handle errors unless they are due to incorrect password
|
||||
(loginInfo.error !== undefined && loginInfo.error.reason !== 'Incorrect password')
|
||||
) {
|
||||
return loginInfo.allowed;
|
||||
}
|
||||
|
||||
// If there was no login error and the account is NOT locked, don't interrupt
|
||||
const unlockTime = KnownUser.unlockTime(loginInfo.user);
|
||||
if (loginInfo.error === undefined && unlockTime === 0) {
|
||||
return loginInfo.allowed;
|
||||
}
|
||||
|
||||
if (this.unchangedSettings instanceof Function) {
|
||||
this.settings = this.unchangedSettings(loginInfo.user);
|
||||
this.validateSettings();
|
||||
}
|
||||
|
||||
const userId = loginInfo.user._id;
|
||||
let failedAttempts = 1 + KnownUser.failedAttempts(loginInfo.user);
|
||||
const firstFailedAttempt = KnownUser.firstFailedAttempt(loginInfo.user);
|
||||
const currentTime = Number(new Date());
|
||||
|
||||
const canReset = (currentTime - firstFailedAttempt) > (1000 * this.settings.failureWindow);
|
||||
if (canReset) {
|
||||
failedAttempts = 1;
|
||||
KnownUser.resetAttempts(failedAttempts, userId);
|
||||
}
|
||||
|
||||
const canIncrement = failedAttempts < this.settings.failuresBeforeLockout;
|
||||
if (canIncrement) {
|
||||
KnownUser.incrementAttempts(failedAttempts, userId);
|
||||
}
|
||||
|
||||
const maxAttemptsAllowed = this.settings.failuresBeforeLockout;
|
||||
const attemptsRemaining = maxAttemptsAllowed - failedAttempts;
|
||||
if (unlockTime > currentTime) {
|
||||
let duration = unlockTime - currentTime;
|
||||
duration = Math.ceil(duration / 1000);
|
||||
duration = duration > 1 ? duration : 1;
|
||||
KnownUser.tooManyAttempts(duration);
|
||||
}
|
||||
if (failedAttempts === maxAttemptsAllowed) {
|
||||
this.setNewUnlockTime(failedAttempts, userId);
|
||||
|
||||
let duration = this.settings.lockoutPeriod;
|
||||
duration = Math.ceil(duration);
|
||||
duration = duration > 1 ? duration : 1;
|
||||
return KnownUser.tooManyAttempts(duration);
|
||||
}
|
||||
return KnownUser.incorrectPassword(
|
||||
failedAttempts,
|
||||
maxAttemptsAllowed,
|
||||
attemptsRemaining,
|
||||
);
|
||||
}
|
||||
|
||||
static resetAttempts(
|
||||
failedAttempts,
|
||||
userId,
|
||||
) {
|
||||
const currentTime = Number(new Date());
|
||||
const query = { _id: userId };
|
||||
const data = {
|
||||
$set: {
|
||||
'services.accounts-lockout.failedAttempts': failedAttempts,
|
||||
'services.accounts-lockout.lastFailedAttempt': currentTime,
|
||||
'services.accounts-lockout.firstFailedAttempt': currentTime,
|
||||
},
|
||||
};
|
||||
Meteor.users.update(query, data);
|
||||
}
|
||||
|
||||
static incrementAttempts(
|
||||
failedAttempts,
|
||||
userId,
|
||||
) {
|
||||
const currentTime = Number(new Date());
|
||||
const query = { _id: userId };
|
||||
const data = {
|
||||
$set: {
|
||||
'services.accounts-lockout.failedAttempts': failedAttempts,
|
||||
'services.accounts-lockout.lastFailedAttempt': currentTime,
|
||||
},
|
||||
};
|
||||
Meteor.users.update(query, data);
|
||||
}
|
||||
|
||||
setNewUnlockTime(
|
||||
failedAttempts,
|
||||
userId,
|
||||
) {
|
||||
const currentTime = Number(new Date());
|
||||
const newUnlockTime = (1000 * this.settings.lockoutPeriod) + currentTime;
|
||||
const query = { _id: userId };
|
||||
const data = {
|
||||
$set: {
|
||||
'services.accounts-lockout.failedAttempts': failedAttempts,
|
||||
'services.accounts-lockout.lastFailedAttempt': currentTime,
|
||||
'services.accounts-lockout.unlockTime': newUnlockTime,
|
||||
},
|
||||
};
|
||||
Meteor.users.update(query, data);
|
||||
Meteor.setTimeout(
|
||||
KnownUser.unlockAccount.bind(null, userId),
|
||||
this.settings.lockoutPeriod * 1000,
|
||||
);
|
||||
}
|
||||
|
||||
static onLogin(loginInfo) {
|
||||
if (loginInfo.type !== 'password') {
|
||||
return;
|
||||
}
|
||||
const userId = loginInfo.user._id;
|
||||
const query = { _id: userId };
|
||||
const data = {
|
||||
$unset: {
|
||||
'services.accounts-lockout.unlockTime': 0,
|
||||
'services.accounts-lockout.failedAttempts': 0,
|
||||
},
|
||||
};
|
||||
Meteor.users.update(query, data);
|
||||
}
|
||||
|
||||
static incorrectPassword(
|
||||
failedAttempts,
|
||||
maxAttemptsAllowed,
|
||||
attemptsRemaining,
|
||||
) {
|
||||
throw new Meteor.Error(
|
||||
403,
|
||||
'Incorrect password',
|
||||
JSON.stringify({
|
||||
message: 'Incorrect password',
|
||||
failedAttempts,
|
||||
maxAttemptsAllowed,
|
||||
attemptsRemaining,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
static tooManyAttempts(duration) {
|
||||
throw new Meteor.Error(
|
||||
403,
|
||||
'Too many attempts',
|
||||
JSON.stringify({
|
||||
message: 'Wrong passwords were submitted too many times. Account is locked for a while.',
|
||||
duration,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
static knownUsers() {
|
||||
let knownUsers;
|
||||
try {
|
||||
knownUsers = Meteor.settings['accounts-lockout'].knownUsers;
|
||||
} catch (e) {
|
||||
knownUsers = false;
|
||||
}
|
||||
return knownUsers || false;
|
||||
}
|
||||
|
||||
static unlockTime(user) {
|
||||
let unlockTime;
|
||||
try {
|
||||
unlockTime = user.services['accounts-lockout'].unlockTime;
|
||||
} catch (e) {
|
||||
unlockTime = 0;
|
||||
}
|
||||
return unlockTime || 0;
|
||||
}
|
||||
|
||||
static failedAttempts(user) {
|
||||
let failedAttempts;
|
||||
try {
|
||||
failedAttempts = user.services['accounts-lockout'].failedAttempts;
|
||||
} catch (e) {
|
||||
failedAttempts = 0;
|
||||
}
|
||||
return failedAttempts || 0;
|
||||
}
|
||||
|
||||
static lastFailedAttempt(user) {
|
||||
let lastFailedAttempt;
|
||||
try {
|
||||
lastFailedAttempt = user.services['accounts-lockout'].lastFailedAttempt;
|
||||
} catch (e) {
|
||||
lastFailedAttempt = 0;
|
||||
}
|
||||
return lastFailedAttempt || 0;
|
||||
}
|
||||
|
||||
static firstFailedAttempt(user) {
|
||||
let firstFailedAttempt;
|
||||
try {
|
||||
firstFailedAttempt = user.services['accounts-lockout'].firstFailedAttempt;
|
||||
} catch (e) {
|
||||
firstFailedAttempt = 0;
|
||||
}
|
||||
return firstFailedAttempt || 0;
|
||||
}
|
||||
|
||||
static unlockAccount(userId) {
|
||||
const query = { _id: userId };
|
||||
const data = {
|
||||
$unset: {
|
||||
'services.accounts-lockout.unlockTime': 0,
|
||||
'services.accounts-lockout.failedAttempts': 0,
|
||||
},
|
||||
};
|
||||
Meteor.users.update(query, data);
|
||||
}
|
||||
}
|
||||
|
||||
export default KnownUser;
|
329
packages/wekan-accounts-lockout/src/unknownUser.js
Normal file
329
packages/wekan-accounts-lockout/src/unknownUser.js
Normal file
|
@ -0,0 +1,329 @@
|
|||
import { Meteor } from 'meteor/meteor';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import _AccountsLockoutCollection from './accountsLockoutCollection';
|
||||
|
||||
class UnknownUser {
|
||||
constructor(
|
||||
settings,
|
||||
{
|
||||
AccountsLockoutCollection = _AccountsLockoutCollection,
|
||||
} = {},
|
||||
) {
|
||||
this.AccountsLockoutCollection = AccountsLockoutCollection;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
startup() {
|
||||
if (!(this.settings instanceof Function)) {
|
||||
this.updateSettings();
|
||||
}
|
||||
this.scheduleUnlocksForLockedAccounts();
|
||||
this.unlockAccountsIfLockoutAlreadyExpired();
|
||||
this.hookIntoAccounts();
|
||||
}
|
||||
|
||||
updateSettings() {
|
||||
const settings = UnknownUser.unknownUsers();
|
||||
if (settings) {
|
||||
settings.forEach(function updateSetting({ key, value }) {
|
||||
this.settings[key] = value;
|
||||
});
|
||||
}
|
||||
this.validateSettings();
|
||||
}
|
||||
|
||||
validateSettings() {
|
||||
if (
|
||||
!this.settings.failuresBeforeLockout ||
|
||||
this.settings.failuresBeforeLockout < 0
|
||||
) {
|
||||
throw new Error('"failuresBeforeLockout" is not positive integer');
|
||||
}
|
||||
if (
|
||||
!this.settings.lockoutPeriod ||
|
||||
this.settings.lockoutPeriod < 0
|
||||
) {
|
||||
throw new Error('"lockoutPeriod" is not positive integer');
|
||||
}
|
||||
if (
|
||||
!this.settings.failureWindow ||
|
||||
this.settings.failureWindow < 0
|
||||
) {
|
||||
throw new Error('"failureWindow" is not positive integer');
|
||||
}
|
||||
}
|
||||
|
||||
scheduleUnlocksForLockedAccounts() {
|
||||
const lockedAccountsCursor = this.AccountsLockoutCollection.find(
|
||||
{
|
||||
'services.accounts-lockout.unlockTime': {
|
||||
$gt: Number(new Date()),
|
||||
},
|
||||
},
|
||||
{
|
||||
fields: {
|
||||
'services.accounts-lockout.unlockTime': 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
const currentTime = Number(new Date());
|
||||
lockedAccountsCursor.forEach((connection) => {
|
||||
let lockDuration = this.unlockTime(connection) - currentTime;
|
||||
if (lockDuration >= this.settings.lockoutPeriod) {
|
||||
lockDuration = this.settings.lockoutPeriod * 1000;
|
||||
}
|
||||
if (lockDuration <= 1) {
|
||||
lockDuration = 1;
|
||||
}
|
||||
Meteor.setTimeout(
|
||||
this.unlockAccount.bind(this, connection.clientAddress),
|
||||
lockDuration,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
unlockAccountsIfLockoutAlreadyExpired() {
|
||||
const currentTime = Number(new Date());
|
||||
const query = {
|
||||
'services.accounts-lockout.unlockTime': {
|
||||
$lt: currentTime,
|
||||
},
|
||||
};
|
||||
const data = {
|
||||
$unset: {
|
||||
'services.accounts-lockout.unlockTime': 0,
|
||||
'services.accounts-lockout.failedAttempts': 0,
|
||||
},
|
||||
};
|
||||
this.AccountsLockoutCollection.update(query, data);
|
||||
}
|
||||
|
||||
hookIntoAccounts() {
|
||||
Accounts.validateLoginAttempt(this.validateLoginAttempt.bind(this));
|
||||
Accounts.onLogin(this.onLogin.bind(this));
|
||||
}
|
||||
|
||||
validateLoginAttempt(loginInfo) {
|
||||
// don't interrupt non-password logins
|
||||
if (
|
||||
loginInfo.type !== 'password' ||
|
||||
loginInfo.user !== undefined ||
|
||||
loginInfo.error === undefined ||
|
||||
loginInfo.error.reason !== 'User not found'
|
||||
) {
|
||||
return loginInfo.allowed;
|
||||
}
|
||||
|
||||
if (this.settings instanceof Function) {
|
||||
this.settings = this.settings(loginInfo.connection);
|
||||
this.validateSettings();
|
||||
}
|
||||
|
||||
const clientAddress = loginInfo.connection.clientAddress;
|
||||
const unlockTime = this.unlockTime(loginInfo.connection);
|
||||
let failedAttempts = 1 + this.failedAttempts(loginInfo.connection);
|
||||
const firstFailedAttempt = this.firstFailedAttempt(loginInfo.connection);
|
||||
const currentTime = Number(new Date());
|
||||
|
||||
const canReset = (currentTime - firstFailedAttempt) > (1000 * this.settings.failureWindow);
|
||||
if (canReset) {
|
||||
failedAttempts = 1;
|
||||
this.resetAttempts(failedAttempts, clientAddress);
|
||||
}
|
||||
|
||||
const canIncrement = failedAttempts < this.settings.failuresBeforeLockout;
|
||||
if (canIncrement) {
|
||||
this.incrementAttempts(failedAttempts, clientAddress);
|
||||
}
|
||||
|
||||
const maxAttemptsAllowed = this.settings.failuresBeforeLockout;
|
||||
const attemptsRemaining = maxAttemptsAllowed - failedAttempts;
|
||||
if (unlockTime > currentTime) {
|
||||
let duration = unlockTime - currentTime;
|
||||
duration = Math.ceil(duration / 1000);
|
||||
duration = duration > 1 ? duration : 1;
|
||||
UnknownUser.tooManyAttempts(duration);
|
||||
}
|
||||
if (failedAttempts === maxAttemptsAllowed) {
|
||||
this.setNewUnlockTime(failedAttempts, clientAddress);
|
||||
|
||||
let duration = this.settings.lockoutPeriod;
|
||||
duration = Math.ceil(duration);
|
||||
duration = duration > 1 ? duration : 1;
|
||||
return UnknownUser.tooManyAttempts(duration);
|
||||
}
|
||||
return UnknownUser.userNotFound(
|
||||
failedAttempts,
|
||||
maxAttemptsAllowed,
|
||||
attemptsRemaining,
|
||||
);
|
||||
}
|
||||
|
||||
resetAttempts(
|
||||
failedAttempts,
|
||||
clientAddress,
|
||||
) {
|
||||
const currentTime = Number(new Date());
|
||||
const query = { clientAddress };
|
||||
const data = {
|
||||
$set: {
|
||||
'services.accounts-lockout.failedAttempts': failedAttempts,
|
||||
'services.accounts-lockout.lastFailedAttempt': currentTime,
|
||||
'services.accounts-lockout.firstFailedAttempt': currentTime,
|
||||
},
|
||||
};
|
||||
this.AccountsLockoutCollection.upsert(query, data);
|
||||
}
|
||||
|
||||
incrementAttempts(
|
||||
failedAttempts,
|
||||
clientAddress,
|
||||
) {
|
||||
const currentTime = Number(new Date());
|
||||
const query = { clientAddress };
|
||||
const data = {
|
||||
$set: {
|
||||
'services.accounts-lockout.failedAttempts': failedAttempts,
|
||||
'services.accounts-lockout.lastFailedAttempt': currentTime,
|
||||
},
|
||||
};
|
||||
this.AccountsLockoutCollection.upsert(query, data);
|
||||
}
|
||||
|
||||
setNewUnlockTime(
|
||||
failedAttempts,
|
||||
clientAddress,
|
||||
) {
|
||||
const currentTime = Number(new Date());
|
||||
const newUnlockTime = (1000 * this.settings.lockoutPeriod) + currentTime;
|
||||
const query = { clientAddress };
|
||||
const data = {
|
||||
$set: {
|
||||
'services.accounts-lockout.failedAttempts': failedAttempts,
|
||||
'services.accounts-lockout.lastFailedAttempt': currentTime,
|
||||
'services.accounts-lockout.unlockTime': newUnlockTime,
|
||||
},
|
||||
};
|
||||
this.AccountsLockoutCollection.upsert(query, data);
|
||||
Meteor.setTimeout(
|
||||
this.unlockAccount.bind(this, clientAddress),
|
||||
this.settings.lockoutPeriod * 1000,
|
||||
);
|
||||
}
|
||||
|
||||
onLogin(loginInfo) {
|
||||
if (loginInfo.type !== 'password') {
|
||||
return;
|
||||
}
|
||||
const clientAddress = loginInfo.connection.clientAddress;
|
||||
const query = { clientAddress };
|
||||
const data = {
|
||||
$unset: {
|
||||
'services.accounts-lockout.unlockTime': 0,
|
||||
'services.accounts-lockout.failedAttempts': 0,
|
||||
},
|
||||
};
|
||||
this.AccountsLockoutCollection.update(query, data);
|
||||
}
|
||||
|
||||
static userNotFound(
|
||||
failedAttempts,
|
||||
maxAttemptsAllowed,
|
||||
attemptsRemaining,
|
||||
) {
|
||||
throw new Meteor.Error(
|
||||
403,
|
||||
'User not found',
|
||||
JSON.stringify({
|
||||
message: 'User not found',
|
||||
failedAttempts,
|
||||
maxAttemptsAllowed,
|
||||
attemptsRemaining,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
static tooManyAttempts(duration) {
|
||||
throw new Meteor.Error(
|
||||
403,
|
||||
'Too many attempts',
|
||||
JSON.stringify({
|
||||
message: 'Wrong emails were submitted too many times. Account is locked for a while.',
|
||||
duration,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
static unknownUsers() {
|
||||
let unknownUsers;
|
||||
try {
|
||||
unknownUsers = Meteor.settings['accounts-lockout'].unknownUsers;
|
||||
} catch (e) {
|
||||
unknownUsers = false;
|
||||
}
|
||||
return unknownUsers || false;
|
||||
}
|
||||
|
||||
findOneByConnection(connection) {
|
||||
return this.AccountsLockoutCollection.findOne({
|
||||
clientAddress: connection.clientAddress,
|
||||
});
|
||||
}
|
||||
|
||||
unlockTime(connection) {
|
||||
connection = this.findOneByConnection(connection);
|
||||
let unlockTime;
|
||||
try {
|
||||
unlockTime = connection.services['accounts-lockout'].unlockTime;
|
||||
} catch (e) {
|
||||
unlockTime = 0;
|
||||
}
|
||||
return unlockTime || 0;
|
||||
}
|
||||
|
||||
failedAttempts(connection) {
|
||||
connection = this.findOneByConnection(connection);
|
||||
let failedAttempts;
|
||||
try {
|
||||
failedAttempts = connection.services['accounts-lockout'].failedAttempts;
|
||||
} catch (e) {
|
||||
failedAttempts = 0;
|
||||
}
|
||||
return failedAttempts || 0;
|
||||
}
|
||||
|
||||
lastFailedAttempt(connection) {
|
||||
connection = this.findOneByConnection(connection);
|
||||
let lastFailedAttempt;
|
||||
try {
|
||||
lastFailedAttempt = connection.services['accounts-lockout'].lastFailedAttempt;
|
||||
} catch (e) {
|
||||
lastFailedAttempt = 0;
|
||||
}
|
||||
return lastFailedAttempt || 0;
|
||||
}
|
||||
|
||||
firstFailedAttempt(connection) {
|
||||
connection = this.findOneByConnection(connection);
|
||||
let firstFailedAttempt;
|
||||
try {
|
||||
firstFailedAttempt = connection.services['accounts-lockout'].firstFailedAttempt;
|
||||
} catch (e) {
|
||||
firstFailedAttempt = 0;
|
||||
}
|
||||
return firstFailedAttempt || 0;
|
||||
}
|
||||
|
||||
unlockAccount(clientAddress) {
|
||||
const query = { clientAddress };
|
||||
const data = {
|
||||
$unset: {
|
||||
'services.accounts-lockout.unlockTime': 0,
|
||||
'services.accounts-lockout.failedAttempts': 0,
|
||||
},
|
||||
};
|
||||
this.AccountsLockoutCollection.update(query, data);
|
||||
}
|
||||
}
|
||||
|
||||
export default UnknownUser;
|
|
@ -1,21 +1,26 @@
|
|||
// https://atmospherejs.com/lucasantoniassi/accounts-lockout
|
||||
// server
|
||||
import { AccountsLockout } from 'meteor/lucasantoniassi:accounts-lockout';
|
||||
Meteor.startup(() => {
|
||||
// https://atmospherejs.com/lucasantoniassi/accounts-lockout
|
||||
// server
|
||||
|
||||
new AccountsLockout({
|
||||
knownUsers: {
|
||||
failuresBeforeLockout:
|
||||
process.env.ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE || 3,
|
||||
lockoutPeriod: process.env.ACCOUNTS_LOCKOUT_KNOWN_USERS_PERIOD || 60,
|
||||
failureWindow:
|
||||
process.env.ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURE_WINDOW || 15,
|
||||
},
|
||||
unknownUsers: {
|
||||
failuresBeforeLockout:
|
||||
process.env.ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURES_BERORE || 3,
|
||||
lockoutPeriod:
|
||||
process.env.ACCOUNTS_LOCKOUT_UNKNOWN_USERS_LOCKOUT_PERIOD || 60,
|
||||
failureWindow:
|
||||
process.env.ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURE_WINDOW || 15,
|
||||
},
|
||||
}).startup();
|
||||
if (Meteor.isServer) {
|
||||
import { AccountsLockout } from 'meteor/wekan-accounts-lockout';
|
||||
|
||||
new AccountsLockout({
|
||||
knownUsers: {
|
||||
failuresBeforeLockout:
|
||||
process.env.ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURES_BEFORE || 3,
|
||||
lockoutPeriod: process.env.ACCOUNTS_LOCKOUT_KNOWN_USERS_PERIOD || 60,
|
||||
failureWindow:
|
||||
process.env.ACCOUNTS_LOCKOUT_KNOWN_USERS_FAILURE_WINDOW || 15,
|
||||
},
|
||||
unknownUsers: {
|
||||
failuresBeforeLockout:
|
||||
process.env.ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURES_BERORE || 3,
|
||||
lockoutPeriod:
|
||||
process.env.ACCOUNTS_LOCKOUT_UNKNOWN_USERS_LOCKOUT_PERIOD || 60,
|
||||
failureWindow:
|
||||
process.env.ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURE_WINDOW || 15,
|
||||
},
|
||||
}).startup();
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue