Add check on password complexity by using a backend call without divulging the list being checked against.

This commit is contained in:
Colby Gutierrez-Kraybill 2022-09-22 00:23:59 -04:00
parent e21a77b112
commit 191cfb1d57
3 changed files with 83 additions and 19 deletions

View file

@ -24,6 +24,7 @@ class UsernameStep extends React.Component {
'handleSetUsernameRef', 'handleSetUsernameRef',
'handleValidSubmit', 'handleValidSubmit',
'validatePasswordIfPresent', 'validatePasswordIfPresent',
'validatePasswordRemotelyWithCache',
'validatePasswordConfirmIfPresent', 'validatePasswordConfirmIfPresent',
'validateUsernameIfPresent', 'validateUsernameIfPresent',
'validateUsernameRemotelyWithCache', 'validateUsernameRemotelyWithCache',
@ -32,9 +33,10 @@ class UsernameStep extends React.Component {
this.state = { this.state = {
focused: null focused: null
}; };
// simple object to memoize remote requests for usernames. // memoize remote requests for username check and password weakness
// keeps us from submitting multiple requests for same data. // keeps us from submitting multiple requests for same data.
this.usernameRemoteCache = Object.create(null); this.usernameRemoteCache = Object.create(null);
this.passwordRemoteCache = Object.create(null);
} }
componentDidMount () { componentDidMount () {
// Send info to analytics when we aren't on the standalone page. // Send info to analytics when we aren't on the standalone page.
@ -92,12 +94,37 @@ class UsernameStep extends React.Component {
} }
); );
} }
// memoize remote requests for weak password check
validatePasswordRemotelyWithCache (password) {
if (typeof this.passwordRemoteCache[password] === 'object') {
return Promise.resolve(this.passwordRemoteCache[password]);
}
// password is not in our cache
return validate.validatePasswordRemotely(password).then(
remoteResult => {
// cache result, if it successfully heard back from server
if (remoteResult.requestSucceeded) {
this.passwordRemoteCache[password] = remoteResult;
}
return remoteResult;
}
);
}
validatePasswordIfPresent (password, username) { validatePasswordIfPresent (password, username) {
if (!password) return null; // skip validation if password is blank; null indicates valid if (!password) return null; // skip validation if password is blank; null indicates valid
const localResult = validate.validatePassword(password, username); // if password is not blank, run both local and remote validations
if (localResult.valid) return null; const localResult = validate.validatePasswordLocally(password, username);
return this.validatePasswordRemotelyWithCache(password).then(
remoteResult => {
if (localResult.valid === false) { // defer to local check first
return this.props.intl.formatMessage({id: localResult.errMsgId}); return this.props.intl.formatMessage({id: localResult.errMsgId});
} else if (remoteResult.valid === false) {
return this.props.intl.formatMessage({id: remoteResult.errMsgId});
} }
return null;
} // remoteResult
); // validatePasswordRemotelyWithCache
} // validatePasswordIfPresent
validatePasswordConfirmIfPresent (password, passwordConfirm) { validatePasswordConfirmIfPresent (password, passwordConfirm) {
if (!passwordConfirm) return null; // allow blank password if not submitting yet if (!passwordConfirm) return null; // allow blank password if not submitting yet
const localResult = validate.validatePasswordConfirm(password, passwordConfirm); const localResult = validate.validatePasswordConfirm(password, passwordConfirm);
@ -114,7 +141,7 @@ class UsernameStep extends React.Component {
if (!usernameResult.valid) { if (!usernameResult.valid) {
errors.username = this.props.intl.formatMessage({id: usernameResult.errMsgId}); errors.username = this.props.intl.formatMessage({id: usernameResult.errMsgId});
} }
const passwordResult = validate.validatePassword(values.password, values.username); const passwordResult = validate.validatePasswordLocally(values.password, values.username);
if (!passwordResult.valid) { if (!passwordResult.valid) {
errors.password = this.props.intl.formatMessage({id: passwordResult.errMsgId}); errors.password = this.props.intl.formatMessage({id: passwordResult.errMsgId});
} }

View file

@ -54,7 +54,7 @@ module.exports.validateUsernameRemotely = username => (
* @param {string} username username value to compare * @param {string} username username value to compare
* @return {object} {valid: boolean, errMsgId: string} * @return {object} {valid: boolean, errMsgId: string}
*/ */
module.exports.validatePassword = (password, username) => { module.exports.validatePasswordLocally = (password, username) => {
if (!password) { if (!password) {
return {valid: false, errMsgId: 'general.required'}; return {valid: false, errMsgId: 'general.required'};
// Using Array.from(string).length, instead of string.length, improves unicode // Using Array.from(string).length, instead of string.length, improves unicode
@ -67,7 +67,7 @@ module.exports.validatePassword = (password, username) => {
// https://stackoverflow.com/a/54370584/2308190 // https://stackoverflow.com/a/54370584/2308190
} else if (Array.from(password).length < 6) { } else if (Array.from(password).length < 6) {
return {valid: false, errMsgId: 'registration.validationPasswordLength'}; return {valid: false, errMsgId: 'registration.validationPasswordLength'};
} else if (password === 'password') { } else if (password.toLowerCase() === 'password') {
return {valid: false, errMsgId: 'registration.validationPasswordNotEquals'}; return {valid: false, errMsgId: 'registration.validationPasswordNotEquals'};
} else if (username && password === username) { } else if (username && password === username) {
return {valid: false, errMsgId: 'registration.validationPasswordNotUsername'}; return {valid: false, errMsgId: 'registration.validationPasswordNotUsername'};
@ -75,6 +75,43 @@ module.exports.validatePassword = (password, username) => {
return {valid: true}; return {valid: true};
}; };
module.exports.validatePasswordRemotely = password => (
new Promise(resolve => {
api({
method: 'POST',
uri: `/accounts/checkpassword`,
json: {
password: `${password}`
}
}, (err, body, res) => {
if (err || res.statusCode !== 200) {
return resolve({
requestSucceeded: false,
valid: false,
errMsgId: 'general.error'
});
}
let msg = '';
if (body && body.msg) msg = body.msg;
else if (body && body[0]) msg = body[0].msg;
switch (msg) {
case 'valid password':
return resolve({
requestSucceeded: true,
valid: true
});
case 'valid password':
default:
return resolve({
requestSucceeded: true,
valid: false,
errMsgId: 'registration.validationPasswordNotEquals'
});
}; // switch
}); // api
}) // promise
);
module.exports.validatePasswordConfirm = (password, passwordConfirm) => { module.exports.validatePasswordConfirm = (password, passwordConfirm) => {
if (!passwordConfirm) { if (!passwordConfirm) {
return {valid: false, errMsgId: 'general.required'}; return {valid: false, errMsgId: 'general.required'};

View file

@ -55,38 +55,38 @@ describe('unit test lib/validate.js', () => {
test('validate password existence', () => { test('validate password existence', () => {
let response; let response;
expect(typeof validate.validatePassword).toBe('function'); expect(typeof validate.validatePassword).toBe('function');
response = validate.validatePassword('abcdef'); response = validate.validatePasswordLocally('abcdef');
expect(response).toEqual({valid: true}); expect(response).toEqual({valid: true});
response = validate.validatePassword(''); response = validate.validatePasswordLocally('');
expect(response).toEqual({valid: false, errMsgId: 'general.required'}); expect(response).toEqual({valid: false, errMsgId: 'general.required'});
}); });
test('validate password length', () => { test('validate password length', () => {
let response; let response;
response = validate.validatePassword('abcdefghijklmnopqrst'); response = validate.validatePasswordLocally('abcdefghijklmnopqrst');
expect(response).toEqual({valid: true}); expect(response).toEqual({valid: true});
response = validate.validatePassword('abcde'); response = validate.validatePasswordLocally('abcde');
expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordLength'}); expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordLength'});
response = validate.validatePassword('😺'); response = validate.validatePasswordLocally('😺');
expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordLength'}); expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordLength'});
response = validate.validatePassword('😺🦆🐝'); response = validate.validatePasswordLocally('😺🦆🐝');
expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordLength'}); expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordLength'});
response = validate.validatePassword('😺🦆🐝🐮🐠'); response = validate.validatePasswordLocally('😺🦆🐝🐮🐠');
expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordLength'}); expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordLength'});
response = validate.validatePassword('😺🦆🐝🐮🐠🐻'); response = validate.validatePasswordLocally('😺🦆🐝🐮🐠🐻');
expect(response).toEqual({valid: true}); expect(response).toEqual({valid: true});
}); });
test('validate password cannot be "password"', () => { test('validate password cannot be "password"', () => {
const response = validate.validatePassword('password'); const response = validate.validatePasswordLocally('password');
expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordNotEquals'}); expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordNotEquals'});
}); });
test('validate password cannot be same as username', () => { test('validate password cannot be same as username', () => {
let response; let response;
response = validate.validatePassword('abcdefg', 'abcdefg'); response = validate.validatePasswordLocally('abcdefg', 'abcdefg');
expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordNotUsername'}); expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordNotUsername'});
response = validate.validatePassword('abcdefg', 'abcdefG'); response = validate.validatePasswordLocally('abcdefg', 'abcdefG');
expect(response).toEqual({valid: true}); expect(response).toEqual({valid: true});
}); });