I've been trying to follow the OWASP recommendations for authentication. In particular:
Applications should enforce password complexity rules to discourage easy to guess passwords. Password mechanisms should allow virtually any character the user can type to be part of their password, including the space character.
Passwords should, obviously, be case sensitive in order to increase their complexity (this is already be handled in our system by use of password_hash functions)
Password must meet at least 3 out of the following 4 complexity rules
1) at least 1 uppercase character (A-Z)
2) at least 1 lowercase character (a-z)
3) at least 1 digit (0-9) 4) at least 1 special character (punctuation) — do not forget to treat space as special characters too
4) not more than 2 identical characters in a row (e.g., 111 not allowed)
Ideally, the application would indicate to the user as they type in their new password how much of the complexity policy their new password meets. In fact, the submit button should be grayed out until the new password meets the complexity policy and the 2nd copy of the new password matches the 1st. This will make it far easier for the user to understand and comply with your complexity policy.

Some questions:
Do you guys [man]trim[/man] passphrases?
Given that all user input is typically UTF-8 these days, do we allow users to use non-latin chars? Seems like a really good idea to me for password complexity but not necessarily for data portability.
Any thoughts on regexes to:
recognize that Å qualifies as uppercase and å is lowercase, etc. for non-latin chars?
recognize 3 or more identical chars in a row?
* recognize the presence of a 'special character' without getting too constrictive?
What logic do you suggest to create one of those 'password strength' meters? I'm guessing that we increase the meter's value once each rule gets matched...

That last point is really important to me personally. I've been infuriated that a certain financial website I use doesn't permit a commonly used puncuation mark that I like to use. It's maddening.

