Search code examples
c#asp.net-coreblazordata-annotations

Blazor DataAnnotations Compare validation conflicts with browser autocomplete


I have a Blazor server app that has a form with two password inputs, "new password" and "confirm password". I'm using DataAnnotations Compare attribute to validate that the two match. I've also tried the CompareProperty attribute from the experimental NuGet package Microsoft.AspNetCore.Components.DataAnnotations.Validation, but the behavior is the same.

The sample page below is able to reproduce the error in Chrome and Edge. Testing steps:

  1. Generate a random password using the browser password generation tool (in Chrome, right click -> Suggest password...). Both password fields should be auto filled in with the generated password.
  2. In the "new password" field, add one character at the end. This updates the value in "confirm password" as well, so the values do match, but a validation failure is triggered.
  3. To remove the validation failure, delete the last character in "confirm password", tab out of the field, then go back to the field and add the same character back in. This time, the validation is successful.

Just from personal experience, I sometimes add extra characters to the end of passwords suggested by Chrome, mostly to satisfy complexity requirements the generated password didn't meet. So this form behavior will be problematic if my users do the same. Anyone know what's going on here and how to solve it?

Index.razor:

@page "/"
@using System.ComponentModel.DataAnnotations;

<PageTitle>Index</PageTitle>

<EditForm name="passwordForm" Model="MyModel" OnSubmit="@SubmitPasswordForm">
    <DataAnnotationsValidator />
    <div class="form-group">
        <label for="NewPassword">New Password</label>
        <InputText @bind-Value="MyModel.NewPassword" class="form-control" id="NewPassword" name="NewPassword" type="password" autocomplete="new-password"></InputText>
        <ValidationMessage For="@(() => MyModel.NewPassword)" />
    </div>
    <div class="form-group">
        <label for="ConfirmNewPassword">Confirm Password</label>
        <InputText @bind-Value="MyModel.ConfirmPassword" class="form-control" id="ConfirmNewPassword" name="ConfirmNewPassword" type="password" autocomplete="new-password"></InputText>
        <ValidationMessage For="@(() => MyModel.ConfirmPassword)" />
    </div>

    <input id="submitResetPassword" type="submit" class="btn btn-default" value="Reset Password" />
</EditForm>

@code {
    private Model MyModel { get; set; } = new();

    private void SubmitPasswordForm()
    {
        // Do something with the password
    }

    private class Model
    {
        [Required]
        public string? NewPassword { get; set; }
        [Required]
        [Compare(nameof(NewPassword))]
        // or [CompareProperty(nameof(NewPassword))]
        public string? ConfirmPassword { get; set; }
    }
}

Solution

  • I tried to reproduce your issue: enter image description here

    trigger validate onblur may solve your issue:

        @using System.ComponentModel.DataAnnotations;
    
    <EditForm name="passwordForm" EditContext="@EC" OnSubmit="@SubmitPasswordForm">
        <DataAnnotationsValidator />
        <div class="form-group">
            <label for="NewPassword">New Password</label>
            <InputText @bind-Value="MyModel.NewPassword" class="form-control" id="NewPassword" name="NewPassword" type="password" autocomplete="new-password" onblur="@OnblurHandler"></InputText>
            <ValidationMessage For="@(() => MyModel.NewPassword)" />
        </div>
        <div class="form-group">
            <label for="ConfirmNewPassword">Confirm Password</label>
            <InputText @bind-Value="MyModel.ConfirmPassword" class="form-control" id="ConfirmNewPassword" name="ConfirmNewPassword" type="password" autocomplete="new-password" ></InputText>
            <ValidationMessage For="@(() => MyModel.ConfirmPassword)" />
        </div>
    
        <input id="submitResetPassword" type="submit" class="btn btn-default" value="Reset Password" />
    </EditForm>
    
    @code {
        private Model MyModel { get; set; } = new();
    
        private void SubmitPasswordForm()
        {
            // Do something with the password
        }
        protected override void OnInitialized()
        {
            EC = new EditContext(MyModel);
            base.OnInitialized();
        }
    
        private void OnblurHandler()
        {
            EC.Validate(); // manually trigger the validation here
        }
    
        private EditContext EC { get; set; }
    
        private class Model
        {
            [Required]
            public string? NewPassword { get; set; }
            [Required]
            [Compare(nameof(NewPassword))]
            // or [CompareProperty(nameof(NewPassword))]
            public string? ConfirmPassword { get; set; }
        }
    }
    

    Result: enter image description here