10
\$\begingroup\$

I'm trying to write regex to validate the password for the given rule.

Passwords must be at least 8 characters in length and contain at least 3 of the following 4 types of characters:

  • lower case letters (i.e. a-z)
  • upper case letters (i.e. A-Z)
  • numbers (i.e. 0-9)
  • special characters (e.g. !@#$&*)

I was going through this discussion and found this really great answer over there.

Now I'm trying to write regex for the mentioned requirements and I came up with the solution like this

^(?=.*[A-Z])(?=.*[!@#$&*])(?=.*[0-9])(?=.*[a-z]).{8,}|
(?=.*[!@#$&*])(?=.*[0-9])(?=.*[a-z]).{8,}|
(?=.*[A-Z])(?=.*[0-9])(?=.*[a-z]).{8,}|
(?=.*[A-Z])(?=.*[!@#$&*])(?=.*[a-z]).{8,}|
(?=.*[A-Z])(?=.*[!@#$&*])(?=.*[0-9]).{8,}$

and it is working perfect see rubular but I want to optimize these regex and I'm not sure If there are any way to simplify this. Any suggestion will be appreciated.

\$\endgroup\$
7
  • 31
    \$\begingroup\$ If possible, drop those silly rules and use a password strength estimator, like zxcvbn instead. Your rules prevent many strong passwords (e.g. 32 random hex characters which have 128 bits of entropy) and allow super weak passwords like Password1 \$\endgroup\$ Commented Aug 1, 2016 at 13:14
  • 24
    \$\begingroup\$ On a usability note, don’t use such simplistic password quality filters, they are crap, infuriatingly annoying for users, and indeed peddle a false understanding of IT security: Passwords can be completely safe and still violate your rules (and, conversely, a passing password may still be unsafe). \$\endgroup\$ Commented Aug 1, 2016 at 13:15
  • 14
    \$\begingroup\$ Every time I have to enter a password and space and punctuation is not accepted, I die a little inside. I should be allowed to write a sentence in there, if I want to. There is no sensible reason to disallow it - the password is going to just be hashed and salted anyway, right? Why does it matter what goes in - it's not like you have limitations on the data that goes into the field - the hashed value would be garbled but of valid type - whether your database handles, say, commas or umlauts or spaces is irrelevant. \$\endgroup\$
    – VLAZ
    Commented Aug 1, 2016 at 16:35
  • 1
    \$\begingroup\$ You tagged this question as both ruby and javascript. Since regex dialects differ by language, please clarify how you intend to use this regex, preferably by including the Ruby and/or JavaScript code. \$\endgroup\$ Commented Aug 1, 2016 at 21:51
  • 4
    \$\begingroup\$ Take those pesky rules out and shoot them! Passphrases are so much better and yet won't pass. \$\endgroup\$ Commented Aug 2, 2016 at 2:25

4 Answers 4

20
\$\begingroup\$

Split the regex into smaller parts to check each rule individually.

Here's the code for JavaScript. The same simple logic can be used for Ruby or any other language.

// Check the length
if (str.length >= 8 && validate()) {

}

// Check if all the characters are present in string
function validate(string) {
    // Initialize counter to zero
    var counter = 0;

    // On each test that is passed, increment the counter
    if (/[a-z]/.test(string)) {
        // If string contain at least one lowercase alphabet character
        counter++;
    }
    if (/[A-Z]/.test(string)) {
        counter++;
    }
    if (/[0-9]/.test(string)) {
        counter++;
    }
    if (/[!@#$&*]/.test(string)) {
        counter++;
    }

    // Check if at least three rules are satisfied
    return counter >= 3;
}
\$\endgroup\$
0
10
\$\begingroup\$

@Tushar's idea isn't bad, but it is NOT optimal for re-usability and extensability.

What I propose is to filter out the rules that failed. This way, you can count how many failed a bit faster, instead of having a counter.
Even if it isn't that much faster, it is still easier to add rules.

function validate(string) {
    if(string.length < 8)
    {
        return false;
    }

    var rules = [
        /[a-z]/, //letters (lower-case)
        /[A-Z]/, //letters (upper-case)
        /\d/, //numbers (similar to /[0-9]/)
        /[!@#$&*]/ //some symbols
    ];

    return rules.filter(function(rule){
        return rule.test(string);
    }).length >= 3;
}

Yes, there is a magic number. I don't see any other way to improve it. If you want, you can do the oposite and count how many rules failed.
This may be a bit more flexible, if you want to allow some rules to fail.


Speed-wise, it is up to 5 times slower, but sometimes it is faster.
I guess is has something to do with the .filter method.

Here's how I tested the speed:

function validate_mine(string) {
    if(string.length < 8)
    {
        return false;
    }

    var rules = [
        /[a-z]/, //letters (lower-case)
        /[A-Z]/, //letters (upper-case)
        /\d/, //numbers (similar to /[0-9]/)
        /[!@#$&*]/ //some symbols
    ];

    return rules.filter(function(rule){
        return rule.test(string);
    }).length >= 3;
}

function validate_Tusha(string) {
    // Initialize counter to zero
    var counter = 0;

    // On each test that is passed, increment the counter
    if (/[a-z]/.test(string)) {
        // If string contain at least one lowercase alphabet character
        counter++;
    }
    if (/[A-Z]/.test(string)) {
        counter++;
    }
    if (/[0-9]/.test(string)) {
        counter++;
    }
    if (/[!@#$&*]/.test(string)) {
        counter++;
    }

    // Check if at least three rules are satisfied
    return counter >= 3;
}

console.time('validate_mine');
[
	'test it',
	'Me want this!',
	'Th1s_is_cool',
	'$tuff!_should-be-fast',
	'now! run it!'
].map(validate_mine);
console.timeEnd('validate_mine');

console.time('validate_Tusha');
[
	'test it',
	'Me want this!',
	'Th1s_is_cool',
	'$tuff!_should-be-fast',
	'now! run it!'
].map(validate_Tusha);
console.timeEnd('validate_Tusha');


If you want to make something extremelly extensible, one could follow @Vld's idea and pass a list of rules to a function, and an optional parameter to relax how many rules can be missed.

Something similar to this:

function validate(string, rules, min_pass) {
    'use strict';
    if(min_pass < 0 || min_pass > rules.length)
    {
        throw new RangeError('min_pass should be between 0 and ' + rules.length);
    }

    return rules.filter(function(rule){
        if(rule instanceof Function)
        {
            return !!rule.bind(string)(string);
        }
        else if(rule instanceof RegExp)
        {
            return rule.test(string);
        }
        return false;
    }).length >= min_pass;
}

This accepts 3 parameters:

  • string: The string to be tested,
  • rules: An array of regular expressions or functions,
  • min_pass: Minimum number of rules to pass.

To obtain the results you want, using this new method, you could do like this:

validate(
    'string here',
    [
        /[a-b]/,
        /[A-B]/,
        /\d/,
        /[!@#$&*]/
    ],
    3
);

You still have to validate the length before, since I don't have a way to say that a rule is important or something.
The formatting still isn't optimal, but you get the idea.


Also, this is not a good way to check security. With your rules, a password like Passw0rd! is valid, but we all know how crappy that is. Any password cracker worth it's salt will have rules to try these permutations. Heck, it may even be the one of the very first words in the dictionary list!
This will leave you prone to dictionary-based attacks.
Just make sure you use a good and unique salt, with a strong hash (like SHA256) instead of relying on this type of rules to check password strength.
This fails for passwords like ñó wÿn fõr ýôü, which may be far more secure than some of the passwords you validate.

\$\endgroup\$
15
  • \$\begingroup\$ But the problem is that you need to have at least 3 out of 4. Doesn't that solution require all rules to match? Also, you aren't showing separate uppercase and lowercase rules, whereas they need to be distinct. \$\endgroup\$
    – VLAZ
    Commented Aug 1, 2016 at 16:38
  • \$\begingroup\$ @Vld You mean, the first rule? It matches both upper and/or lower. \$\endgroup\$ Commented Aug 1, 2016 at 16:39
  • \$\begingroup\$ Yes. That's the problem. password123 and Password123 are different - the latter should be valid according to OP's requirements. With your validation it won't be. Password_ should also be valid. While Password_123 and password_123 would be valid with yours, it's incidental, not explicitly checking for the requirements. \$\endgroup\$
    – VLAZ
    Commented Aug 1, 2016 at 16:45
  • \$\begingroup\$ @Vld Give me 2 minutes \$\endgroup\$ Commented Aug 1, 2016 at 16:47
  • \$\begingroup\$ @Vld What do you think now? I hope it is better now. \$\endgroup\$ Commented Aug 1, 2016 at 16:54
6
\$\begingroup\$

Honestly, I think regex is the wrong approach here. Just go with regular form validation code - take the value, validate its length, set a counter to 0, and then for each requirement that it meets, increment the counter. Then check if it meets at least 3 by checking the counter.

The resulting code will be understandable for any programmer that comes across the code. It will be possible to easily change the rules to allow other special characters, to disallow certain common passwords, to change the length...

Your long regex is, whilst properly formatted, a real pain to maintain. You can still use the individual sub-expressions in your validation code, but it'll be a lot more readable.

\$\endgroup\$
0
1
\$\begingroup\$

No.

This is not a regular grammar. Regular expressions are not the right tool for this job.

Check string length and for the existence of character code points within each of the necessary ranges.

Better still, follow the other suggestions to push for a more legitimate password complexity algorithm.

I'll let someone edit in the appropriate and obligatory xkcd and stock overflow references.

\$\endgroup\$
3
  • 2
    \$\begingroup\$ It's a regular grammar, though the theoretical regular expression is rather complex. Anyway, it's true that regular expression is too clunky for this use case. \$\endgroup\$
    – nhahtdh
    Commented Aug 2, 2016 at 3:03
  • \$\begingroup\$ @nhahtdh I'll take your word for it. Procedural validation is straight forward enough I can't force my brain to try a regex approach. \$\endgroup\$
    – psaxton
    Commented Aug 2, 2016 at 3:32
  • 1
    \$\begingroup\$ It's a very regular grammar. If you think about it in a straightforward way - you could create a RegExp that does | between all the different orders of 3 of the 4 requirements (with anything in between and after) and if one of those matches the regexp matches. Writing a regexp is easy but it's clearly not a good idea. A good way to think about non-regular grammars is that they require some sort of "memory" or the ability to "keep track" of an unbounded amount of things. For any bounded amount (3 in this case) the grammar is regular. \$\endgroup\$ Commented Aug 2, 2016 at 6:31

Not the answer you're looking for? Browse other questions tagged or ask your own question.