Also welcome are Javascript techniques for this as the guidelines encourage immediate feedback for the user while they enter their passwords. I think it's important that we match our PHP rules with corresponding JS.

    Short and sweet, ask if you want talking 🙂

    1) No I do not alter the password in any way
    2) I do believe I allow full utf-8 always, since my forms accept it and I don't do any modifications. As for portability, password_hash produces an
    3) this is how I check for a valid password on the service. I've done it this way because I can make similar on javascript easily. I'm not good with regex that works on utf-8... and I don't like complex regex checking multiple conditions at once.

    function checkPass($str, &$failures=array(), $complexity=3) 
    {
    	$len = strlen($str);
    	$i = 0;
    	$p = count($failures);
    
    
    if( $len < 8 || $len > 72 ) {
    	$failures[] = $len < 8 ?
    		'Not long enough (min 8)' :
    		'Too long (max 72)';
    }
    
    if( getMaxRptCharCount($str) > 2 ) {
    	$failures[] = 'Too many repeating characters';
    }
    
    if( count($failures) > $p ) {
    	return false;
    }
    
    
    if( preg_match('/[^\w\d]/i', $str) ) {
    	$i++;
    } else {
    	$failures[] = 'Contains no special characters';
    }
    
    if( preg_match('/\d/', $str) ) {
    	$i++;
    } else {
    	$failures[] = 'Contains no digits';
    }
    
    if( $len > similar_text($str, strtoupper($str)) ) {
    	$i++;
    } else {
    	$failures[] = 'Contains no lower case letters';
    }
    
    if( $len > similar_text($str, strtolower($str)) ) {
    	$i++;
    } else {
    	$failures[] = 'Contains no upper case letters';
    }
    
    return $i >= $complexity;
    }
    
    function getMaxRptCharCount($str) 
    {
    	$max = 0;
    	$i = 0;
    	$prev = '';
    	foreach(str_split($str) as $char) {
    		if($prev != $char) {
    			if( $i > $max ) {
    				$max = $i;
    			}
    			$i = 0;
    			$prev = $char;
    		}
    		$i++;
    	}
    	return $i > $max ? $i : $max;
    }
    

      PCRE regexps support Unicode properties, so that, e.g., upper/lowercase requirements in other scripts (that have an upper/lowercase distinction) can be used.

        Derokorian, thanks for your response. I'm not really sure what [man]similar_text[/man] calculates, but I'm not sure it does what you think it does:

        		$str = "åHA";
        		$len = strlen($str);
        		if( $len > similar_text($str, strtoupper($str)) ) {
        			echo "its OK\n";
        		} else {
        			echo 'Contains no lower case letters';
        		}
        

        This results in "Contains no lower case letters" despite the lowercase å char. It's not clear to me if you should be using [man]mb_strlen[/man] and [man]mb_strtoupper[/man] or what as I am not sure what similar_text actually does.

        Weedpacket;11044767 wrote:

        PCRE regexps support Unicode properties, so that, e.g., upper/lowercase requirements in other scripts (that have an upper/lowercase distinction) can be used.

        I was aware that PCRE (specifcally preg_* functions) did support Unicode characters, however I still cannot imagine any clear way to concoct a preg_match statement that would check for the existence of any lowercase string in all the support Unicode/UTF-8 languages. I'm imagining that accented latin-type characters alone would make a really huge regex if you had to specify them all which we would be forced to do because the range [A-Z] does not include accented uppercase characters. This:

        php -r 'var_dump(preg_match("/[A-Z]+/","Å"));'

        outputs int(0). And then of course there are cyrillic characters and chinese and japanese chars, etc.

        I tried looking at the PHP docs on [rl=http://php.net/manual/en/regexp.reference.unicode.php]PCRE Unicode functionality[/url] and see that you can use the \p escape sequence (where \p{Lu} apparently requires the existence of some uppercase character) however I'm getting mixed results. Not sure if these results are due to an older version of PHP on my workstation (5.3.10-1ubuntu3.15) or what. Check it:

        php -r 'var_dump(preg_match("/\p{Lu}+/","A"));'
        int(1)
        php -r 'var_dump(preg_match("/\p{Lu}+/","Å"));'
        int(1)
        php -r 'var_dump(preg_match("/\p{Lu}+/","a"));'
        int(0)
        php -r 'var_dump(preg_match("/\p{Lu}+/","å"));'
        int(1)
        

        It's that last result which is disappointing. It obviously says å is an uppercase letter.

          sneakyimp;11044775 wrote:

          This results in "Contains no lower case letters" despite the lowercase å char. It's not clear to me if you should be using [man]mb_strlen[/man] and [man]mb_strtoupper[/man] or what as I am not sure what similar_text actually does..

          Erm, you're correct. In my actual code I use mb_strlen, mb_strtolower, and mb_strtoupper. But I simplified my code for posting and tested it in writecodeonline.com which doesn't have the mb library enabled, and forgot to change it back when posting! Good catch. It works really well with mb_* funcs as expected. Also, js String.toLowerCase() handles multibyte already.

            Hmmmm....I dunno. This:

            		$str = "åHA";
            		$len = mb_strlen($str);
            		if( $len > similar_text($str, mb_strtoupper($str)) ) {
            			echo "its OK\n";
            		} else {
            			echo 'Contains no lower case letters';
            		}
            

            still outputs "Contains no lower case letters" on my workstation.

            This also looks suspect:

            php -r '$str="Å"; var_dump(mb_strlen($str));'
            int(2)
            

              This is bad too:

              php -r '$str="å"; var_dump(mb_strtoupper($str));'
              string(2) "å"
              
                sneakyimp;11044783 wrote:

                This is bad too:

                php -r '$str="å"; var_dump(mb_strtoupper($str));'
                string(2) "å"
                

                PHP doesn't have native multibyte support. I believe you need to use escape sequences to put multibyte characters into a string in PHP.

                php.net wrote:

                A string is series of characters, where a character is the same as a byte. This means that PHP only supports a 256-character set, and hence does not offer native Unicode support.

                Additionally, I've never worked with UTF-8 literals in my code, only ever as input from a form or database read.

                  sneakyimp wrote:

                  It's that last result which is disappointing. It obviously says å is an uppercase letter.

                  Have you tried adding the '/u' modifier to the pattern? Also note that the result probably depends on the character set you're using to enter the text (that of your command line interface in this instance). With the modifier the pattern and search text are expected to be in UTF-8.

                    Derokorian;11044787 wrote:

                    PHP doesn't have native multibyte support. I believe you need to use escape sequences to put multibyte characters into a string in PHP.

                    It's been a long time since Weedpacket kindly edified me about how strings work in PHP, but I don't think you are correct. As he said so long ago:

                    Weedpacket wrote:

                    As far as PHP is concerned the string literal is just a string of bytes. No magic there. Whether it that string of bytes will represent a UTF-8-encoded string depends on whether whatever you edited the PHP file in saved it as such.

                    Derokorian wrote:

                    Additionally, I've never worked with UTF-8 literals in my code, only ever as input from a form or database read.

                    For the most part, this is true for me too but I am working on Ubuntu and vaguely recall that the native terminal window is properly UTF-8 compliant. I.e., any text files I create using the terminal window should be UTF-8 encoded without a Byte-Order Mark (BOM) at the beginning. I tried to test this just now by creating a file from the command line

                    mkdir ~/test
                    cd ~/test
                    echo "Å" > char.txt
                    

                    I then inspected the contents of char.txt with xxd:

                     $xxd char.txt
                    0000000: c385 0a                  

                    If I read that output correctly, the file contains 3 bytes, the first two of which correspond to the UTF-8 encoding of Å (namely c3 85 according to this chart) and the third of which is a newline char (\n). That the result has 3 bytes is itself a solid indicator that the charset in effect is in fact a multibyte charset because it encodes Å with more than one byte.

                    I can at least write a php script now that fetches the contents, trims off the newline char, and returns a single utf-8 encoded char, no?

                    <?php
                    
                    $str = trim(file_get_contents("char.txt"));
                    
                    echo $str . "\n";
                    echo "traditional string length is " . strlen($str) . "\n";
                    
                    echo "mb_strlen is " . mb_strlen($str) . "\n";
                    var_dump(preg_match("/[A-Z]+/", $str));
                    var_dump(preg_match("/\p{Lu}+/", $str));
                    var_dump(preg_match("/\p{Lu}+/u", $str));
                    var_dump(preg_match("/\p{Ll}+/",$str));
                    var_dump(preg_match("/\p{Ll}+/u",$str));
                    ?>
                    

                    The output of this is also bad in that mb_strlen is 2, but otherwise OK.

                    $ php test.php
                    Å
                    traditional string length is 2
                    mb_strlen is 2
                    int(0)
                    int(1)
                    int(1)
                    int(0)
                    int(0)
                    

                    Changing the contents of char.txt to the lowercase å still has problems with mb_strlen being 2 instead of 1, but the /u flag does in fact help.

                    $ php test.php
                    å
                    traditional string length is 2
                    mb_strlen is 2
                    int(0)
                    int(1)
                    int(0)
                    int(0)
                    int(1)
                    

                    Trying to convert it to uppercase using mb_strtoupper also does not work.

                    echo mb_strtoupper($str) . "\n"; // just outputs å, does not convert.

                    Something is obviously not working correctly.

                      Ah so I'm looking at the docs for [man]mb_strlen[/man] and it takes a second parameter, "encoding"

                      docs wrote:

                      The encoding parameter is the character encoding. If it is omitted, the internal character encoding value will be used.

                      Adding a second param to my utf-8 calls and using /u for preg calls solves the problems:

                      <?php
                      $str = "å";
                      
                      echo "mb_strlen is " . mb_strlen($str, "UTF-8") . "\n";
                      echo "uppercase is " . mb_strtoupper($str, "UTF-8") . "\n";
                      
                      
                      if (preg_match("/\p{Lu}+/u", $str)) {
                        echo "Your string contains an uppercase letter\n";
                      } else {
                        echo "No uppercase letters in your string!\n";
                      }
                      

                      Output:

                      mb_strlen is 1
                      uppercase is Å
                      No uppercase letters in your string!
                      

                      I should have RTFM! :rolleyes:

                        OK so I think I've found a pretty clever regex to recognize repeated characters:

                        <?php
                        // trying to recognize repeated chars
                        
                        $pattern = '/(.)+\1{2,}/u';
                        
                        $str = "aaabcdef";
                        
                        if (preg_match($pattern, $str)) {
                          echo "You repeated a character too many times!\n";
                        } else {
                          echo "NO excessive repeating\n";
                        }
                        ?>
                        

                        Ignoring for a moment that, this pattern doesn't seem to catch repeated å chars on an old server I'm running, can anyone suggest a good way to test it to make sure it'll work for all the myriad UTF-8 char possibilities that exist? Is there some way to do a comprehensive test across all utf-8-supported chars?

                          Well, you'll need to make sure your literals are encoded in the source code using UTF-8 (whether your command line does that I don't know, but I suspect it may not).

                          If you want to see if a single character appears at least three times consecutively you don't need to see how many times it appears, just whether it appears at least three times: [font=monospace]/(.)\1\1/u[/font] would be sufficient (this would also avoid matching "qwerqwerqwer").

                          Is there some way to do a comprehensive test across all utf-8-supported chars?

                          I suppose you could use the Unicode datasets to extract a comprehensive list of characters and their categories. Generate a UTF-8-encoded list of characters:

                          
                          for($i = 0; $i < 65536; $i++)
                          {
                          	$l = chr($i >> 8);
                          	$r = chr($i & 255);
                          	$o = iconv('UCS-2', 'UTF-8', $l.$r);
                          	echo dechex($i)," = ",$o,"\n";
                          }
                          
                            Weedpacket;11044815 wrote:

                            Well, you'll need to make sure your literals are encoded in the source code using UTF-8 (whether your command line does that I don't know, but I suspect it may not).

                            As I am using Ubuntu, and based on most of the behavior I've seen, I think that I have managed to properly encode my source as UTF-8. The mb_* functions seem to properly recognize å as lowercase and Å as uppercase and convert between the two. I have also used xxd to inspect the contents of the text files I have created containing UTF-8 strings and these appear to match the binary encodings that I've seen in various UTF-8 charts. I believe I mentioned this above.

                            Weedpacket;11044815 wrote:

                            If you want to see if a single character appears at least three times consecutively you don't need to see how many times it appears, just whether it appears at least three times: [font=monospace]/(.)\1\1/u[/font] would be sufficient (this would also avoid matching "qwerqwerqwer").

                            Thanks for pointing out that these regexes will also match repeated patterns. The OWASP guidelines don't seem to take exception to that kind of repetition so I expect I'll adopt your pattern.

                            Weedpacket;11044815 wrote:

                            I suppose you could use the Unicode datasets to extract a comprehensive list of characters and their categories.

                            Oh my I was sniffing around those files a little bit and became immediately aware that Unicode is quite complicated.

                            Weedpacket;11044815 wrote:

                            Generate a UTF-8-encoded list of characters:

                            for($i = 0; $i < 65536; $i++)
                            {
                            	$l = chr($i >> 8);
                            	$r = chr($i & 255);
                            	$o = iconv('UCS-2', 'UTF-8', $l.$r);
                            	echo dechex($i)," = ",$o,"\n";
                            }
                            

                            Thanks for that bit of code. I'm not at all sure what the bitshifting is about, but it looks to me like you are attempting to traverse the ordinal space of UCS-2 (old-school UTF-16??) chars and generate their utf-8 counterparts using iconf. Interestingly, that code generates an E_NOTICE for 2048 of those ordinals. Here are a few:

                            d8, d9, da, db, dc, dd, de, df, 1d8, 1d9, 1da, 1db, 1dc, 1dd, 1de, 1df, 2d8, 2d9, 2da, 2db, 2dc, 2dd, 2de, 2df...

                            I'm guessing the reason you stopped at 65536 is because this will exhaust the UCS-2 ordinal space because it's only a 2-byte encoding scheme. Note that this would mean that we never get around to &#55357;&#56892;, the cat face with wry smile char (U+1F63C or &#128572).

                            I doubt I'll attempt the repeated char checker on the entire UTF-8 space. I've tested this function with &#55357;&#56892; and it seems to recognize when there are 3 and when there are not

                            	function string_repeats_char_too_much($str) {
                            		if (preg_match('/(.)\1\1/u', $str)) {
                            			return TRUE;
                            		} else {
                            			return FALSE;
                            		}
                            	}
                            
                            

                            I'm trying to to use the Unicode preg matching to concoct a regex that will match not just latin numbers (0-9) but also Chinese Numerals or Japanese Numerals, etc.:'

                            	function string_has_digit($str) {
                            		if (preg_match("/\p{N}+/u", $str)) {
                            			return TRUE;
                            		} else {
                            			return FALSE;
                            		}
                            	}
                            

                            It seems to work for latin numbers, but curiously, &#19971;, the Japanese char for 7, returns false:

                            var_dump(string_has_digit("&#19971;"));
                            

                            Using my string_has_digit function against your loop above, I get 691 chars that return TRUE. &#12838; is one of these, &#19971; is not. WEIRD.

                              I'm also hoping to implement a function to sniff for 'special chars' as described in the OWASP recommendations. Looking at these, it occurs to me that this is a pretty limited set of supposedly special chars (i.e., not really so special are they?). On the other hand, to use the much broader Unicode pattern matching to detect special chars (punctuation, symbols, whitespace) could open the door to an much broader array of password possibilities but it might get tricky if you let folks enter these UTF-8 possibilities because they might end up entering characters that simply aren't available in another context. What do we think? Do we limit 'special chars' to the 20 or so listed in the OWASP recommendations or do we do a broader check?

                              If we do limit special chars to the OWASP recommendations, I have this function working, but I wonder if it would detect these chars properly in the midst of some UTF-8 string. My guess is that the 'special chars' defined here are all basic ASCII and will therefore be represented with the same binary sequence whether UTF-8 or ASCII strings or latin strings are being dealt with, however the docs don't say that [man]strpbrk[/man] is binary safe.

                              if (strpbrk($chk, " !\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~") !== FALSE) {
                              	echo "TRUE";
                              } else {
                              	echo "FALSE";
                              }
                              

                              These alternative functions would open the door to a much broader spectrum of possibilities, but I'm wondering if this might cause users to get themselves into trouble somehow:

                              	/**
                              	 * Function to test whether a string has a "special" char -- i.e., punctuation or white space. Should work with UTF-8 strings with all kinds of weird chars.
                              	 * @param string $str The string to be tested
                              	 * @return boolean TRUE if the string contains a "special" char.
                              	 */
                              	public static function string_has_special_char($str) {
                              		if (self::string_has_punctuation($str) || self::string_has_symbol($str) || self::string_has_whitespace($str)) {
                              			return TRUE;
                              		} else {
                              			return FALSE;
                              		}
                              	}
                              
                              /**
                               * Function to test whether a string has a punctuation char. Should work with UTF-8 strings with all kinds of weird chars.
                               * @param string $str The string to be tested
                               * @return boolean TRUE if the string contains a punctuation char.
                               */
                              public static function string_has_punctuation($str) {
                              	if (preg_match("/\p{P}+/u", $str)) {
                              		return TRUE;
                              	} else {
                              		return FALSE;
                              	}
                              }
                              /**
                               * Function to test whether a string has a symbol char -- i.e., math, currency symbols, etc. Should work with UTF-8 strings with all kinds of weird chars.
                               * @param string $str The string to be tested
                               * @return boolean TRUE if the string contains a symbol char.
                               */
                              public static function string_has_symbol($str) {
                              	if (preg_match("/\p{S}+/u", $str)) {
                              		return TRUE;
                              	} else {
                              		return FALSE;
                              	}
                              }
                              /**
                               * Function to test whether a string has a whitespace char -- i.e., any kind of whitespace or invisible separator. Should work with UTF-8 strings with all kinds of weird chars.
                               * @param string $str The string to be tested
                               * @return boolean TRUE if the string contains a space char.
                               */
                              public static function string_has_whitespace($str) {
                              	if (preg_match("/\p{Z}+/u", $str)) {
                              		return TRUE;
                              	} else {
                              		return FALSE;
                              	}
                              }
                              

                                I will have to give this thread a full read-through when I have more time as it looks like it contains a lot of interesting and useful information.

                                Passwords is sort of a hobby/interest of mine. I really enjoy reading articles and publications about authentication and all-things password. From my experience and from what I have read, it's generally considered a detriment to actively restrict users on their password creation, aside from some very simple things (such as the password cannot contain your user name or email address - that sort of thing). Again, from my experience and from what I have read it's considered better to provide the user with a strength meter of some kind (which appears to be the bulk of the discussion in this thread) to let them know their password of Password123 is not secure.

                                Basically, if you make it too difficult to create an account, users are going to go somewhere else. Of course, this train of thought doesn't really apply to a workplace or educational institution where users can't really "go somewhere else" (at least not easily :p); however, if a user cannot go somewhere else, they're going to get frustrated and enter the simplest password possible to pass the verification to move on, which usually means the password is insecure. A lot of what I have read seems to come to a consensus that it's the password itself that's the problem.

                                Also arbitrary restrictions are just plain annoying; I think we can all relate to at least one situation where we have tried to sign up somewhere only to have the site or service come back to us saying our password isn't secure enough or doesn't match their random restrictions (restricting certain characters or length are by far the biggest offenders).

                                As a small side note, personally, I think preventing repeating characters is completely pointless and doesn't add any real security; it's just another arbitrary restriction put in place to give the illusion of security. To me it adds the same amount of "security" as the mentality of "Put a 1 at the end of your password to make it better". Indeed, on paper "password1" seems more secure than "password" but in reality anyone trying to guess or crack the password wouldn't even notice.

                                  Bonesnap;11044899 wrote:

                                  I will have to give this thread a full read-through when I have more time as it looks like it contains a lot of interesting and useful information.

                                  Yes I hope it's useful. It's the UTF8/charset stuff that mostly concerns me. I'm thinking that to be world-class these days, you need to be able to handle any language. I'm also thinking this dramatically expands the password space to allow non-ASCII chars.

                                  Bonesnap;11044899 wrote:

                                  Passwords is sort of a hobby/interest of mine. I really enjoy reading articles and publications about authentication and all-things password. From my experience and from what I have read, it's generally considered a detriment to actively restrict users on their password creation, aside from some very simple things (such as the password cannot contain your user name or email address - that sort of thing).

                                  Surely you've seen this XKCD comic?

                                  Bonesnap;11044899 wrote:

                                  Again, from my experience and from what I have read it's considered better to provide the user with a strength meter of some kind (which appears to be the bulk of the discussion in this thread) to let them know their password of Password123 is not secure.

                                  Sadly, the logic I've been working on would not permit correcthorsebatterystaple as a password. I might consider modifying it to relax the rules if the passphrase exceeds a certain length. I have actually considered comparing the password supplied to a dictionary too. I also like those little password strength meters although we must admit that they do restrict the space permitted for a password -- is that really improving security or making it worse?

                                  Bonesnap;11044899 wrote:

                                  Basically, if you make it too difficult to create an account, users are going to go somewhere else...they're going to get frustrated and enter the simplest password possible to pass the verification to move on, which usually means the password is insecure.

                                  Agreed and I've done precisely this myself with the aforementioned financial institution. They didn't offer a strength meter and did not permit a commonly used punctuation char. I believe the preg commands I've come up with are pretty cool in that they allow non-English punctuation, non-english numerals, and check for non-English uppercase and lowercase. The checks they perform seem to implement the OWASP restrictions for your usual english/ASCII chars but also permit a much wider range of international chars (chinese numbers and punctuation, all kinds of utf-8 glyphs and chars). I'll post them when I get some time

                                  Bonesnap;11044899 wrote:

                                  A lot of what I have read seems to come to a consensus that it's the password itself that's the problem.

                                  Could you clarify that statement? I suspect there's something important behind it.

                                  Bonesnap;11044899 wrote:

                                  Also arbitrary restrictions are just plain annoying; I think we can all relate to at least one situation where we have tried to sign up somewhere only to have the site or service come back to us saying our password isn't secure enough or doesn't match their random restrictions (restricting certain characters or length are by far the biggest offenders).

                                  This is true. The OWASP recommendations are pretty clear about providing clear and immediate feedback to your users in the form of specific instructions, lists of punctuation, etc.

                                  Bonesnap;11044899 wrote:

                                  As a small side note, personally, I think preventing repeating characters is completely pointless and doesn't add any real security; it's just another arbitrary restriction put in place to give the illusion of security.

                                  I agree with this. I think the reason for the restriction is to prevent users from just typing aaaaaaaa as their password. A user may not care very much about the account on your site (I join a lot of forum sites to ask one question or two and never return) but it's a nuisance to have careless people register on your site and leave their abandoned accounts vulnerable to crackers and spammers.

                                    Bonesnap;11044899 wrote:

                                    Also arbitrary restrictions are just plain annoying; I think we can all relate to at least one situation where we have tried to sign up somewhere only to have the site or service come back to us saying our password isn't secure enough or doesn't match their random restrictions (restricting certain characters or length are by far the biggest offenders).

                                    Indeed! Recently I've come across a whole bunch of sites that require me to have at least one
                                    - lower case
                                    - upper case
                                    - digit
                                    - [ 6-7 "special" characters ]

                                    while one or more restrictions are in place:
                                    - not allowing any other than those 6-7 "special" characters
                                    - max lenght of N chars
                                    - not having part of the password being some recognizable word they dislike
                                    - cannot have used the password previously this year

                                    I encountered the first of these places a few years ago, and the restrictions are for Apple ID. Because I had no strategy for building passwords that met these requirements, I tried making something up on the spot, with limited success. Over the last couple of years, I have had to reset my Apple ID password roughly 9 times out of 10. And even now that I have a strategy for dealing with "must contain 1 uppercase letter", I instead run into the problem that the passwords I try to reset to have been ”used” (as in password was once set to this value, and then immediately used one single time and never again), preventing me from coming up with new rememberable passwords.

                                    I'm not sure if this is what you refer to as "the password itself being the problem", but in this case, they'd be better off simpliyfing the login process by forgoing password altogether and replace login with:
                                    - click here to get login link
                                    - go to email inbox
                                    - click link
                                    - logged in!
                                    Do note that this is pretty much what they have reduced their login process to when it comes to me. Except that I still have to enter a new password and then use it once to log in.

                                    Or possibly providing alternatives such as
                                    - send code to cell phone
                                    - require said code to be entered within X seconds

                                    • click to have image sent to email
                                    • show image in either phone or computer
                                    • use the other of phone/computer to webcam the image (or super long code)
                                    • be logged in

                                    Other than that, allowing users the entire realm of characters from some set such as utf-8 would be better. If restricting it, rather do it by requiring at least 16 characters or some such, and then advice users to create passwords such as "myflyingpinkhorseisamazing".

                                    But, not taking only xkcd.com as the source of best practices... 🙂
                                    perhaps we'd need some math geniuses to also consider what happens if everyone starts using dictionary combinations of length 5-10 words… If you can safely assume that the majority of users have such passwords, what would then happen to the number of combinations? Can it still be considered N characters from an alphabet of size 50, or should it instead be considered N characters (words) from an alphabet of size 30k (+ endings, variations etc).

                                    However, while restrictions are annoying, I can to some degree understand that restrictions are placed on passwords. In certain work-places, without any kind of restriction on the realm of acceptable passwords, you'd usually find a match for any one of [summer, coffee, spring, sun] (translated to english) among no more than 10-20 users. If you force these users to put a digit, a special character and an uppercase letter in there, at least there'd be a few more variations, such as ”Summer1!”, ”Summer-1” etc. Then again, perhaps this is not enough to warrant a restriction that makes others use fewer characters for their passwords. These new ”improved” passwords could still be brute-guessed quickly enough (a few days) even if you'd stick to max two fails per 15 minutes to avoid alarms and lock-outs.

                                    I also wonder what would happen with the xkcd-approach. Would there be enough bias in certain cultures, languages, countries etc that you could stick to trying "myhorseisamazing", "funwillnowcommence" or "asofyetinsufficientdata" or minor variations thereof, as long as you have sufficient knowledge of your target group?

                                    When it comes to security there is usuall PEBKAC in three places: implementation, application (password entry) and circumvention. But when it comes to circumvention, 'P' stands for ”Pro” - not ”Problem”.

                                      I definitely feel there's an arms race developing between crackers and password trends. Some say that the era of the password is over. I enjoyed your list of alternatives. Does anyone have any multi-factor authentication stories to share? I've often wanted to implement something more secure than just a password, but have never really gotten around to the sending-a-test-message approach. I did concoct a function some time ago that would use email to send someone a text message but it would only work on certain wireless providers.

                                        4 days later

                                        Just in case anyone is interested, a partial version of my Auth class. The methods here are for checking if a password meets the OWASP requirements and, if not, creating feedback which can be piped to a view somewhere. It assumes you have a language object to construct the prompts. I provide it not because I think it's a particularly well-designed or reusable class but because the functions that check for digits and punctuation should also work for foreign languages where digits are not just 0-9 and lowercase is not just a-z and uppercase is not just A-Z, etc.

                                        <?php
                                        /**
                                         * class as a starting point for user authentication functionality.
                                         */
                                        
                                        // this class uses functions that are only available in recent versions of php
                                        if (!function_exists("password_hash")){
                                        	throw new Exception("necessary functions don't exist!");
                                        }
                                        
                                        /**
                                         * Class to encapsulate Erepairable authentication-related functionality
                                         * @author jaith
                                         *
                                         */
                                        class Auth {
                                        	 * The character encoding scheme used for supplied passwords. Should be
                                        	 * a string compatible with PHP's mb_* functions (e.g., mb_strlen)
                                        	 * @var string
                                        	 */
                                        	const CHAR_ENCODING = "UTF-8";
                                        
                                        /**
                                         * User passphrases must contain at least this many chars
                                         * @var int
                                         */
                                        const MINIMUM_PASSPHRASE_LENGTH = 8;
                                        /**
                                         * User passphrases cannot be longer than this
                                         * @var int
                                         */
                                        const MAXIMUM_PASSPHRASE_LENGTH = 128;
                                        /**
                                         * A password must achieve this score to pass validation
                                         * @var int
                                         */
                                        const MINIMUM_SECURITY_SCORE = 3;
                                        
                                        /**
                                         * Array of strings containing feedback about most recently validated password
                                         * @var array
                                         */
                                        public $password_validation_errors;
                                        
                                        /**
                                         * Array of strings suggesting ways to improve password strength
                                         * @var array
                                         */
                                        public $password_suggestions;
                                        
                                        /**
                                         * Ranks the security of the most recently supplied password
                                         * @var int
                                         */
                                        public $password_security_score;
                                        
                                        // various booleans that get set when validating a password. might be useful for showing checkmarks in a UI or something
                                        public $password_long_enough;
                                        public $password_not_too_long;
                                        public $password_does_not_repeat;
                                        public $password_has_uppercase;
                                        public $password_has_lowercase;
                                        public $password_has_digit;
                                        public $password_has_special_char;
                                        
                                        
                                        
                                        /**
                                         * Given a hashed password, returns TRUE if that password needs to be updated with a more secure hashing technique.
                                         * Currently just a wrapper around PHP's password_needs_rehash function
                                         * @see http://php.net/password_needs_rehash
                                         * @param string $hashed_password
                                         * @return boolean
                                         */
                                        public static function password_needs_rehash($hashed_password) {
                                        	return password_needs_rehash($hashed_password, PASSWORD_DEFAULT);
                                        }
                                        
                                        /**
                                         * Hashes a cleartext password. Currently just a wrapper around PHP's password_hash function
                                         * @see http://php.net/password_hash
                                         * @param string $cleartext_password
                                         * @return Ambigous <string, false, mixed, NULL, boolean>
                                         */
                                        public static function hash_password($cleartext_password) {
                                        	return password_hash($cleartext_password, PASSWORD_DEFAULT);
                                        }
                                        
                                        
                                        /**
                                         * Function to test whether a string has uppercase letters. Should work with UTF-8 strings with all kinds of weird chars.
                                         * @param string $str The string to be tested
                                         * @return boolean TRUE if the string contains uppercase chars.
                                         */
                                        public static function string_has_uppercase_chars($str) {
                                        	if (preg_match("/\p{Lu}+/u", $str)) {
                                        		return TRUE;
                                        	} else {
                                        		return FALSE;
                                        	}
                                        }
                                        
                                        /**
                                         * Function to test whether a string has lowercase letters. Should work with UTF-8 strings with all kinds of weird chars.
                                         * @param string $str The string to be tested
                                         * @return boolean TRUE if the string contains lowercase chars.
                                         */
                                        public static function string_has_lowercase_chars($str) {
                                        	if (preg_match("/\p{Ll}+/u", $str)) {
                                        		return TRUE;
                                        	} else {
                                        		return FALSE;
                                        	}
                                        }
                                        
                                        /**
                                         * Function to test whether a string has a digit. Should work with UTF-8 strings with all kinds of weird chars.
                                         * @param string $str The string to be tested
                                         * @return boolean TRUE if the string contains a digit.
                                         */
                                        public static function string_has_digit($str) {
                                        	if (preg_match("/\p{N}+/u", $str)) {
                                        		return TRUE;
                                        	} else {
                                        		return FALSE;
                                        	}
                                        }
                                        
                                        /**
                                         * Function to test whether a char is repeated too much. Should work with UTF-8 strings with all kinds of weird chars.
                                         * @param string $str The string to be tested
                                         * @return boolean TRUE if a character is repeated too often (3 times as of now).
                                         */
                                        public static function string_repeats_char_too_much($str) {
                                        	if (preg_match('/(.)\1\1/u', $str)) {
                                        		return TRUE;
                                        	} else {
                                        		return FALSE;
                                        	}
                                        
                                        }
                                        
                                        /**
                                         * Function to test whether a string has a "special" char -- i.e., punctuation or white space. Should work with UTF-8 strings with all kinds of weird chars.
                                         * @param string $str The string to be tested
                                         * @return boolean TRUE if the string contains a "special" char.
                                         */
                                        public static function string_has_special_char($str) {
                                        	if (self::string_has_punctuation($str) || self::string_has_symbol($str) || self::string_has_whitespace($str)) {
                                        		return TRUE;
                                        	} else {
                                        		return FALSE;
                                        	}
                                        }
                                        
                                        /**
                                         * Function to test whether a string has a punctuation char. Should work with UTF-8 strings with all kinds of weird chars.
                                         * @param string $str The string to be tested
                                         * @return boolean TRUE if the string contains a punctuation char.
                                         */
                                        public static function string_has_punctuation($str) {
                                        	if (preg_match("/\p{P}+/u", $str)) {
                                        		return TRUE;
                                        	} else {
                                        		return FALSE;
                                        	}
                                        }
                                        /**
                                         * Function to test whether a string has a symbol char -- i.e., math, currency symbols, etc. Should work with UTF-8 strings with all kinds of weird chars.
                                         * @param string $str The string to be tested
                                         * @return boolean TRUE if the string contains a symbol char.
                                         */
                                        public static function string_has_symbol($str) {
                                        	if (preg_match("/\p{S}+/u", $str)) {
                                        		return TRUE;
                                        	} else {
                                        		return FALSE;
                                        	}
                                        }
                                        /**
                                         * Function to test whether a string has a whitespace char -- i.e., any kind of whitespace or invisible separator. Should work with UTF-8 strings with all kinds of weird chars.
                                         * @param string $str The string to be tested
                                         * @return boolean TRUE if the string contains a space char.
                                         */
                                        public static function string_has_whitespace($str) {
                                        	if (preg_match("/\p{Z}+/u", $str)) {
                                        		return TRUE;
                                        	} else {
                                        		return FALSE;
                                        	}
                                        }
                                        
                                        /**
                                         * Check if the string has one of the small list of special chars described in the OWASP recommendations.
                                         * @see https://www.owasp.org/index.php/Password_special_characters
                                         * @param string $str The string to be checked
                                         * @return boolean TRUE if the string contains one of the limited list of special chars
                                         */
                                        public static function string_has_limited_special_chars($str) {
                                        	// TODO: no idea if this is safe for checking utf-8 encoded strings?
                                        	if (strpbrk($str, " !\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~") !== FALSE) {
                                        		return TRUE;
                                        	} else {
                                        		return FALSE;
                                        	}  
                                        }
                                        
                                        /**
                                         * Validates the password provided according to OWASP recommendations and sets a variety of password related properties on this class to help provide user feedback.
                                         * @see https://www.owasp.org/index.php/Authentication_Cheat_Sheet
                                         * @param string $password
                                         * @param CI_Lang $lang CI language object. NOTE this object must have already loaded the appropriate language strings file!
                                         * @return boolean
                                         */
                                        public function validate_password($password, $lang) {
                                        	$this->password_security_score = 0;
                                        	$this->password_validation_errors = array();
                                        	$this->password_suggestions = array();
                                        	$this->password_long_enough = FALSE;
                                        	$this->password_not_too_long = FALSE;
                                        	$this->password_does_not_repeat = FALSE;
                                        	$this->password_has_uppercase = FALSE;
                                        	$this->password_has_lowercase = FALSE;
                                        	$this->password_has_digit = FALSE;
                                        	$this->password_has_special_char = FALSE;
                                        
                                        
                                        	$valid = TRUE; // we will set this to false if any problems are encountered
                                        
                                        	if (mb_strlen($password, self::CHAR_ENCODING) < self::MINIMUM_PASSPHRASE_LENGTH){
                                        		$valid = FALSE;
                                        		$this->password_validation_errors[] = sprintf($lang->line("auth_minimum_password_length"), self::MINIMUM_PASSPHRASE_LENGTH);
                                        	} else {
                                        		$this->password_long_enough = TRUE;
                                        	}
                                        
                                        	if (mb_strlen($password, self::CHAR_ENCODING) > self::MAXIMUM_PASSPHRASE_LENGTH){
                                        		$valid = FALSE;
                                        		$this->password_validation_errors[] = sprintf($lang->line("auth_maximum_password_length"), self::maxim);
                                        	} else {
                                        		$this->password_not_too_long = TRUE;
                                        	}
                                        
                                        	if (self::string_repeats_char_too_much($password)){
                                        		$valid = FALSE;
                                        		$this->password_validation_errors[] = $lang->line("auth_password_repeats");
                                        	} else {
                                        		$this->password_does_not_repeat = TRUE;
                                        	}
                                        
                                        	// evaluate password complexity according to OWASP's 4 rules
                                        	if (self::string_has_uppercase_chars($password)) {
                                        		$this->password_security_score++;
                                        		$this->password_has_uppercase = TRUE;
                                        	} else {
                                        		$this->password_suggestions[] = $lang->line("auth_suggest_uppercase");
                                        	}
                                        
                                        	if (self::string_has_lowercase_chars($password)) {
                                        		$this->password_security_score++;
                                        		$this->password_has_lowercase = TRUE;
                                        	} else {
                                        		$this->password_suggestions[] = $lang->line("auth_suggest_lowercase");
                                        	}
                                        
                                        	if (self::string_has_digit($password)) {
                                        		$this->password_security_score++;
                                        		$this->password_has_digit = TRUE;
                                        	} else {
                                        		$this->password_suggestions[] = $lang->line("auth_suggest_digits");
                                        	}
                                        
                                        	if (self::string_has_special_char($password)) {
                                        		$this->password_security_score++;
                                        		$this->password_has_special_char = TRUE;
                                        	} else {
                                        		$this->password_suggestions[] = $lang->line("auth_suggest_special_chars");
                                        	}
                                        
                                        	if ($this->password_security_score < self::MINIMUM_SECURITY_SCORE){
                                        		$valid = FALSE;
                                        		$this->password_validation_errors[] = "Your password is too predictable";
                                        	}
                                        
                                        
                                        	return $valid;
                                        
                                        } // validate_password()
                                        
                                        } // class Auth