Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for optional Password Policy #3032

Merged
merged 26 commits into from
Nov 17, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
05cc0d9
Introducing passwordPolicy with resetTokenValidityDuration
bhaskaryasa Nov 7, 2016
bbd5d37
validator added to passwordPolicy
bhaskaryasa Nov 8, 2016
96920bd
Add some unit tests for passwordPolicy.validator
bhaskaryasa Nov 9, 2016
e7307be
Add unit test for reset password failure for non-conformance
bhaskaryasa Nov 9, 2016
3206b34
Update README.md for passwordPolicy
bhaskaryasa Nov 9, 2016
052d6eb
Added code to handle Parse.Error from rest.update in UserController.u…
bhaskaryasa Nov 10, 2016
6bbdbbe
Merge branch 'password-policy' of https://github.com/bhaskaryasa/pars…
bhaskaryasa Nov 10, 2016
838d7cb
Added optional setting to disallow username in password
bhaskaryasa Nov 10, 2016
54bf4d1
fdescribe -> describe
bhaskaryasa Nov 10, 2016
59fb9d7
updated PasswordPolicy.spec.js to use request-promise
bhaskaryasa Nov 11, 2016
e82e1bf
passwordPolicy.validator split into two separate options - RegExp and…
bhaskaryasa Nov 11, 2016
2f0fcd7
Introducing passwordPolicy with resetTokenValidityDuration
bhaskaryasa Nov 7, 2016
f41747b
validator added to passwordPolicy
bhaskaryasa Nov 8, 2016
3cd904e
Add some unit tests for passwordPolicy.validator
bhaskaryasa Nov 9, 2016
f385f6a
Add unit test for reset password failure for non-conformance
bhaskaryasa Nov 9, 2016
5b868f6
Update README.md for passwordPolicy
bhaskaryasa Nov 9, 2016
1c1a515
Added code to handle Parse.Error from rest.update in UserController.u…
bhaskaryasa Nov 10, 2016
bd1673d
Added optional setting to disallow username in password
bhaskaryasa Nov 10, 2016
45ee8b5
fdescribe -> describe
bhaskaryasa Nov 10, 2016
f7ce2c7
updated PasswordPolicy.spec.js to use request-promise
bhaskaryasa Nov 11, 2016
838eb27
passwordPolicy.validator split into two separate options - RegExp and…
bhaskaryasa Nov 11, 2016
a9f55f8
fixed some typos
bhaskaryasa Nov 11, 2016
7401000
expect username parameter in redirect to password_reset_success
bhaskaryasa Nov 11, 2016
4ce59aa
pull from origin
bhaskaryasa Nov 11, 2016
9ed141c
Fix postgres issue for _perishable_token_expires_at
bhaskaryasa Nov 12, 2016
72a0670
fix for _perishable_token_expires_at
bhaskaryasa Nov 12, 2016
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ var server = ParseServer({
// optional setting to enforce strong passwords
// can be a RegExp/String representing pattern to enforce or a function that return a bool
validator: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})/, // enforce password with at least 8 char with at least 1 lower case, 1 upper case and 1 digit
doNotAllowUsername: true, // optional setting to disallow username in passwords
//optional setting to set a validity duration for password reset links (in seconds)
resetTokenValidityDuration: 24*60*60, // expire after 24 hours
}
Expand Down
250 changes: 249 additions & 1 deletion spec/PasswordPolicy.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
const request = require('request');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bhaskaryasa
Can you please userequest-promise or request-promise-native instead of just request?

const Config = require('../src/Config');

describe("Password Policy: ", () => {
fdescribe("Password Policy: ", () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooops

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sorry!! corrected that.


it('should show the invalid link page if the user clicks on the password reset link after the token expires', done => {
const user = new Parse.User();
Expand Down Expand Up @@ -446,4 +446,252 @@ describe("Password Policy: ", () => {
});
});

it('should fail if passwordPolicy.doNotAllowUsername is not a boolean value', (done) => {
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
doNotAllowUsername: 'no'
},
publicServerURL: "http://localhost:8378/1"
})
.then(() => {
fail('passwordPolicy.doNotAllowUsername type test failed');
done();
})
.catch(err => {
expect(err).toEqual('passwordPolicy.doNotAllowUsername must be a boolean value.');
done();
});
});

