Regex to find 3 out of 4 conditions
Asked Answered
A

5

37

My client has requested that passwords on their system must following a specific set of validation rules, and I'm having great difficulty coming up with a "nice" regular expression.

The rules I have been given are...

  • Minimum of 8 character
  • Allow any character
  • Must have at least one instance from three of the four following character types...
    1. Upper case character
    2. Lower case character
    3. Numeric digit
    4. "Special Character"

When I pressed more, "Special Characters" are literally everything else (including spaces).

I can easily check for at least one instance for all four, using the following...

^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?\d)(?=.*?[^a-zA-Z0-9]).{8,}$

The following works, but it's horrible and messy...

^((?=.*?[A-Z])(?=.*?[a-z])(?=.*?\d)|(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[^a-zA-Z0-9])|(?=.*?[A-Z])(?=.*?\d)(?=.*?[^a-zA-Z0-9])|(?=.*?[a-z])(?=.*?\d)(?=.*?[^a-zA-Z0-9])).{8,}$

So you don't have to work it out yourself, the above is checking for (1,2,3|1,2,4|1,3,4|2,3,4) which are the 4 possible combinations of the 4 groups (where the number relates to the "types" in the set of rules).

Is there a "nicer", cleaner or easier way of doing this?

(Please note, this is going to be used in an <asp:RegularExpressionValidator> control in an ASP.NET website, so therefore needs to be a valid regex for both .NET and javascript.)

Arminius answered 5/3, 2014 at 17:26 Comment(7)
I don't think there is a "nice" one shot pattern to do this. The best way is to set a counter variable to zero and increment it with four separate tests. You can perform these tests with regex or with build in functions if available (I don't know asp)Community
I see what you're saying @Casimiret, but that would require multiple regexs and extra custom processing to work it out. Unfortunately this needs to be a single pattern, as it's a "configuration" that will drive the <asp:RegularExpressionValidator> control in several different parts of the website.Arminius
If you have the possibility to avoid, for this type of data, the RegularExpressionValidator and find an other way to do it, do not hesitate. Keep in mind that a simple (?=.*[a-z]) is bad in term of performance, multiplicate it by twelve...Community
The best way is to use <asp:CustomValidator> and write your own function.Community
Thanks again @Casimir - I fully take on board your comments, especially the bad performance of so many (?=.*[a-z]) blocks, but unfortunately I'm not in a position to implement anything else. This software is used in multiple locations, and having different custom validation for each just can't happen. That is why I'm stuck with a regex pattern.Arminius
This specific scenario doesn't seem to work. Any ideas? "AAAAAA1$"Elena
This should be the correct answer: https://mcmap.net/q/426844/-regex-for-checking-that-at-least-3-of-4-different-character-groups-existPersistent
A
29

It's not much of a better solution, but you can reduce [^a-zA-Z0-9] to [\W_], since a word character is all letters, digits and the underscore character. I don't think you can avoid the alternation when trying to do this in a single regex. I think you have pretty much have the best solution.

One slight optimization is that \d*[a-z]\w_*|\d*[A-Z]\w_* ~> \d*[a-zA-Z]\w_*, so I could remove one of the alternation sets. If you only allowed 3 out of 4 this wouldn't work, but since \d*[A-Z][a-z]\w_* was implicitly allowed it works.

(?=.{8,})((?=.*\d)(?=.*[a-z])(?=.*[A-Z])|(?=.*\d)(?=.*[a-zA-Z])(?=.*[\W_])|(?=.*[a-z])(?=.*[A-Z])(?=.*[\W_])).*

Extended version:

(?=.{8,})(
  (?=.*\d)(?=.*[a-z])(?=.*[A-Z])|
  (?=.*\d)(?=.*[a-zA-Z])(?=.*[\W_])|
  (?=.*[a-z])(?=.*[A-Z])(?=.*[\W_])
).*

Because of the fourth condition specified by the OP, this regular expression will match even unprintable characters such as new lines. If this is unacceptable then modify the set that contains \W to allow for more specific set of special characters.

