Password value objects

 

Almost every web application has a login. While you could use  an identity provider and just skip it, in many cases this is not wanted by the website owner for specific reasons (users might not trust to log in on a different website or domain name). A password field stays quite common in web applications.

The definition of a strong password

Most websites force you to use a strong password, but every application has its own rules what a strong password is.
For example some want a 8 character minimum length, but no requirements that it should contain digits or upper case characters. An other project could have a 5 character minimum length, but forces you to contains digits, uppercase and special characters. Sometimes a blacklist is also added (no 'admin', 'Welcome123', etc.)
So in  nutshell a value object that represents a strong password would be a good call.

Custom strong password value object.

For the reasons mentioned above, it's easy to see that a strong password is a value object on application level with some shared logic. So we created a IsPasswordValueObject in the Apie library. We already have a pre-defined class inside Composer package apie/text-value-objects called Apie\TextValueObjects\StrongPassword, but it could be that the website requires different requirements for a strong password.

Let's show how StrongPassword looks:
use Apie\Core\ValueObjects\Interfaces\HasRegexValueObjectInterface;
use Apie\Core\ValueObjects\IsPasswordValueObject;

class StrongPassword implements HasRegexValueObjectInterface
{
    use IsPasswordValueObject;

    public static function getMinLength(): int
    {
        return 6;
    }

    public static function getMaxLength(): int
    {
        return 42;
    }

    public static function getAllowedSpecialCharacters(): string
    {
        return '!@#$%^&*()-_+.';
    }

    public static function getMinSpecialCharacters(): int
    {
        return 1;
    }

    public static function getMinDigits(): int
    {
        return 1;
    }

    public static function getMinLowercase(): int
    {
        return 1;
    }

    public static function getMinUppercase(): int
    {
        return 1;
    }

    protected function convert(string $input): string
    {
        return trim($input);
    }
}
It looks like a lot of static getters, but all these getters are used to determine the restrictions used on a password. So StrongPassword has these requirements:
  • spaces are trimmed at the start and end of the string to avoid copy, paste mistakes (happens more than you think)
  • Minimum length is 6 characters
  • Maximum length is 42 characters
  • There should be at least one upper case letter (A-Z)
  • There should be at least one lower case letter (a-z)
  • There should be at least one digit (0-9)
  • There should be at least one character of !@#$%^&*()-_+.
  • The StrongPassword class has a method to give me a regular expression with lookaheads that should match the entered password.
So if I want a different one I can just use this class as example and just modify the static getters to get my own restrictions.

Storing passwords

While having a password value object looks awesome, the problem is that Apie should not decide how to store them even if storing a password as plaintext is really, really bad. Ideally you want to only store a hash of the password and verify an entered password the moment a user fills in the password and tries to login. We have a value object for this! And it is called EncryptedPassword. The User created by the apie project starter is using this value object. EncryptedPassword can no longer decrypt back to the original password, but it can verify that the password entered is correct.
A user constructor and verify method should be something like this:

class User implements EntityInterface
{
    private EncryptedPassword $password;
    
    public function __construct(private Email $email, StrongPassword $password)
    {
        $this->email = $email;
        $this->password = EncryptedPassword::fromUnencryptedPassword($password);
    }
    
    public function verify(string $passwordEntered): bool
    {
        return $this->password->verifyUnencryptedPassword($passwordEntered);
    }
}

Apie integration

Password value objects have a very predictable way of working with a fromNative() to convert a string into a value object and a toNative() to convert back to string.
Even though they were designed for Apie applications, they can be used standalone in any framework or library. Make sure you do not have a getPassword() method or you will add a column value search on the swagger UI. I'm actually considering writing a phpstan check for it to avoid mistakes in accidentally returning (encrypted) passwords.

apie/faker

The apie faker generates random passwords with the restrictions you applied. Right now it uses Fakerphp randomElement() and a shuffle(), which are not cryptographically secure, but it's in my planning to use cryptographically secure methods in the first place.
So to generate a random password, you just need to ask the faker to fake the class StrongPassword to get a random password.

$faker->fakeClass(StrongPassword::class);

apie/rest-api

The apie rest api will display a password field in OpenAPI as a string and provides a regular expression for valid passwords. This regular expression contains look-aheads, so it is possible Swagger UI provides an example that does not follow as Swagger UI ignores look-aheads. Still you will get a 422 validation error response if you submit a password that does not match the requirements.
Swagger UI displays a random password by looking at the regular expression.

If you do have a getPassword(), most data layers will allow you to filter on password fields, even though it's a weird case. The only use case I can imagine is cases where the server side generates a password and (temporarily) returns a generated password.

Oh yeah, and passwords are never indexed for text search.

apie/cms

apie/cms has a special Password input field that will also display the password requirements that needs to be done to enter a valid password. Input fields could have a pattern attribute but not all browser support lookahead regular expressions, so I decided not to add it.

I entered '12a' as password here.

On validation errors the field is never filled in again. At the time of the writing the correct validation message is not done yet, so it will mention the text you actually entered in the validation error. 
I'm actually considering make use of the SensitiveParameter attribute introduced in PHP 8.2 to make it more general (so adding SensitiveParameter attribute on an argument will make it empty the input field if a validation error occurred) 

Conclusion

My library Apie is still in development, but the StrongPassword value objects can already be used in any application.


Comments