Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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 [, options])** | 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 }`
**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 occurrences 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/sanitizers.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,28 @@ describe('Sanitizers', () => {
});
});

it('should score passwords', () => {
test({
sanitizer: 'isStrongPassword',
args: [{
returnScore: true,
pointsPerUnique: 1,
pointsPerRepeat: 0.5,
pointsForContainingLower: 10,
pointsForContainingUpper: 10,
pointsForContainingNumber: 10,
pointsForContainingSymbol: 10,
}],
expect: {
abc: 13,
abcc: 13.5,
aBc: 23,
'Abc123!': 47,
'!@#$%^&*()': 20,
},
});
});

it('should normalize an email based on domain', () => {
test({
sanitizer: 'normalizeEmail',
Expand Down
29 changes: 29 additions & 0 deletions test/validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -8561,6 +8561,35 @@ describe('Validators', () => {
});
});

it('should validate strong passwords', () => {
test({
validator: 'isStrongPassword',
args: [{
minLength: 8,
minLowercase: 1,
minUppercase: 1,
minNumbers: 1,
minSymbols: 1,
}],
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