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

add isStrongPassword method #1348

Merged
merged 23 commits into from
Nov 19, 2020
Merged
Show file tree
Hide file tree
Changes from 19 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ Validator | Description
**isSurrogatePair(str)** | check if the string contains any surrogate pairs chars.
**isUppercase(str)** | check if the string is uppercase.
**isSlug** | Check if the string is of type slug. `Options` allow a single hyphen between string. e.g. [`cn-cn`, `cn-c-c`]
**isStrongPassword(str, requirementOptions?, scoringOptions?)** | Check if a password is strong or not. Allows for custom requirements or scoring rules. If `returnScore` is true, then the function returns an integer score for the password rather than a boolean.<br/>Default options: <br/>`{ minLength: 8, minLowercase: 1, minUppercase: 1, minNumbers: 1, minSymbols: 1, returnScore: false, pointsPerUnique: 1, pointsPerRepeat: 0.5, pointsForContainingLower: 10, pointsForContainingUpper: 10, pointsForContainingNumber: 10, pointsForContainingSymbol: 10 }`
Copy link
Member

Choose a reason for hiding this comment

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

From my look of the eye, I thought, I needed to pass 3 parameters, though 2 are optional.
But, from function arguments call, it takes str & option.
I expected something like below...

isStrongPassword(str [, options])

It is not a priority but my suggestion for consistency and easy readability/usage.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Missed this, thanks!

**isTaxID(str, locale)** | Check if the given value is a valid Tax Identification Number. Default locale is `en-US`
**isURL(str [, options])** | check if the string is an URL.<br/><br/>`options` is an object which defaults to `{ protocols: ['http','https','ftp'], require_tld: true, require_protocol: false, require_host: true, require_valid_protocol: true, allow_underscores: false, host_whitelist: false, host_blacklist: false, allow_trailing_dot: false, allow_protocol_relative_urls: false, disallow_auth: false }`.<br/><br/>require_protocol - if set as true isURL will return false if protocol is not present in the URL.<br/>require_valid_protocol - isURL will check if the URL's protocol is present in the protocols option.<br/>protocols - valid protocols can be modified with this option.<br/>require_host - if set as false isURL will not check if host is present in the URL.<br/>allow_protocol_relative_urls - if set as true protocol relative URLs will be allowed.
**isUUID(str [, version])** | check if the string is a UUID (version 3, 4 or 5).
Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ import isWhitelisted from './lib/isWhitelisted';
import normalizeEmail from './lib/normalizeEmail';

import isSlug from './lib/isSlug';
import isStrongPassword from './lib/isStrongPassword';

const version = '13.1.1';

Expand Down Expand Up @@ -211,6 +212,7 @@ const validator = {
normalizeEmail,
toString,
isSlug,
isStrongPassword,
isTaxID,
isDate,
};
Expand Down
96 changes: 96 additions & 0 deletions src/lib/isStrongPassword.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import merge from './util/merge';
import assertString from './util/assertString';

const upperCaseRegex = /^[A-Z]$/;
const lowerCaseRegex = /^[a-z]$/;
const numberRegex = /^[0-9]$/;
const symbolRegex = /^[-#!$%^&*()_+|~=`{}\[\]:";'<>?,.\/ ]$/;

const defaultOptions = {
minLength: 8,
minLowercase: 1,
minUppercase: 1,
minNumbers: 1,
minSymbols: 1,
returnScore: false,
pointsPerUnique: 1,
pointsPerRepeat: 0.5,
pointsForContainingLower: 10,
pointsForContainingUpper: 10,
pointsForContainingNumber: 10,
pointsForContainingSymbol: 10,
};

/* Counts number of occurances of each char in a string
* could be moved to util/ ?
*/
function countChars(str) {
let result = {};
Array.from(str).forEach((char) => {
let curVal = result[char];
if (curVal) {
result[char] += 1;
} else {
result[char] = 1;
}
});
return result;
}

/* Return information about a password */
function analyzePassword(password) {
let charMap = countChars(password);
let analysis = {
length: password.length,
uniqueChars: Object.keys(charMap).length,
uppercaseCount: 0,
lowercaseCount: 0,
numberCount: 0,
symbolCount: 0,
};
Object.keys(charMap).forEach((char) => {
if (upperCaseRegex.test(char)) {
analysis.uppercaseCount += charMap[char];
} else if (lowerCaseRegex.test(char)) {
analysis.lowercaseCount += charMap[char];
} else if (numberRegex.test(char)) {
analysis.numberCount += charMap[char];
} else if (symbolRegex.test(char)) {
analysis.symbolCount += charMap[char];
}
});
return analysis;
}

function scorePassword(analysis, scoringOptions) {
let points = 0;
points += analysis.uniqueChars * scoringOptions.pointsPerUnique;
points += (analysis.length - analysis.uniqueChars) * scoringOptions.pointsPerRepeat;
if (analysis.lowercaseCount > 0) {
points += scoringOptions.pointsForContainingLower;
}
if (analysis.uppercaseCount > 0) {
points += scoringOptions.pointsForContainingUpper;
}
if (analysis.numberCount > 0) {
points += scoringOptions.pointsForContainingNumber;
}
if (analysis.symbolCount > 0) {
points += scoringOptions.pointsForContainingSymbol;
}
return points;
}

export default function isStrongPassword(str, options = null) {
assertString(str);
const analysis = analyzePassword(str);
options = merge(options || {}, defaultOptions);
if (options.returnScore) {
return scorePassword(analysis, options);
}
return analysis.length >= options.minLength
&& analysis.lowercaseCount >= options.minLowercase
&& analysis.uppercaseCount >= options.minUppercase
&& analysis.numberCount >= options.minNumbers
&& analysis.symbolCount >= options.minSymbols;
}
22 changes: 22 additions & 0 deletions test/validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -8561,6 +8561,28 @@ describe('Validators', () => {
});
});

it('should validate strong passwords', () => {
test({
validator: 'isStrongPassword',
valid: [
'%2%k{7BsL"M%Kd6e',
'EXAMPLE of very long_password123!',
'mxH_+2vs&54_+H3P',
'+&DxJ=X7-4L8jRCD',
'etV*p%Nr6w&H%FeF',
Copy link
Member

Choose a reason for hiding this comment

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

There is a need for tests that pass returnScore as an option

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added the tests for this case in the sanitizers.js test file since with this option, the function essentially becomes a sanitizer, turning the string into a number. Hope that makes sense

],
invalid: [
'',
'password',
'hunter2',
'hello world',
'passw0rd',
'password!',
'PASSWORD!',
],
});
});

it('should validate base64URL', () => {
test({
validator: 'isBase64',
Expand Down