Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions spec/AccountLockoutPolicy.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,31 @@ describe('Account Lockout Policy: ', () => {
});
});

it('allows a fresh set of attempts after the lockout duration expires', async () => {
await reconfigureServer({
appName: 'lockout reset',
accountLockout: { duration: 0.05 /* 3s */, threshold: 2 },
publicServerURL: 'http://localhost:8378/1',
});
const user = new Parse.User();
user.setUsername('username_reset');
user.setPassword('correct password');
await user.signUp();

// Trip the lockout (threshold = 2 failed attempts).
await loginWithWrongCredentialsShouldFail('username_reset', 'wrong password');
await loginWithWrongCredentialsShouldFail('username_reset', 'wrong password');

// Wait out the 3s lockout.
await new Promise(resolve => setTimeout(resolve, 3100));

// One post-expiry failure must NOT re-lock (bug: it throws the lockout error here)...
await loginWithWrongCredentialsShouldFail('username_reset', 'wrong password');
// ...and a correct login within the fresh window must still succeed.
const loggedIn = await Parse.User.logIn('username_reset', 'correct password');
expect(loggedIn.getUsername()).toBe('username_reset');
});

it('should enforce lockout threshold under concurrent failed login attempts', async () => {
const threshold = 3;
await reconfigureServer({
Expand Down
49 changes: 40 additions & 9 deletions src/AccountLockout.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,21 +120,52 @@ export class AccountLockout {
}

/**
* handle login attempt if the Account Lockout Policy is enabled
* 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.
*/
handleLoginAttempt(loginSuccessful) {
if (!this._config.accountLockout) {
return Promise.resolve();
}
return this._notLocked().then(() => {
if (loginSuccessful) {
return this._setFailedLoginCount(0);
_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 {
return this._handleFailedLoginAttempt();
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.
*/
Expand Down
Loading