How to Generate a Secure Password with PHP

Alex

by Alex


In this tutorial we are going to implement a PHP class method to generate a random secure password.

The password will contain random letters, digits and special characters.
It will include at least a lowercase letter, an uppercase letter, a digit and a special character.

You can set the minimum and maximum length of the password when calling the class constructor.

You can also provide a list of strings that must be different from the password.
For example, you can provide the old password and the username, and make sure that the new password is different from them.

Using a class instead of a simple function makes it possible for you to easily extend it and improve it.
If you are not familiar with class inheritance, this is the tutorial for you.

PHP random password generator class

First of all, let’s create a class named passwordGenerator:

class passwordGenerator {
}

Next, let’s create some constants to hold the characters we want to include in the password.
It is a good idea to create separate constants for letters, digits and special characters.

Let’s also add a constant to define the maximum similarity percentage. We will use this value when verifying that the new password is not too similar to the strings provided by the caller.

class passwordGenerator {
  
  // Alphabetic letters, lowercase
  const LETTERS = 'abcdefghijklmnopqrstuvwxyz';
  
  // Digits
  const DIGITS = '0123456789';
  
  // Special characters
  const SPECIAL_CHARS = '!@#$%^&*()_+-={}[]|:;"<>,.?/';
  
  // The maximum similarity percentage
  const MAX_SIMILARITY_PERC = 20;
}

Here are our constants:

  • LETTERS contains all the alphabetic characters, lowercase.
  • DIGITS contains all the digits.
  • SPECIAL_CHARS contains a list of usable special characters. You can edit this list if you want to add more characters or exclude some of them.
  • MAX_SIMILARITY_PERC is the maximum similarity between the password and the strings provided by the caller, as a percentage. For example, setting it to 20 means that the similarity must be less than 20%.
    We will be using the similar_text() function to calculate this value.

We also need three class properties where to store the parameters passed by the caller: the minimum and maximum length of the password, and the list of strings that must be different from the password.

Let’s add them as well (note that all the code is inside the class):

// The password minimum length
private $minLength;
// The password maximum length
private $maxLength;
// The optional list of strings that must be different from the password
private $diffStrings;

All right, let’s move on to the class constructor.

The constructor takes three arguments: the minimum length, the maximum length, and a list of diff strings (short for: the strings that must be different from the password).

Each argument also a default value, with the last argument default value being an empty array:

public function __construct(int $minLength = 4, int $maxLength = 32, array $diffStrings = []) {
  $this->minLength = $minLength;
  $this->maxLength = $maxLength;
  $this->diffStrings = $diffStrings;
}

How to generate a random password: the generate() method

Now it’s finally time to create the password generator method.

Let’s call it generate().
Here’s the full implementation, followed by an explanation of the code:

public function generate(): string {
  // List of usable characters
  $chars = self::LETTERS . mb_strtoupper(self::LETTERS) . self::DIGITS . self::SPECIAL_CHARS;
  
  // Set to true when a valid password is generated
  $passwordReady = false;
  
  while (!$passwordReady) {
    // The password
    $password = '';
    
    // Password requirements
    $hasLowercase = false;
    $hasUppercase = false;
    $hasDigit = false;
    $hasSpecialChar = false;
    
    // A random password length
    $length = random_int($this->minLength, $this->maxLength);
    
    while ($length > 0) {
      $length--;
      
      // Choose a random character and add it to the password
      $index = random_int(0, mb_strlen($chars) - 1);
      $char = $chars[$index];
      $password .= $char;
      
      // Verify the requirements
      $hasLowercase = $hasLowercase || (mb_strpos(self::LETTERS, $char) !== false);
      $hasUppercase = $hasUppercase || (mb_strpos(mb_strtoupper(self::LETTERS), $char) !== false);
      $hasDigit = $hasDigit || (mb_strpos(self::DIGITS, $char) !== false);
      $hasSpecialChar = $hasSpecialChar || (mb_strpos(self::SPECIAL_CHARS, $char) !== false);
    }
    
    $passwordReady = ($hasLowercase && $hasUppercase && $hasDigit && $hasSpecialChar);
    
    // If the new password is valid, check for similarity
    if ($passwordReady) {
      foreach ($this->diffStrings as $string) {
        similar_text($password, $string, $similarityPerc);
        $passwordReady = $passwordReady && ($similarityPerc < self::MAX_SIMILARITY_PERC);
      }
    }
  }
  
  return $password;
}

Here’s what the generate() method does:

  • $char contains the full list of characters to be used in the password. The uppercase letters are created by using mb_strtoupper() on LETTERS.
  • A while loop continues until a password that meets all the requirements is found.
  • Inside the loop, there is a list of boolean variables that checks if the new password has at least one lowercase letter, one uppercase letter, a digit and a special character. They are false at the beginning of the loop, and they are set to true when a corresponding character is added to the password.
  • A random length, $length, is calculated.
  • Random characters from $char are added to the password until the length reaches $length.
  • If all the requirements are met, the method then checks for similarity with the strings in $diffStrings.
  • If this check passes as well then the password is returned.

To make sure the password meets the characters requirements, the method uses the mb_strpos() function on each added character. Note how you need to use strict comparison on the mb_strpos() return value.

To choose the random characters to add to the password, the method uses random_int(). This function is cryptographically secure so that the characters are actually random.

Finally, the method uses the similar_text() function to calculate the similarity between the password and the “diff strings”.

Here’s an example usage:

$passwordGenerator = new passwordGenerator(6, 10, ['old_password', 'myUsername']);
for ($i = 0; $i < 5; $i++) {
  echo htmlspecialchars($passwordGenerator->generate());
  echo '<br>';
}
Output:
7vj;[AU
3XrO2r_Y
2^Z)3{%UXw
rKcWjw.A1
0o!]^gnlU6

Conclusion

You can use this class method to generate random and secure passwords.
It can be handy when implementing a login system to suggest new passwords to users.

Remember: never store plain-text passwords on the database. Use hashes instead.

Feel free to edit this class by adding any new functionalities you may need.
If you have any questions just leave me a comment.

6 thoughts on “How to Generate a Secure Password with PHP”

  1. Yes, I’m using the for loop from your example.
    I’ve found the reason of the issue: in my case I saw 4 strings instead of 5 because of the presence of symbols, that “altered” the output.
    In other words, the 4th string was longer than 10 just because it was – on the screen – the strings 4th and 5th merged.
    Your class works correctly – but maybe those symbols should be removed from the SPECIAL_CHARS constant.

    • Thank you!

      My error was that I was not sanitizing the HTML output.
      I fixed that by using htmlspecialchars() on the generated passwords (you can see how in the for loop).

      Thank you again for your help.

  2. It doesn’t work correctly: passwords are longer than you want in $maxLength, as you can see in your own example.

      • This is what I got with ‘new passwordGenerator(6, 10, [‘old_password’, ‘myUsername’])’:

        D)Lr10Lyd
        5P!ujl
        A;r9l5HY7
        z1$2@yD{87:S3

        As you can see, the last pwd is longer than 10 (13).
        PHP 8.2.3

        • Thank you for your test.
          I’m on PHP 8.1.12 on this PC and I can’t reproduce the problem.

          Are you using the for loop from the example? Because I see 4 strings instead of 5. Maybe the last one, z1$2@yD{87:S3, is actually the last 2 strings together because for some reason the newline got lost?

Comments are closed.