Adds ability to set an account lockout policy (#2601)

* Adds ability to set account lockout policy

* change fit to it in tests
This commit is contained in:
Diwakar Cherukumilli
2016-09-02 17:00:47 -07:00
committed by Florent Vilmart
parent f6516a1d1e
commit 28bd37884d
10 changed files with 614 additions and 24 deletions

View File

@@ -206,6 +206,7 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo
* `loggerAdapter` - The default behavior/transport (File) can be changed by creating an adapter class (see [`LoggerAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Logger/LoggerAdapter.js)).
* `sessionLength` - The length of time in seconds that a session should be valid for. Defaults to 31536000 seconds (1 year).
* `revokeSessionOnPasswordReset` - When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.
* `accountLockout` - Lock account when a malicious user is attempting to determine an account password by trial and error.
##### Logging
@@ -259,7 +260,14 @@ var server = ParseServer({
// Your API key from mailgun.com
apiKey: 'key-mykey',
}
}
},
// account lockout policy setting (OPTIONAL) - defaults to undefined
// if the account lockout policy is set and there are more than `threshold` number of failed login attempts then the `login` api call returns error code `Parse.Error.OBJECT_NOT_FOUND` with error message `Your account is locked due to multiple failed login attempts. Please try again after <duration> minute(s)`. After `duration` minutes of no login attempts, the application will allow the user to try login again.
accountLockout: {
duration: 5, // duration policy setting determines the number of minutes that a locked-out account remains locked out before automatically becoming unlocked. Set it to a value greater than 0 and less than 100000.
threshold: 3, // threshold policy setting determines the number of failed sign-in attempts that will cause a user account to be locked. Set it to an integer value greater than 0 and less than 1000.
},
});
```

View File

@@ -0,0 +1,318 @@
"use strict";
const Config = require("../src/Config");
var loginWithWrongCredentialsShouldFail = function(username, password) {
return new Promise((resolve, reject) => {
Parse.User.logIn(username, password)
.then(user => reject('login should have failed'))
.catch(err => {
if (err.message === 'Invalid username/password.') {
resolve();
} else {
reject(err);
}
});
});
};
var isAccountLockoutError = function(username, password, duration, waitTime) {
return new Promise((resolve, reject) => {
setTimeout(() => {
Parse.User.logIn(username, password)
.then(user => reject('login should have failed'))
.catch(err => {
if (err.message === 'Your account is locked due to multiple failed login attempts. Please try again after ' + duration + ' minute(s)') {
resolve();
} else {
reject(err);
}
});
}, waitTime);
});
};
describe("Account Lockout Policy: ", () => {
it('account should not be locked even after failed login attempts if account lockout policy is not set', done => {
reconfigureServer({
appName: 'unlimited',
publicServerURL: 'http://localhost:1337/1',
})
.then(() => {
var user = new Parse.User();
user.setUsername('username1');
user.setPassword('password');
return user.signUp(null);
})
.then(user => {
return loginWithWrongCredentialsShouldFail('username1', 'incorrect password 1');
})
.then(() => {
return loginWithWrongCredentialsShouldFail('username1', 'incorrect password 2');
})
.then(() => {
return loginWithWrongCredentialsShouldFail('username1', 'incorrect password 3');
})
.then(() => done())
.catch(err => {
fail('allow unlimited failed login attempts failed: ' + JSON.stringify(err));
done();
});
});
it('throw error if duration is set to an invalid number', done => {
reconfigureServer({
appName: 'duration',
accountLockout: {
duration: 'invalid value',
threshold: 5
},
publicServerURL: "https://my.public.server.com/1"
})
.then(() => {
var config = new Config('test');
fail('set duration to an invalid number test failed');
done();
})
.catch(err => {
if (err && err === 'Account lockout duration should be greater than 0 and less than 100000') {
done();
} else {
fail('set duration to an invalid number test failed: ' + JSON.stringify(err));
done();
}
});
});
it('throw error if threshold is set to an invalid number', done => {
reconfigureServer({
appName: 'threshold',
accountLockout: {
duration: 5,
threshold: 'invalid number'
},
publicServerURL: "https://my.public.server.com/1"
})
.then(() => {
var config = new Config('test');
fail('set threshold to an invalid number test failed');
done();
})
.catch(err => {
if (err && err === 'Account lockout threshold should be an integer greater than 0 and less than 1000') {
done();
} else {
fail('set threshold to an invalid number test failed: ' + JSON.stringify(err));
done();
}
});
});
it('throw error if threshold is < 1', done => {
reconfigureServer({
appName: 'threshold',
accountLockout: {
duration: 5,
threshold: 0
},
publicServerURL: "https://my.public.server.com/1"
})
.then(() => {
var config = new Config('test');
fail('threshold value < 1 is invalid test failed');
done();
})
.catch(err => {
if (err && err === 'Account lockout threshold should be an integer greater than 0 and less than 1000') {
done();
} else {
fail('threshold value < 1 is invalid test failed: ' + JSON.stringify(err));
done();
}
});
});
it('throw error if threshold is > 999', done => {
reconfigureServer({
appName: 'threshold',
accountLockout: {
duration: 5,
threshold: 1000
},
publicServerURL: "https://my.public.server.com/1"
})
.then(() => {
var config = new Config('test');
fail('threshold value > 999 is invalid test failed');
done();
})
.catch(err => {
if (err && err === 'Account lockout threshold should be an integer greater than 0 and less than 1000') {
done();
} else {
fail('threshold value > 999 is invalid test failed: ' + JSON.stringify(err));
done();
}
});
});
it('throw error if duration is <= 0', done => {
reconfigureServer({
appName: 'duration',
accountLockout: {
duration: 0,
threshold: 5
},
publicServerURL: "https://my.public.server.com/1"
})
.then(() => {
var config = new Config('test');
fail('duration value < 1 is invalid test failed');
done();
})
.catch(err => {
if (err && err === 'Account lockout duration should be greater than 0 and less than 100000') {
done();
} else {
fail('duration value < 1 is invalid test failed: ' + JSON.stringify(err));
done();
}
});
});
it('throw error if duration is > 99999', done => {
reconfigureServer({
appName: 'duration',
accountLockout: {
duration: 100000,
threshold: 5
},
publicServerURL: "https://my.public.server.com/1"
})
.then(() => {
var config = new Config('test');
fail('duration value > 99999 is invalid test failed');
done();
})
.catch(err => {
if (err && err === 'Account lockout duration should be greater than 0 and less than 100000') {
done();
} else {
fail('duration value > 99999 is invalid test failed: ' + JSON.stringify(err));
done();
}
});
});
it('lock account if failed login attempts are above threshold', done => {
reconfigureServer({
appName: 'lockout threshold',
accountLockout: {
duration: 1,
threshold: 2
},
publicServerURL: "http://localhost:8378/1"
})
.then(() => {
var user = new Parse.User();
user.setUsername("username2");
user.setPassword("failedLoginAttemptsThreshold");
return user.signUp();
})
.then(() => {
return loginWithWrongCredentialsShouldFail('username2', 'wrong password');
})
.then(() => {
return loginWithWrongCredentialsShouldFail('username2', 'wrong password');
})
.then(() => {
return isAccountLockoutError('username2', 'wrong password', 1, 1);
})
.then(() => {
done();
})
.catch(err => {
fail('lock account after failed login attempts test failed: ' + JSON.stringify(err));
done();
});
});
it('lock account for accountPolicy.duration minutes if failed login attempts are above threshold', done => {
reconfigureServer({
appName: 'lockout threshold',
accountLockout: {
duration: 0.05, // 0.05*60 = 3 secs
threshold: 2
},
publicServerURL: "http://localhost:8378/1"
})
.then(() => {
var user = new Parse.User();
user.setUsername("username3");
user.setPassword("failedLoginAttemptsThreshold");
return user.signUp();
})
.then(() => {
return loginWithWrongCredentialsShouldFail('username3', 'wrong password');
})
.then(() => {
return loginWithWrongCredentialsShouldFail('username3', 'wrong password');
})
.then(() => {
return isAccountLockoutError('username3', 'wrong password', 0.05, 1);
})
.then(() => {
// account should still be locked even after 2 seconds.
return isAccountLockoutError('username3', 'wrong password', 0.05, 2000);
})
.then(() => {
done();
})
.catch(err => {
fail('account should be locked for duration mins test failed: ' + JSON.stringify(err));
done();
});
});
it('allow login for locked account after accountPolicy.duration minutes', done => {
reconfigureServer({
appName: 'lockout threshold',
accountLockout: {
duration: 0.05, // 0.05*60 = 3 secs
threshold: 2
},
publicServerURL: "http://localhost:8378/1"
})
.then(() => {
var user = new Parse.User();
user.setUsername("username4");
user.setPassword("correct password");
return user.signUp();
})
.then(() => {
return loginWithWrongCredentialsShouldFail('username4', 'wrong password');
})
.then(() => {
return loginWithWrongCredentialsShouldFail('username4', 'wrong password');
})
.then(() => {
// allow locked user to login after 3 seconds with a valid userid and password
return new Promise((resolve, reject) => {
setTimeout(() => {
Parse.User.logIn('username4', 'correct password')
.then(user => resolve())
.catch(err => reject(err));
}, 3001);
});
})
.then(() => {
done();
})
.catch(err => {
fail('allow login for locked account after accountPolicy.duration minutes test failed: ' + JSON.stringify(err));
done();
});
});
})

200
src/AccountLockout.js Normal file
View File

@@ -0,0 +1,200 @@
// This class handles the Account Lockout Policy settings.
import Config from './Config';
export class AccountLockout {
constructor(user, config) {
this._user = user;
this._config = config;
}
/**
* set _failed_login_count to value
*/
_setFailedLoginCount(value) {
let query = {
username: this._user.username,
};
const updateFields = {
_failed_login_count: value
};
return this._config.database.update('_User', query, updateFields);
}
/**
* check if the _failed_login_count field has been set
*/
_isFailedLoginCountSet() {
return new Promise((resolve, reject) => {
const query = {
username: this._user.username,
_failed_login_count: { $exists: true }
};
this._config.database.find('_User', query)
.then(users => {
if (Array.isArray(users) && users.length > 0) {
resolve(true);
} else {
resolve(false);
}
})
.catch(err => {
reject(err);
});
});
}
/**
* if _failed_login_count is NOT set then set it to 0
* else do nothing
*/
_initFailedLoginCount() {
return new Promise((resolve, reject) => {
this._isFailedLoginCountSet()
.then(failedLoginCountIsSet => {
if (!failedLoginCountIsSet) {
return this._setFailedLoginCount(0);
} else {
return Promise.resolve();
}
})
.then(() => {
resolve();
})
.catch(err => {
reject(err);
});
});
}
/**
* increment _failed_login_count by 1
*/
_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() {
return new Promise((resolve, reject) => {
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))
};
this._config.database.update('_User', query, updateFields)
.then(() => {
resolve();
})
.catch(err => {
if (err && err.code && err.message && err.code === 101 && err.message === 'Object not found.') {
resolve(); // nothing to update so we are good
} else {
reject(err); // unknown error
}
});
});
}
/**
* if _account_lockout_expires_at > current_time and _failed_login_count > threshold
* reject with account locked error
* else
* resolve
*/
_notLocked() {
return new Promise((resolve, reject) => {
const query = {
username: this._user.username,
_account_lockout_expires_at: { $gt: Parse._encode(new Date()) },
_failed_login_count: {$gte: this._config.accountLockout.threshold}
};
this._config.database.find('_User', query)
.then(users => {
if (Array.isArray(users) && users.length > 0) {
reject(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)'));
} else {
resolve();
}
})
.catch(err => {
reject(err);
});
});
}
/**
* set and/or increment _failed_login_count
* if _failed_login_count > threshold
* set the _account_lockout_expires_at to current_time + accountPolicy.duration
* else
* do nothing
*/
_handleFailedLoginAttempt() {
return new Promise((resolve, reject) => {
this._initFailedLoginCount()
.then(() => {
return this._incrementFailedLoginCount();
})
.then(() => {
return this._setLockoutExpiration();
})
.then(() => {
resolve();
})
.catch(err => {
reject(err);
});
});
}
/**
* handle login attempt if the Account Lockout Policy is enabled
*/
handleLoginAttempt(loginSuccessful) {
if (!this._config.accountLockout) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
this._notLocked()
.then(() => {
if (loginSuccessful) {
return this._setFailedLoginCount(0);
} else {
return this._handleFailedLoginAttempt();
}
})
.then(() => {
resolve();
})
.catch(err => {
reject(err);
});
});
}
}
export default AccountLockout;

