Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Regex: "password must have at least 3 of the 4 of the following"

I'm a Regex newbie, and so far have only used it for simple things, like "must be a number or letter". Now I have to do something a bit more complex.

I need to use it to validate a password, which must be 8-16 characters, free of control/non-printing/non-ASCII characters, and must have at least three of the following:

  • one capital letter
  • one lowercase letter
  • one number 0-9
  • one symbol character ($, %, &, etc.)

I'm thinking what I have to do is write something like "one capital letter, lowercase letter and number, OR one capital letter, lowercase letter and one symbol, OR one capital letter, one number or one symbol, OR...." to cover all possible "3 out of 4" combinations, but that seems excessive. Is there a simpler solution?

like image 998
james.spinella Avatar asked Jul 02 '15 17:07

james.spinella


3 Answers

The correct way to do this is to check all of the five conditions separately. However, I assume there is a reason you want a regex, here you go:

/^((?=.*[A-Z])(?=.*[a-z])(?=.*\d)|(?=.*[a-z])(?=.*\d)(?=.*[\$\%\&])|(?=.*[A-Z])(?=.*\d)(?=.*[\$\%\&])|(?=.*[A-Z])(?=.*[a-z])(?=.*[\$\%\&])).{8,16}$/

Explanation:

  • We want to match the whole thing, hence we surround it with ^$
  • .{n,m} matches between n and m characters (8 and 16 in our case).
  • The general way you can check if a string contains something, without actually matching it is by using positive lookahead (?=.*X), where X is the thing you want to check. For example, if you want to make sure the string contains a lowercase letter you can do (?=.*[a-z]).
  • If you want to check if a string contains X, Y and Z, but without actually matching them, you can use the previous recipe by appending the three lookaheads (?=.*X)(?=.*Y)(?=.*Z)
  • We use the above to match three of the four things mentioned. We go through all possible combinations with |(or) - cCD|cDS|CDS|CcS (c = lowercase letter, C = capital letter, D = digit, S = special)

See it in action

like image 78
ndnenkov Avatar answered Nov 09 '22 08:11

ndnenkov


The best way to do this is by checking each condition separately. Performance will suffer if you try to fit all conditional criteria into one expression (see the accepted answer). I also highly recommend against limiting the length of the password to 16 chars — this is extremely insecure for modern standards. Try something more like 64 chars, or even better, 128 — assuming your hashing architecture can handle the load.

You also didn't specify a language, but this is one way to do it in JavaScript:

var pws = [
    "%5abCdefg",
    "&5ab",
    "%5abCdef",
    "5Bcdwefg",
    "BCADLKJSDSDFlk"
];

function pwCheck(pw) {
    var criteria = 0;
    if (pw.toUpperCase() != pw) {
        // has lower case letters
        criteria++;
    }
    if (pw.toLowerCase() != pw) {
        // has upper case letters
        criteria++;
    }
    if (/^[a-zA-Z0-9]*$/.test(pw) === false) {
        // has special characters
        criteria++;
    }
    if (/\d/.test(pw) === true) {
        // has numbers
        criteria++;
    }
    // returns true if 3 or more criteria was met and length is appropriate
    return (criteria >= 3 && pw.length >= 8 && pw.length <= 16);
}

pws.forEach(function(pw) {
    console.log(pw + ": " + pwCheck(pw).toString());
});
like image 45
brandonscript Avatar answered Nov 09 '22 09:11

brandonscript


Not sure if its a iOS thing, the regex with "d" for digits [0-9] wasn't working as expected, example String that had issues = "AAAAAA1$"

The fix below works fine in Objective-C and Swift 3

   ^((?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])|(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[^a-zA-Z0-9])|(?=.*?[A-Z])(?=.*?[0-9])(?=.*?[^a-zA-Z0-9])|(?=.*?[a-z])(?=.*?[0-9])(?=.*?[^a-zA-Z0-9])).{8,16}$
like image 30
Naishta Avatar answered Nov 09 '22 09:11

Naishta