it('signup should fail if password contains the username and is not allowed by policy', (done) => {
const user = new Parse.User();
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
validator: /[0-9]+/,
doNotAllowUsername: true
},
publicServerURL: "http://localhost:8378/1"
}).then(() => {
user.setUsername("user1");
user.setPassword("@user11");
user.set('email', '[email protected]');
user.signUp().then(() => {
fail('Should have failed as password contains username.');
done();
}, (error) => {
expect(error.code).toEqual(142);
done();
});
})
});

it('signup should succeed if password does not contain the username and is not allowed by policy', (done) => {
const user = new Parse.User();
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
doNotAllowUsername: true
},
publicServerURL: "http://localhost:8378/1"
}).then(() => {
user.setUsername("user1");
user.setPassword("r@nd0m");
user.set('email', '[email protected]');
user.signUp().then(() => {
done();
}, (error) => {
fail('Should have succeeded as password does not contain username.');
done();
});
})
});

it('signup should succeed if password contains the username and it is allowed by policy', (done) => {
const user = new Parse.User();
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
validator: /[0-9]+/
},
publicServerURL: "http://localhost:8378/1"
}).then(() => {
user.setUsername("user1");
user.setPassword("user1");
user.set('email', '[email protected]');
user.signUp().then(() => {
done();
}, (error) => {
fail('Should have succeeded as policy allows username in password.');
done();
});
})
});

it('should fail to reset password if the new password contains username and not allowed by password policy', done => {
var user = new Parse.User();
var emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: options => {
request.get(options.link, {
followRedirect: false,
}, (error, response, body) => {
if (error) {
jfail(error);
fail("Failed to get the reset link");
return;
}
expect(response.statusCode).toEqual(302);
var re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/;
var match = response.body.match(re);
if (!match) {
fail("should have a token");
done();
return;
}
var token = match[1];

request.post({
url: "http://localhost:8378/1/apps/test/request_password_reset",
body: `new_password=xuser12&token=${token}&username=user1`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
followRedirect: false,
}, (error, response, body) => {
if (error) {
jfail(error);
fail("Failed to POST request password reset");
return;
}
expect(response.statusCode).toEqual(302);
expect(response.body).toEqual(`Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=Password%20does%20not%20confirm%20to%20the%20Password%20Policy.&app=passwordPolicy`);

Parse.User.logIn("user1", "r@nd0m").then(function (user) {
done();
}, (err) => {
jfail(err);
fail("should login with old password");
done();
});

});
});
},
sendMail: () => {
}
}
reconfigureServer({
appName: 'passwordPolicy',
verifyUserEmails: false,
emailAdapter: emailAdapter,
passwordPolicy: {
doNotAllowUsername: true
},
publicServerURL: "http://localhost:8378/1"
})
.then(() => {
user.setUsername("user1");
user.setPassword("r@nd0m");
user.set('email', '[email protected]');
user.signUp().then(() => {
Parse.User.requestPasswordReset('[email protected]', {
error: (err) => {
jfail(err);
fail("Reset password request should not fail");
done();
}
});
}, error => {
jfail(error);
fail("signUp should not fail");
done();
});
});
});