Archiepiscopal answered 5/3, 2014 at 18:7 Comment(12)
Thanks for that @Daniel. I do like the [\W_], although I personally don't think it's a readable as [^a-zA-Z0-9]Arminius
I have removed the part about matching "abcdefg1" from the above comment - my regex tester defaults the i switch to on. Switching i off and your expression work perfectlyArminius
Interesting idea... but why doesn't it work in my preferred checker? regextester.comArminius
@Arminius I removed that idea, the grouping didn't work correctly with the look aheads. However I found a d'oh! simplification that makes the regex much easier on the eyes.Archiepiscopal
@DanielGimenez I have to keep some special characters only so can you please help me. I have to keep [@#$%^*()!&=\s] this only. Rest conditions are same.Woodshed
Works for all the 3 out of 4 scenarios, EXCEPT this "AAAAAA1$". Any ideas?Elena
@Elena what flavor regex? I tried it on RegExr and it passed the regex.Archiepiscopal
This regex picks up 'q1q1q1q1' when it shouldn'tBrod
@Vladimirs, what flavor of regex did you try it in? I just tried q1q1q1q1 in regextester and it didn't match - did you mistakenly include those single quotes?Archiepiscopal
@DanielGimenez I tried Javascript and PCRE - regexr.com, also I see the same in regextester.com . I think it might be matching it because of new line.. Might be worth to wrap your regex into ^$Brod
@Brod - Yes, it would match if you put in a new line. As described in the OP, condition 4 matches Special Characters which are further defined as "literally everything else." I will add a comment to the end of the answer to avoid any future confusion, however I don't think wrapping in ^$ is a specific enough of a solution (what happens if new line is mid string?).Archiepiscopal
@DanielGimenez new lines and carriage returns are generally not allowed in password boxes and most proper browsers strip them out, hence I would prefer wrapping in ^$. However comment will do as well. ThanksBrod
M
2

I'd like to improve the accepted solution with this one

^(?=.{8,})(
    (?=.*[^a-zA-Z\s])(?=.*[a-z])(?=.*[A-Z])|
    (?=.*[^a-zA-Z0-9\s])(?=.*\d)(?=.*[a-zA-Z])
).*$
Mar answered 30/9, 2017 at 12:32 Comment(0)
C
2

Is there a "nicer", cleaner or easier way of doing this?

This is a 10 year old question and has an accepted answer but as one observe that all the existing answers are repeating conditions thus making regex not-so-clean-and-nice and obviously that makes regex very hard to maintain and read.

Here is how you can do this in a single regex without repeating conditions andwithout using any lookahead:

^(?:[a-z]()|\d()|[A-Z]()|[^\n\w]()){8,}(?:\1\2(?:\3|\4)|\3\4(?:\1|\2))$

RegEx Demo

RegEx Details:

  • ^:
  • (?::
    • [a-z](): Match an lowercase letter and an empty capture #1
    • |: OR
    • \d(): Match a digit and an empty capture #2
    • |: OR
    • [A-Z](): Match an uppercase letter and an empty capture #3
    • |: OR
    • [^\n\w](): Match a special letter and an empty capture #4
  • ){8,}: End non-capture group. Repeat this group 8+ times
  • (?:\1\2(?:\3|\4)|\3\4(?:\1|\2)): In this non-capture group we are making sure that any 3 empty groups were matched earlier
  • $: End
Canoe answered 5/3 at 17:57 Comment(4)
Very, very clever... took me a while to get my head around the logic of the non-capture group. And I wasn't aware you could have/use empty capture groups in that way. Where were you 10 years ago?! ;-)Arminius
Thanks for the kind words!! I was very much around stackoverflow 10 years ago as well but somehow didn't come across this problem then. Those were the heydays of SO when we used to get tons of exciting questionsCanoe
While clever, this is technically incorrect since the regex was meant to be used in a JS context, and JS back references to uncaptured capturing groups simply match the empty string instead of failing as in PCRE and .NET. A simple example: /(?:(a)|b)\1/.test('b') // true.Inhabited
.NET would be fine with this regex though, which was one of the required regex flavor in OP.Canoe
E
1

The above Regex worked well for most scenarios except for strings such as "AAAAAA1$", "$$$$$$1a" This could be an issue only in iOS ( Objective C and Swift) that the regex "\d" has issues The following fix worked in iOS, i.e changing to [0-9] for digits

^((?=.*?[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,}$
Elena answered 8/5, 2017 at 13:48 Comment(0)
F
1

Password must meet at least 3 out of the following 4 complexity rules,

[at least 1 uppercase character (A-Z) at least 1 lowercase character (a-z) at least 1 digit (0-9) at least 1 special character — do not forget to treat space as special characters too]

at least 10 characters

at most 128 characters

not more than 2 identical characters in a row (e.g., 111 not allowed)

'^(?!.(.)\1{2}) ((?=.[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])).{10,127}$'

(?!.*(.)\1{2})

(?=.[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])

.{10,127}

Farmann answered 9/8, 2018 at 20:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.