123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 |
- 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;
|