it('should reset password even if the new password contains user name while the policy allows', done => {
var user = new Parse.User();
var emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: options => {
request.get(options.link, {
followRedirect: false,
}, (error, response, body) => {
if (error) {
jfail(error);
fail("Failed to get the reset link");
return;
}
expect(response.statusCode).toEqual(302);
var re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/;
var match = response.body.match(re);
if (!match) {
fail("should have a token");
done();
return;
}
var token = match[1];

request.post({
url: "http://localhost:8378/1/apps/test/request_password_reset",
body: `new_password=uuser11&token=${token}&username=user1`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
followRedirect: false,
}, (error, response, body) => {
if (error) {
jfail(error);
fail("Failed to POST request password reset");
return;
}
expect(response.statusCode).toEqual(302);
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html');

Parse.User.logIn("user1", "uuser11").then(function (user) {
done();
}, (err) => {
jfail(err);
fail("should login with new password");
done();
});

});
});
},
sendMail: () => {
}
}
reconfigureServer({
appName: 'passwordPolicy',
verifyUserEmails: false,
emailAdapter: emailAdapter,
passwordPolicy: {
validator: /[0-9]+/,
doNotAllowUsername: false
},
publicServerURL: "http://localhost:8378/1"
})
.then(() => {
user.setUsername("user1");
user.setPassword("has 1 digit");
user.set('email', '[email protected]');
user.signUp().then(() => {
Parse.User.requestPasswordReset('[email protected]', {
error: (err) => {
jfail(err);
fail("Reset password request should not fail");
done();
}
});
}, error => {
jfail(error);
fail("signUp should not fail");
done();
});
});
});

})
4 changes: 4 additions & 0 deletions src/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ export class Config {
if(passwordPolicy.validator && typeof passwordPolicy.validator !== 'string' && !(passwordPolicy.validator instanceof RegExp) && typeof passwordPolicy.validator !== 'function' ) {
throw 'passwordPolicy.validator must be a RegExp, a string or a function.';
}

if(passwordPolicy.doNotAllowUsername && typeof passwordPolicy.doNotAllowUsername !== 'boolean') {
throw 'passwordPolicy.doNotAllowUsername must be a boolean value.';
}
}
}

Expand Down
41 changes: 35 additions & 6 deletions src/RestWrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -369,18 +369,47 @@ RestWrite.prototype.transformUser = function() {
return;
}

let defer = Promise.resolve();

// check if the password confirms to the defined password policy if configured
if (this.config.passwordPolicy && this.config.passwordPolicy.validator && !this.config.passwordPolicy.validator(this.data.password)) {
return Promise.reject(new Parse.Error(Parse.Error.VALIDATION_ERROR, 'Password does not confirm to the Password Policy.'))
if (this.config.passwordPolicy) {
const policyError = 'Password does not confirm to the Password Policy.';

// check whether the password confirms to the policy
if (this.config.passwordPolicy.validator && !this.config.passwordPolicy.validator(this.data.password)) {
return Promise.reject(new Parse.Error(Parse.Error.VALIDATION_ERROR, policyError))
}

// check whether password contain username
if (this.config.passwordPolicy.doNotAllowUsername === true) {
if (this.data.username) { // username is not passed during password reset
if (this.data.password.indexOf(this.data.username) >= 0)
return Promise.reject(new Parse.Error(Parse.Error.VALIDATION_ERROR, policyError));

} else { // retrieve the User object using objectId during password reset
defer = this.config.database.find('_User', {objectId: this.objectId()})
.then(results => {
if (results.length != 1) {
throw undefined;
}
if (this.data.password.indexOf(results[0].username) >= 0)
return Promise.reject(new Parse.Error(Parse.Error.VALIDATION_ERROR, policyError));
return Promise.resolve();
});
}
}
}

if (this.query && !this.auth.isMaster ) {
if (this.query && !this.auth.isMaster) {
this.storage['clearSessions'] = true;
this.storage['generateNewSession'] = true;
}
return passwordCrypto.hash(this.data.password).then((hashedPassword) => {
this.data._hashed_password = hashedPassword;
delete this.data.password;

return defer.then(() => {
return passwordCrypto.hash(this.data.password).then((hashedPassword) => {
this.data._hashed_password = hashedPassword;
delete this.data.password;
});
});

}).then(() => {
Expand Down