filament icon indicating copy to clipboard operation
filament copied to clipboard

Password validation only showing 1 password strength error at a time

Open binaryfire opened this issue 7 months ago • 1 comments

Package

filament/filament

Package Version

4-alpha6

Laravel Version

12

Livewire Version

3

PHP Version

8.4

Problem description

Laravel's password validation is configured to return all validation errors at once:

https://github.com/laravel/framework/blob/bcfa7c32acc17a7e49892efc5040fcaae3f207da/src/Illuminate/Validation/Rules/Password.php#L322

Showing all the failures each time is important for UX. Let's say you have this password strength config in a service provider:

Password::defaults(function () {
    return Password::min(8)
                ->letters()
                ->numbers()
                ->symbols()
                ->mixedCase();
});

When a user tries to register with a password that doesn't meet these criteria, the expectation is that a message for every failed criteria is displayed. Currently Filament displays these errors one by one:

First try - length failure: Image

Second try - length corrected, mixed case failure: Image

Third try - mixed case corrected, symbol failure: Image

Fourth try - symbol corrected, number failure: Image

Also, each validation failure hits the rate limiter which adds to the UX issues: Image

Showing password requirements as a helper text or a hint help a little, but displaying the full list of errors for the currently entered password is the most important visual feedback. I considered using a notification but I think that would feel strange. All other platforms display the list of errors under the password field. That's the UX users have been trained to expect.

Expected behavior

All password field validation errors should be displayed at once.

Steps to reproduce

  1. Clone reproduction repo
  2. Visit /register page and try using a password with just letters to start with.

Reproduction repository (issue will be closed if this is not valid)

https://github.com/binaryfire/filament4-password-validation

Relevant log output


binaryfire avatar May 11 '25 05:05 binaryfire

This is how I'm working around it atm, in case it helps. Ideally the messages would be in an unordered list but validation error messages are escaped so I can't use HTML.

Image

<?php

declare(strict_types=1);

namespace Packages\Laravel\Auth\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Validation\Rules\Password;

class FilamentPasswordRule implements ValidationRule
{
    protected array $failedRules = [];
    protected Password $passwordRule;

    public function __construct()
    {
        $this->passwordRule = Password::default();
    }

    /**
     * Run the validation rule.
     *
     * @param  \Closure(string, ?string=): \Illuminate\Translation\PotentiallyTranslatedString  $fail
     */
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $this->failedRules = [];

        if (!is_string($value)) {
            $fail(__('The :attribute must be a string.', ['attribute' => $attribute]));
            return;
        }

        // Get the applied rules from the Password instance
        $appliedRules = $this->passwordRule->appliedRules();

        // Check minimum length
        if (strlen($value) < $appliedRules['min']) {
            $this->failedRules[] = 'min_length';
        }

        // Check mixed case
        if ($appliedRules['mixedCase'] && !preg_match('/(\p{Ll}+.*\p{Lu})|(\p{Lu}+.*\p{Ll})/u', $value)) {
            $this->failedRules[] = 'mixed_case';
        }

        // Check letters
        if ($appliedRules['letters'] && !preg_match('/\pL/u', $value)) {
            $this->failedRules[] = 'letters';
        }

        // Check symbols
        if ($appliedRules['symbols'] && !preg_match('/\p{Z}|\p{S}|\p{P}/u', $value)) {
            $this->failedRules[] = 'symbols';
        }

        // Check numbers
        if ($appliedRules['numbers'] && !preg_match('/\pN/u', $value)) {
            $this->failedRules[] = 'numbers';
        }

        // If there are failed rules, construct and return the error message
        if (!empty($this->failedRules)) {
            $fail($this->constructMessage());
        }
    }

    /**
     * Construct the failure message
     */
    protected function constructMessage(): string
    {
        $messageFragments = [];

        foreach ($this->failedRules as $failedRule) {
            switch ($failedRule) {
                case 'min_length':
                    $min = $this->passwordRule->appliedRules()['min'];
                    $messageFragments[] = __('at least :min characters', ['min' => $min]);
                    break;
                case 'mixed_case':
                    $messageFragments[] = __('at least one uppercase and one lowercase letter');
                    break;
                case 'letters':
                    $messageFragments[] = __('at least one letter');
                    break;
                case 'symbols':
                    $messageFragments[] = __('at least one symbol');
                    break;
                case 'numbers':
                    $messageFragments[] = __('at least one number');
                    break;
            }
        }

        // Construct the sentence
        if (count($messageFragments) === 1) {
            return __('The password must contain :requirement.', ['requirement' => $messageFragments[0]]);
        } elseif (count($messageFragments) === 2) {
            return __('The password must contain :first and :second.', [
                'first' => $messageFragments[0],
                'second' => $messageFragments[1]
            ]);
        } else {
            $lastMessageFragment = array_pop($messageFragments);
            return __('The password must contain :requirements, and :last.', [
                'requirements' => implode(', ', $messageFragments),
                'last' => $lastMessageFragment
            ]);
        }
    }
}

PS. It'd be nice if validation messages supported html. Is there any reason they need to be escaped? It's not user-generated data so I don't think there's any risk using {!! !!}. Here's a common type of use case: https://youtu.be/_k4zwJIUPH0?si=-JzjZ_L-UNm_zu-A&t=62

binaryfire avatar May 11 '25 07:05 binaryfire

@danharrin Now that https://github.com/filamentphp/filament/pull/16247 has been merged, I'll get on this

binaryfire avatar Jun 30 '25 09:06 binaryfire

Thanks, but I'll handle it if you can't work out a way to validate using the built-in Password rule instead of creating a custom one

danharrin avatar Jun 30 '25 09:06 danharrin

Closed by https://github.com/filamentphp/filament/pull/16741

binaryfire avatar Jul 08 '25 01:07 binaryfire