First off, a disclaimer: I'm not convinced that using a single regular expression is the best tool for this task. We can make it work anyway, but now we all know that I know that we're abusing regexps :-)
Your current code will produce strange patterns that won't do anything close to what you want. Since they're all along the same lines let's look at one example and see what's going wrong.
Suppose we only have an uppercase policy to apply (say, policy.UpperCaseLength = 4
). With a bit of whitespace to help see what's going on, this results in:
pattern = `^[A-Z{4,}]{8,20}$`
Everything between those square brackets is treated as part of a character class. Although your intention was for {4,}
to say that there should be at least four characters matching [A-Z]
, [A-Z{4,}]
is interpreted as "an uppercase character, or {
, or 4
, or ,
, or }
".
What you really want to test is "disregarding other characters in the input, are there 4 separate instances of an uppercase letter?"
This is actually a pretty easy pattern: we can search for anything (.*
) followed by [A-Z]
, at least four times, i.e. (.*[A-Z]){4,}
. In fact we can be slightly more efficient about this – as long as there are four matches we don't need to keep matching, and while we're at it we can use a non-greedy quantifier, and a non-capturing group. So, to match a string that contains at least policy.UpperCaseLength
instances of an uppercase letter, we could use a pattern:
`(?:.*?[A-Z]){${policy.UpperCaseLength}}`
We can use this same shape of thinking to make patterns for the other character classes e.g.
`(?:.*?\\d){${policy.NumericLength}}`
We can combine the patterns into one regular expression using look-ahead assertions. An assertion effectively checks the pattern against the input string without "using up" the characters, so we can apply several patterns to the string at once.
So, if we add in the rules wrapped in the look-ahead assertion syntax ((?= ... )
) then our pattern from before looks like:
pattern = `^(?=(?:.*?[A-Z]){4}){8,20}$`
This still isn't quite right (in fact it won't match anything – the look-ahead group matches zero characters, so it could only match an empty string that had at least four uppercase characters!) – what we really want to do with the MinimumLength
and MaximumLength
policies is check that there are between 8 and 20 instances of any character, i.e. .
. So, add that to the pattern:
pattern = `^(?=(?:.*?[A-Z]){4}).{8,20}$`
// ^^^^^^^ enforce length range
Now the pattern checks that there are at least four uppercase characters, and then that the whole input is between 8 and 20 characters. We can construct other patterns based on the policy
in the same way:
function generateRegexExpression(policy) {
let pattern = "";
if (policy.UppercaseLength > 0) pattern += `(?=(?:.*?[A-Z]){${policy.UppercaseLength},})`;
if (policy.LowercaseLength > 0) pattern += `(?=(?:.*?[a-z]){${policy.LowercaseLength},})`;
if (policy.NonAlphaLength > 0) pattern += `(?=(?:.*?[!@#$%^&*()_+-={}\\[\\]\\|;:'\",.<>/?\`~]){${policy.NonAlphaLength},})`;
if (policy.NumericLength > 0) pattern += `(?=(?:.*?\\d){${policy.NumericLength},})`;
if (policy.AlphaLength > 0) pattern += `(?=(?:.*?[A-Za-z]){${policy.AlphaLength},})`;
pattern = `^${pattern}.{${policy.MinimumLength},${policy.MaximumLength}}$`;
return new RegExp(pattern);
}
const policy = {
"MinimumLength": 8,
"MaximumLength": 20,
"UppercaseLength": 0,
"LowercaseLength": 0,
"NonAlphaLength": 0,
"NumericLength": 1,
"AlphaLength": 1
};
const re = generateRegexExpression(policy);
// Test some inputs
for (const password of [
"aBcDeFgH1", // SUCCESS
"aBcDeFgH", // FAIL -- does not satisfy NumericLength
"aBcDeF1", // FAIL -- MinimumLength
"1abcDEFGhijklmNOPQrstuvwxyz", // FAIL -- MaximumLength
"12345678" // FAIL -- AlphaLength
]) {
console.log(password + " => " + re.test(password));
}