diff --git a/.meteor/versions b/.meteor/versions index da76e4646..3c512c95f 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -138,7 +138,7 @@ useraccounts:unstyled@1.14.2 webapp@1.13.8 webapp-hashing@1.1.1 wekan-accounts-cas@0.1.0 -wekan-accounts-lockout@1.0.0 +wekan-accounts-lockout@1.1.0 wekan-accounts-oidc@1.0.10 wekan-accounts-sandstorm@0.8.0 wekan-fontawesome@6.4.2 diff --git a/models/lockoutSettings.js b/models/lockoutSettings.js index d0f4dfa6e..9020f6227 100644 --- a/models/lockoutSettings.js +++ b/models/lockoutSettings.js @@ -59,7 +59,7 @@ if (Meteor.isServer) { await LockoutSettings._collection.createIndexAsync({ modifiedAt: -1 }); // Known users settings - LockoutSettings.upsert( + await LockoutSettings.upsertAsync( { _id: 'known-failuresBeforeLockout' }, { $setOnInsert: { @@ -71,7 +71,7 @@ if (Meteor.isServer) { }, ); - LockoutSettings.upsert( + await LockoutSettings.upsertAsync( { _id: 'known-lockoutPeriod' }, { $setOnInsert: { @@ -83,7 +83,7 @@ if (Meteor.isServer) { }, ); - LockoutSettings.upsert( + await LockoutSettings.upsertAsync( { _id: 'known-failureWindow' }, { $setOnInsert: { @@ -99,7 +99,7 @@ if (Meteor.isServer) { const typoVar = process.env.ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURES_BERORE; const correctVar = process.env.ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURES_BEFORE; - LockoutSettings.upsert( + await LockoutSettings.upsertAsync( { _id: 'unknown-failuresBeforeLockout' }, { $setOnInsert: { @@ -111,7 +111,7 @@ if (Meteor.isServer) { }, ); - LockoutSettings.upsert( + await LockoutSettings.upsertAsync( { _id: 'unknown-lockoutPeriod' }, { $setOnInsert: { @@ -123,7 +123,7 @@ if (Meteor.isServer) { }, ); - LockoutSettings.upsert( + await LockoutSettings.upsertAsync( { _id: 'unknown-failureWindow' }, { $setOnInsert: { diff --git a/packages/wekan-accounts-lockout/package.js b/packages/wekan-accounts-lockout/package.js index 7f1a64b21..7909e667f 100644 --- a/packages/wekan-accounts-lockout/package.js +++ b/packages/wekan-accounts-lockout/package.js @@ -2,7 +2,7 @@ Package.describe({ name: 'wekan-accounts-lockout', - version: '1.0.0', + version: '1.1.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', diff --git a/packages/wekan-accounts-lockout/src/knownUser.js b/packages/wekan-accounts-lockout/src/knownUser.js index 81558e1b8..2f4d2012e 100644 --- a/packages/wekan-accounts-lockout/src/knownUser.js +++ b/packages/wekan-accounts-lockout/src/knownUser.js @@ -9,12 +9,12 @@ class KnownUser { this.settings = settings; } - startup() { + async startup() { if (!(this.unchangedSettings instanceof Function)) { this.updateSettings(); } - this.scheduleUnlocksForLockedAccounts(); - KnownUser.unlockAccountsIfLockoutAlreadyExpired(); + await this.scheduleUnlocksForLockedAccounts(); + await KnownUser.unlockAccountsIfLockoutAlreadyExpired(); this.hookIntoAccounts(); } @@ -49,7 +49,7 @@ class KnownUser { } } - scheduleUnlocksForLockedAccounts() { + async scheduleUnlocksForLockedAccounts() { const lockedAccountsCursor = Meteor.users.find( { 'services.accounts-lockout.unlockTime': { @@ -63,7 +63,7 @@ class KnownUser { }, ); const currentTime = Number(new Date()); - lockedAccountsCursor.forEach((user) => { + for await (const user of lockedAccountsCursor) { let lockDuration = KnownUser.unlockTime(user) - currentTime; if (lockDuration >= this.settings.lockoutPeriod) { lockDuration = this.settings.lockoutPeriod * 1000; @@ -75,10 +75,10 @@ class KnownUser { KnownUser.unlockAccount.bind(null, user._id), lockDuration, ); - }); + } } - static unlockAccountsIfLockoutAlreadyExpired() { + static async unlockAccountsIfLockoutAlreadyExpired() { const currentTime = Number(new Date()); const query = { 'services.accounts-lockout.unlockTime': { @@ -91,7 +91,7 @@ class KnownUser { 'services.accounts-lockout.failedAttempts': 0, }, }; - Meteor.users.update(query, data); + await Meteor.users.updateAsync(query, data); } hookIntoAccounts() { @@ -100,7 +100,7 @@ class KnownUser { } - validateLoginAttempt(loginInfo) { + async validateLoginAttempt(loginInfo) { if ( // don't interrupt non-password logins loginInfo.type !== 'password' || @@ -130,12 +130,12 @@ class KnownUser { const canReset = (currentTime - firstFailedAttempt) > (1000 * this.settings.failureWindow); if (canReset) { failedAttempts = 1; - KnownUser.resetAttempts(failedAttempts, userId); + await KnownUser.resetAttempts(failedAttempts, userId); } const canIncrement = failedAttempts < this.settings.failuresBeforeLockout; if (canIncrement) { - KnownUser.incrementAttempts(failedAttempts, userId); + await KnownUser.incrementAttempts(failedAttempts, userId); } const maxAttemptsAllowed = this.settings.failuresBeforeLockout; @@ -147,7 +147,7 @@ class KnownUser { KnownUser.tooManyAttempts(duration); } if (failedAttempts === maxAttemptsAllowed) { - this.setNewUnlockTime(failedAttempts, userId); + await this.setNewUnlockTime(failedAttempts, userId); let duration = this.settings.lockoutPeriod; duration = Math.ceil(duration); @@ -161,7 +161,7 @@ class KnownUser { ); } - static resetAttempts( + static async resetAttempts( failedAttempts, userId, ) { @@ -174,10 +174,10 @@ class KnownUser { 'services.accounts-lockout.firstFailedAttempt': currentTime, }, }; - Meteor.users.update(query, data); + await Meteor.users.updateAsync(query, data); } - static incrementAttempts( + static async incrementAttempts( failedAttempts, userId, ) { @@ -189,10 +189,10 @@ class KnownUser { 'services.accounts-lockout.lastFailedAttempt': currentTime, }, }; - Meteor.users.update(query, data); + await Meteor.users.updateAsync(query, data); } - setNewUnlockTime( + async setNewUnlockTime( failedAttempts, userId, ) { @@ -206,14 +206,14 @@ class KnownUser { 'services.accounts-lockout.unlockTime': newUnlockTime, }, }; - Meteor.users.update(query, data); + await Meteor.users.updateAsync(query, data); Meteor.setTimeout( KnownUser.unlockAccount.bind(null, userId), this.settings.lockoutPeriod * 1000, ); } - static onLogin(loginInfo) { + static async onLogin(loginInfo) { if (loginInfo.type !== 'password') { return; } @@ -225,7 +225,7 @@ class KnownUser { 'services.accounts-lockout.failedAttempts': 0, }, }; - Meteor.users.update(query, data); + await Meteor.users.updateAsync(query, data); } static incorrectPassword( @@ -306,7 +306,7 @@ class KnownUser { return firstFailedAttempt || 0; } - static unlockAccount(userId) { + static async unlockAccount(userId) { const query = { _id: userId }; const data = { $unset: { @@ -314,7 +314,7 @@ class KnownUser { 'services.accounts-lockout.failedAttempts': 0, }, }; - Meteor.users.update(query, data); + await Meteor.users.updateAsync(query, data); } } diff --git a/packages/wekan-accounts-lockout/src/unknownUser.js b/packages/wekan-accounts-lockout/src/unknownUser.js index 443507c82..9226969e6 100644 --- a/packages/wekan-accounts-lockout/src/unknownUser.js +++ b/packages/wekan-accounts-lockout/src/unknownUser.js @@ -13,12 +13,12 @@ class UnknownUser { this.settings = settings; } - startup() { + async startup() { if (!(this.settings instanceof Function)) { this.updateSettings(); } - this.scheduleUnlocksForLockedAccounts(); - this.unlockAccountsIfLockoutAlreadyExpired(); + await this.scheduleUnlocksForLockedAccounts(); + await this.unlockAccountsIfLockoutAlreadyExpired(); this.hookIntoAccounts(); } @@ -53,7 +53,7 @@ class UnknownUser { } } - scheduleUnlocksForLockedAccounts() { + async scheduleUnlocksForLockedAccounts() { const lockedAccountsCursor = this.AccountsLockoutCollection.find( { 'services.accounts-lockout.unlockTime': { @@ -67,8 +67,8 @@ class UnknownUser { }, ); const currentTime = Number(new Date()); - lockedAccountsCursor.forEach((connection) => { - let lockDuration = this.unlockTime(connection) - currentTime; + for await (const connection of lockedAccountsCursor) { + let lockDuration = await this.unlockTime(connection) - currentTime; if (lockDuration >= this.settings.lockoutPeriod) { lockDuration = this.settings.lockoutPeriod * 1000; } @@ -79,10 +79,10 @@ class UnknownUser { this.unlockAccount.bind(this, connection.clientAddress), lockDuration, ); - }); + } } - unlockAccountsIfLockoutAlreadyExpired() { + async unlockAccountsIfLockoutAlreadyExpired() { const currentTime = Number(new Date()); const query = { 'services.accounts-lockout.unlockTime': { @@ -95,7 +95,7 @@ class UnknownUser { 'services.accounts-lockout.failedAttempts': 0, }, }; - this.AccountsLockoutCollection.update(query, data); + await this.AccountsLockoutCollection.updateAsync(query, data); } hookIntoAccounts() { @@ -103,7 +103,7 @@ class UnknownUser { Accounts.onLogin(this.onLogin.bind(this)); } - validateLoginAttempt(loginInfo) { + async validateLoginAttempt(loginInfo) { // don't interrupt non-password logins if ( loginInfo.type !== 'password' || @@ -120,20 +120,20 @@ class UnknownUser { } 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 unlockTime = await this.unlockTime(loginInfo.connection); + let failedAttempts = 1 + await this.failedAttempts(loginInfo.connection); + const firstFailedAttempt = await 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); + await this.resetAttempts(failedAttempts, clientAddress); } const canIncrement = failedAttempts < this.settings.failuresBeforeLockout; if (canIncrement) { - this.incrementAttempts(failedAttempts, clientAddress); + await this.incrementAttempts(failedAttempts, clientAddress); } const maxAttemptsAllowed = this.settings.failuresBeforeLockout; @@ -145,7 +145,7 @@ class UnknownUser { UnknownUser.tooManyAttempts(duration); } if (failedAttempts === maxAttemptsAllowed) { - this.setNewUnlockTime(failedAttempts, clientAddress); + await this.setNewUnlockTime(failedAttempts, clientAddress); let duration = this.settings.lockoutPeriod; duration = Math.ceil(duration); @@ -159,7 +159,7 @@ class UnknownUser { ); } - resetAttempts( + async resetAttempts( failedAttempts, clientAddress, ) { @@ -172,10 +172,10 @@ class UnknownUser { 'services.accounts-lockout.firstFailedAttempt': currentTime, }, }; - this.AccountsLockoutCollection.upsert(query, data); + await this.AccountsLockoutCollection.upsertAsync(query, data); } - incrementAttempts( + async incrementAttempts( failedAttempts, clientAddress, ) { @@ -187,10 +187,10 @@ class UnknownUser { 'services.accounts-lockout.lastFailedAttempt': currentTime, }, }; - this.AccountsLockoutCollection.upsert(query, data); + await this.AccountsLockoutCollection.upsertAsync(query, data); } - setNewUnlockTime( + async setNewUnlockTime( failedAttempts, clientAddress, ) { @@ -204,14 +204,14 @@ class UnknownUser { 'services.accounts-lockout.unlockTime': newUnlockTime, }, }; - this.AccountsLockoutCollection.upsert(query, data); + await this.AccountsLockoutCollection.upsertAsync(query, data); Meteor.setTimeout( this.unlockAccount.bind(this, clientAddress), this.settings.lockoutPeriod * 1000, ); } - onLogin(loginInfo) { + async onLogin(loginInfo) { if (loginInfo.type !== 'password') { return; } @@ -223,7 +223,7 @@ class UnknownUser { 'services.accounts-lockout.failedAttempts': 0, }, }; - this.AccountsLockoutCollection.update(query, data); + await this.AccountsLockoutCollection.updateAsync(query, data); } static userNotFound( @@ -264,14 +264,14 @@ class UnknownUser { return unknownUsers || false; } - findOneByConnection(connection) { - return this.AccountsLockoutCollection.findOne({ + async findOneByConnection(connection) { + return await this.AccountsLockoutCollection.findOneAsync({ clientAddress: connection.clientAddress, }); } - unlockTime(connection) { - connection = this.findOneByConnection(connection); + async unlockTime(connection) { + connection = await this.findOneByConnection(connection); let unlockTime; try { unlockTime = connection.services['accounts-lockout'].unlockTime; @@ -281,8 +281,8 @@ class UnknownUser { return unlockTime || 0; } - failedAttempts(connection) { - connection = this.findOneByConnection(connection); + async failedAttempts(connection) { + connection = await this.findOneByConnection(connection); let failedAttempts; try { failedAttempts = connection.services['accounts-lockout'].failedAttempts; @@ -292,8 +292,8 @@ class UnknownUser { return failedAttempts || 0; } - lastFailedAttempt(connection) { - connection = this.findOneByConnection(connection); + async lastFailedAttempt(connection) { + connection = await this.findOneByConnection(connection); let lastFailedAttempt; try { lastFailedAttempt = connection.services['accounts-lockout'].lastFailedAttempt; @@ -303,8 +303,8 @@ class UnknownUser { return lastFailedAttempt || 0; } - firstFailedAttempt(connection) { - connection = this.findOneByConnection(connection); + async firstFailedAttempt(connection) { + connection = await this.findOneByConnection(connection); let firstFailedAttempt; try { firstFailedAttempt = connection.services['accounts-lockout'].firstFailedAttempt; @@ -314,7 +314,7 @@ class UnknownUser { return firstFailedAttempt || 0; } - unlockAccount(clientAddress) { + async unlockAccount(clientAddress) { const query = { clientAddress }; const data = { $unset: { @@ -322,7 +322,7 @@ class UnknownUser { 'services.accounts-lockout.failedAttempts': 0, }, }; - this.AccountsLockoutCollection.update(query, data); + await this.AccountsLockoutCollection.updateAsync(query, data); } } diff --git a/server/accounts-lockout-config.js b/server/accounts-lockout-config.js index af437c25b..e14322ae7 100644 --- a/server/accounts-lockout-config.js +++ b/server/accounts-lockout-config.js @@ -1,21 +1,21 @@ import { AccountsLockout } from 'meteor/wekan-accounts-lockout'; import LockoutSettings from '/models/lockoutSettings'; -Meteor.startup(() => { +Meteor.startup(async () => { // Wait for the database to be ready - Meteor.setTimeout(() => { + Meteor.setTimeout(async () => { try { // Get configurations from database const knownUsersConfig = { - failuresBeforeLockout: LockoutSettings.findOne('known-failuresBeforeLockout')?.value || 3, - lockoutPeriod: LockoutSettings.findOne('known-lockoutPeriod')?.value || 60, - failureWindow: LockoutSettings.findOne('known-failureWindow')?.value || 15 + failuresBeforeLockout: (await LockoutSettings.findOneAsync('known-failuresBeforeLockout'))?.value || 3, + lockoutPeriod: (await LockoutSettings.findOneAsync('known-lockoutPeriod'))?.value || 60, + failureWindow: (await LockoutSettings.findOneAsync('known-failureWindow'))?.value || 15 }; const unknownUsersConfig = { - failuresBeforeLockout: LockoutSettings.findOne('unknown-failuresBeforeLockout')?.value || 3, - lockoutPeriod: LockoutSettings.findOne('unknown-lockoutPeriod')?.value || 60, - failureWindow: LockoutSettings.findOne('unknown-failureWindow')?.value || 15 + failuresBeforeLockout: (await LockoutSettings.findOneAsync('unknown-failuresBeforeLockout'))?.value || 3, + lockoutPeriod: (await LockoutSettings.findOneAsync('unknown-lockoutPeriod'))?.value || 60, + failureWindow: (await LockoutSettings.findOneAsync('unknown-failureWindow'))?.value || 15 }; // Initialize the AccountsLockout with configuration diff --git a/server/methods/lockedUsers.js b/server/methods/lockedUsers.js index e4eaf8bbc..a5de5075b 100644 --- a/server/methods/lockedUsers.js +++ b/server/methods/lockedUsers.js @@ -2,7 +2,7 @@ import { ReactiveCache } from '/imports/reactiveCache'; // Method to find locked users and release them if needed Meteor.methods({ - getLockedUsers() { + async getLockedUsers() { // Check if user has admin rights const userId = Meteor.userId(); if (!userId) { @@ -17,7 +17,7 @@ Meteor.methods({ const currentTime = Number(new Date()); // Find users that are locked (known users) - const lockedUsers = Meteor.users.find( + const lockedUsers = await Meteor.users.find( { 'services.accounts-lockout.unlockTime': { $gt: currentTime, @@ -32,7 +32,7 @@ Meteor.methods({ 'services.accounts-lockout.failedAttempts': 1 } } - ).fetch(); + ).fetchAsync(); // Format the results for the UI return lockedUsers.map(user => { @@ -50,7 +50,7 @@ Meteor.methods({ }); }, - unlockUser(userId) { + async unlockUser(userId) { // Check if user has admin rights const adminId = Meteor.userId(); if (!adminId) { @@ -62,13 +62,13 @@ Meteor.methods({ } // Make sure the user to unlock exists - const userToUnlock = Meteor.users.findOne(userId); + const userToUnlock = await Meteor.users.findOneAsync(userId); if (!userToUnlock) { throw new Meteor.Error('error-user-not-found', 'User not found'); } // Unlock the user - Meteor.users.update( + await Meteor.users.updateAsync( { _id: userId }, { $unset: { @@ -80,7 +80,7 @@ Meteor.methods({ return true; }, - unlockAllUsers() { + async unlockAllUsers() { // Check if user has admin rights const adminId = Meteor.userId(); if (!adminId) { @@ -92,7 +92,7 @@ Meteor.methods({ } // Unlock all users - Meteor.users.update( + await Meteor.users.updateAsync( { 'services.accounts-lockout.unlockTime': { $exists: true } }, { $unset: { diff --git a/server/methods/lockoutSettings.js b/server/methods/lockoutSettings.js index 047999bdc..cf12b083a 100644 --- a/server/methods/lockoutSettings.js +++ b/server/methods/lockoutSettings.js @@ -3,7 +3,7 @@ import { ReactiveCache } from '/imports/reactiveCache'; import LockoutSettings from '/models/lockoutSettings'; Meteor.methods({ - reloadAccountsLockout() { + async reloadAccountsLockout() { // Check if user has admin rights const userId = Meteor.userId(); if (!userId) { @@ -17,15 +17,15 @@ Meteor.methods({ try { // Get configurations from database const knownUsersConfig = { - failuresBeforeLockout: LockoutSettings.findOne('known-failuresBeforeLockout')?.value || 3, - lockoutPeriod: LockoutSettings.findOne('known-lockoutPeriod')?.value || 60, - failureWindow: LockoutSettings.findOne('known-failureWindow')?.value || 15 + failuresBeforeLockout: (await LockoutSettings.findOneAsync('known-failuresBeforeLockout'))?.value || 3, + lockoutPeriod: (await LockoutSettings.findOneAsync('known-lockoutPeriod'))?.value || 60, + failureWindow: (await LockoutSettings.findOneAsync('known-failureWindow'))?.value || 15 }; const unknownUsersConfig = { - failuresBeforeLockout: LockoutSettings.findOne('unknown-failuresBeforeLockout')?.value || 3, - lockoutPeriod: LockoutSettings.findOne('unknown-lockoutPeriod')?.value || 60, - failureWindow: LockoutSettings.findOne('unknown-failureWindow')?.value || 15 + failuresBeforeLockout: (await LockoutSettings.findOneAsync('unknown-failuresBeforeLockout'))?.value || 3, + lockoutPeriod: (await LockoutSettings.findOneAsync('unknown-lockoutPeriod'))?.value || 60, + failureWindow: (await LockoutSettings.findOneAsync('unknown-failureWindow'))?.value || 15 }; // Initialize the AccountsLockout with configuration