View File

@@ -57,10 +57,16 @@ const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSc
key = '_email_verify_token_expires_at';
timeField = true;
break;
case '_account_lockout_expires_at':
key = '_account_lockout_expires_at';
timeField = true;
break;
case '_failed_login_count':
key = '_failed_login_count';
break;
case '_rperm':
case '_wperm':
return {key: key, value: restValue};
break;
}
if ((parseFormatSchema.fields[key] && parseFormatSchema.fields[key].type === 'Pointer') || (!parseFormatSchema.fields[key] && restValue && restValue.__type == 'Pointer')) {
@@ -155,6 +161,13 @@ function transformQueryKeyValue(className, key, value, schema) {
}
return {key: '_id', value}
}
case '_account_lockout_expires_at':
if (valueAsDate(value)) {
return {key: '_account_lockout_expires_at', value: valueAsDate(value)}
}
break;
case '_failed_login_count':
return {key, value};
case 'sessionToken': return {key: '_session_token', value}
case '_rperm':
case '_wperm':
@@ -231,6 +244,11 @@ const parseObjectKeyValueToMongoObjectKeyValue = (restKey, restValue, schema) =>
transformedValue = transformTopLevelAtom(restValue);
coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue
return {key: '_email_verify_token_expires_at', value: coercedToDate};
case '_account_lockout_expires_at':
transformedValue = transformTopLevelAtom(restValue);
coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue
return {key: '_account_lockout_expires_at', value: coercedToDate};
case '_failed_login_count':
case '_rperm':
case '_wperm':
case '_email_verify_token':
@@ -730,6 +748,8 @@ const mongoObjectToParseObject = (className, mongoObject, schema) => {
case '_perishable_token':
case '_tombstone':
case '_email_verify_token_expires_at':
case '_account_lockout_expires_at':
case '_failed_login_count':
// Those keys will be deleted if needed in the DB Controller
restObject[key] = mongoObject[key];
break;

View File

@@ -53,7 +53,7 @@ const toPostgresValue = value => {
}
const transformValue = value => {
if (value.__type == 'Pointer') {
if (value.__type === 'Pointer') {
return value.objectId;
}
return value;
@@ -171,7 +171,7 @@ const buildWhereClause = ({ schema, query, index }) => {
if (fieldName.indexOf('.') >= 0) {
let components = fieldName.split('.').map((cmpt, index) => {
if (index == 0) {
if (index === 0) {
return `"${cmpt}"`;
}
return `'${cmpt}'`; 
@@ -431,6 +431,8 @@ export class PostgresStorageAdapter {
if (className === '_User') {
fields._email_verify_token_expires_at = {type: 'Date'};
fields._email_verify_token = {type: 'String'};
fields._account_lockout_expires_at = {type: 'Date'};
fields._failed_login_count = {type: 'Number'};
fields._perishable_token = {type: 'String'};
}
let index = 2;
@@ -439,7 +441,7 @@ export class PostgresStorageAdapter {
let parseType = fields[fieldName];
// Skip when it's a relation
// We'll create the tables later
if (parseType.type == 'Relation') {
if (parseType.type === 'Relation') {
relations.push(fieldName)
return;
}
@@ -641,18 +643,26 @@ export class PostgresStorageAdapter {
columnsArray.push(fieldName);
if (!schema.fields[fieldName] && className === '_User') {
if (fieldName == '_email_verify_token') {
if (fieldName === '_email_verify_token' ||
fieldName === '_failed_login_count' ||
fieldName === '_perishable_token') {
valuesArray.push(object[fieldName]);
}
if (fieldName == '_email_verify_token_expires_at') {
if (fieldName === '_email_verify_token_expires_at') {
if (object[fieldName]) {
valuesArray.push(object[fieldName].iso);
} else {
valuesArray.push(null);
}
}
if (fieldName == '_perishable_token') {
if (fieldName === '_account_lockout_expires_at') {
if (object[fieldName]) {
valuesArray.push(object[fieldName].iso);
} else {
valuesArray.push(null);
}
}
return;
}
@@ -690,7 +700,6 @@ export class PostgresStorageAdapter {
break;
default:
throw `Type ${schema.fields[fieldName].type} not supported yet`;
break;
}
});
@@ -738,7 +747,7 @@ export class PostgresStorageAdapter {
let index = 2;
let where = buildWhereClause({ schema, index, query })
values.push(...where.values);
if (Object.keys(query).length == 0) {
if (Object.keys(query).length === 0) {
where.pattern = 'TRUE';
}
let qs = `WITH deleted AS (DELETE FROM $1:name WHERE ${where.pattern} RETURNING *) SELECT count(*) FROM deleted`;
@@ -870,13 +879,13 @@ export class PostgresStorageAdapter {
index += 2;
} else if (typeof fieldValue === 'object'
&& schema.fields[fieldName]
&& schema.fields[fieldName].type == 'Object') {
&& schema.fields[fieldName].type === 'Object') {
updatePatterns.push(`$${index}:name = $${index + 1}`);
values.push(fieldName, fieldValue);
index += 2;
} else if (Array.isArray(fieldValue)
&& schema.fields[fieldName]
&& schema.fields[fieldName].type == 'Array') {
&& schema.fields[fieldName].type === 'Array') {
let expectedType = parseTypeToPostgresType(schema.fields[fieldName]);
if (expectedType === 'text[]') {
updatePatterns.push(`$${index}:name = $${index + 1}::text[]`);
@@ -905,7 +914,7 @@ export class PostgresStorageAdapter {
let createValue = Object.assign({}, query, update);
return this.createObject(className, schema, createValue).catch((err) => {
// ignore duplicate value errors as it's upsert
if (err.code == Parse.Error.DUPLICATE_VALUE) {
if (err.code === Parse.Error.DUPLICATE_VALUE) {
return this.findOneAndUpdate(className, schema, query, update);
}
throw err;
@@ -992,6 +1001,9 @@ export class PostgresStorageAdapter {
if (object._email_verify_token_expires_at) {
object._email_verify_token_expires_at = { __type: 'Date', iso: object._email_verify_token_expires_at.toISOString() };
}
if (object._account_lockout_expires_at) {
object._account_lockout_expires_at = { __type: 'Date', iso: object._account_lockout_expires_at.toISOString() };
}
for (let fieldName in object) {
if (object[fieldName] === null) {
@@ -1052,7 +1064,7 @@ export class PostgresStorageAdapter {
debug('performInitialization');
let promises = VolatileClassesSchemas.map((schema) => {
return this.createTable(schema.className, schema).catch((err) =>{
if (err.code === PostgresDuplicateRelationError || err.code == Parse.Error.INVALID_CLASS_NAME) {
if (err.code === PostgresDuplicateRelationError || err.code === Parse.Error.INVALID_CLASS_NAME) {
return Promise.resolve();
}
throw err;

View File

@@ -48,6 +48,7 @@ export class Config {
this.verifyUserEmails = cacheInfo.verifyUserEmails;
this.preventLoginWithUnverifiedEmail = cacheInfo.preventLoginWithUnverifiedEmail;
this.emailVerifyTokenValidityDuration = cacheInfo.emailVerifyTokenValidityDuration;
this.accountLockout = cacheInfo.accountLockout;
this.appName = cacheInfo.appName;
this.analyticsController = cacheInfo.analyticsController;
@@ -76,13 +77,16 @@ export class Config {
revokeSessionOnPasswordReset,
expireInactiveSessions,
sessionLength,
emailVerifyTokenValidityDuration
emailVerifyTokenValidityDuration,
accountLockout
}) {
const emailAdapter = userController.adapter;
if (verifyUserEmails) {
this.validateEmailConfiguration({emailAdapter, appName, publicServerURL, emailVerifyTokenValidityDuration});
}
this.validateAccountLockoutPolicy(accountLockout);
if (typeof revokeSessionOnPasswordReset !== 'boolean') {
throw 'revokeSessionOnPasswordReset must be a boolean value';
}
@@ -96,7 +100,19 @@ export class Config {
this.validateSessionConfiguration(sessionLength, expireInactiveSessions);
}
static validateEmailConfiguration({emailAdapter, appName, publicServerURL, emailVerifyTokenValidityDuration}) {
static validateAccountLockoutPolicy(accountLockout) {
if (accountLockout) {
if (typeof accountLockout.duration !== 'number' || accountLockout.duration <= 0 || accountLockout.duration > 99999) {
throw 'Account lockout duration should be greater than 0 and less than 100000';
}
if (!Number.isInteger(accountLockout.threshold) || accountLockout.threshold < 1 || accountLockout.threshold > 999) {
throw 'Account lockout threshold should be an integer greater than 0 and less than 1000';
}
}
}
static validateEmailConfiguration({emailAdapter, appName, publicServerURL, emailVerifyTokenValidityDuration}) {
if (!emailAdapter) {
throw 'An emailAdapter is required for e-mail verification and password resets.';
}

View File

@@ -43,7 +43,7 @@ const transformObjectACL = ({ ACL, ...result }) => {
return result;
}
const specialQuerykeys = ['$and', '$or', '_rperm', '_wperm', '_perishable_token', '_email_verify_token', '_email_verify_token_expires_at'];
const specialQuerykeys = ['$and', '$or', '_rperm', '_wperm', '_perishable_token', '_email_verify_token', '_email_verify_token_expires_at', '_account_lockout_expires_at', '_failed_login_count'];
const validateQuery = query => {
if (query.ACL) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.');
@@ -166,6 +166,8 @@ const filterSensitiveData = (isMaster, aclGroup, className, object) => {
delete object._perishable_token;
delete object._tombstone;
delete object._email_verify_token_expires_at;
delete object._failed_login_count;
delete object._account_lockout_expires_at;
if ((aclGroup.indexOf(object.objectId) > -1)) {
return object;
@@ -182,7 +184,7 @@ const filterSensitiveData = (isMaster, aclGroup, className, object) => {
// acl: a list of strings. If the object to be updated has an ACL,
// one of the provided strings must provide the caller with
// write permissions.
const specialKeysForUpdate = ['_hashed_password', '_perishable_token', '_email_verify_token', '_email_verify_token_expires_at'];
const specialKeysForUpdate = ['_hashed_password', '_perishable_token', '_email_verify_token', '_email_verify_token_expires_at', '_account_lockout_expires_at', '_failed_login_count'];
DatabaseController.prototype.update = function(className, query, update, {
acl,
many,

View File

@@ -124,6 +124,7 @@ class ParseServer {
verifyUserEmails = defaults.verifyUserEmails,
preventLoginWithUnverifiedEmail = defaults.preventLoginWithUnverifiedEmail,
emailVerifyTokenValidityDuration,
accountLockout,
cacheAdapter,
emailAdapter,
publicServerURL,
@@ -211,6 +212,7 @@ class ParseServer {
verifyUserEmails: verifyUserEmails,
preventLoginWithUnverifiedEmail: preventLoginWithUnverifiedEmail,
emailVerifyTokenValidityDuration: emailVerifyTokenValidityDuration,
accountLockout: accountLockout,
allowClientClassCreation: allowClientClassCreation,
authDataManager: authDataManager(oauth, enableAnonymousUsers),
appName: appName,

View File

@@ -2,6 +2,7 @@
import deepcopy from 'deepcopy';
import Config from '../Config';
import AccountLockout from '../AccountLockout';
import ClassesRouter from './ClassesRouter';
import PromiseRouter from '../PromiseRouter';
import rest from '../rest';
@@ -80,6 +81,8 @@ export class UsersRouter extends ClassesRouter {
}
let user;
let isValidPassword = false;
return req.config.database.find('_User', { username: req.body.username })
.then((results) => {
if (!results.length) {
@@ -90,11 +93,15 @@ export class UsersRouter extends ClassesRouter {
if (req.config.verifyUserEmails && req.config.preventLoginWithUnverifiedEmail && !user.emailVerified) {
throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.');
}
return passwordCrypto.compare(req.body.password, user.password);
}).then((correct) => {
if (!correct) {
})
.then((correct) => {
isValidPassword = correct;
let accountLockoutPolicy = new AccountLockout(user, req.config);
return accountLockoutPolicy.handleLoginAttempt(isValidPassword);
})
.then(() => {
if (!isValidPassword) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.');
}

View File

@@ -163,6 +163,11 @@ export default {
help: "Email verification token validity duration",
action: numberParser("emailVerifyTokenValidityDuration")
},
"accountLockout": {
env: "PARSE_SERVER_ACCOUNT_LOCKOUT",
help: "account lockout policy for failed login attempts",
action: objectParser
},
"appName": {
env: "PARSE_SERVER_APP_NAME",
help: "Sets the app name"