ASP.NET Core 2.2 - Identity Model Validation


Ken Haggerty
Created 03/21/2019 - Updated 11/17/2019 23:54

This article will cover model validation for ASP.NET Core Identity. I will assume you have created a new ASP.NET Core 2.2 Razor Pages project with Individual User Accounts, updated the database with the CreateIdentitySchema migration and scaffolded the Identity UI.

Let's start with the InputModel in Register.cshtml.cs.

Default Register.cshtml.cs > InputModel:
public class InputModel
{
    [Required]
    [EmailAddress]
    [Display(Name = "Email")]
    public string Email { get; set; }

    [Required]
    [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Display(Name = "Confirm password")]
    [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
    public string ConfirmPassword { get; set; }
}

If you have applied the recommendations from Require A Confirmed Email and Password Requirements, your model will have these properties and attributes.

Register.cshtml.cs > InputModel:
public class InputModel
{
    [Required]
    [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
    [Display(Name = "Login Name")]
    public string UserName { get; set; }

    [Required]
    [EmailAddress]
    [Display(Name = "Email")]
    public string Email { get; set; }

    [Required]
    [RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W]).{8,}$", ErrorMessage = "The {0} does not meet requirements.")]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Display(Name = "Confirm password")]
    [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
    public string ConfirmPassword { get; set; }
}

I will briefly discuss the UserName property but will focus on the Email property. The Password Requirements article covers the Password property and RegularExpression attribute.

The default allowed characters for the UserName in Identity are:

abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789-._@+

I use this RegularExpression attribute.

[RegularExpression(@"[a-zA-Z0-9._@+-]{6,100}",
                ErrorMessage = "The {0} must be 6 to 100 valid characters which are any digit, any letter and -._@+.")]
Section references:

The EmailAddress attribute doesn't do much more than check for an @ symbol. The address a@b will pass validation.

Register.cshtml.cs > InputModel:
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; }

I agree with What is a valid email address?, I don't want to tell anyone I think their email is invalid and the way to validate an email is to send a confirmation email. I don't want to attempt to send a confirmation email if the address is obviously a non deliverable. MS docs has a reg ex method which does a much better job than the EmailAddress attribute.

public static bool IsValidEmail(string email)
{
    if (string.IsNullOrWhiteSpace(email))
        return false;

    try
    {
        // Normalize the domain
        email = Regex.Replace(email, @"(@)(.+)$", DomainMapper,
                            RegexOptions.None, TimeSpan.FromMilliseconds(200));

        // Examines the domain part of the email and normalizes it.
        string DomainMapper(Match match)
        {
            // Use IdnMapping class to convert Unicode domain names.
            var idn = new IdnMapping();

            // Pull out and process domain name (throws ArgumentException on invalid)
            var domainName = idn.GetAscii(match.Groups[2].Value);

            return match.Groups[1].Value + domainName;
        }
    }
    catch (RegexMatchTimeoutException e)
    {
        return false;
    }
    catch (ArgumentException e)
    {
        return false;
    }

    try
    {
        return Regex.IsMatch(email,
                @"^(?("")("".+?(?<!\\)""@)|(([0-9a-z]((\.(?!\.))|[-!#\$%&'\*\+/=\?\^`\{\}\|~\w])*)(?<=[0-9a-z])@))" +
                @"(?(\[)(\[(\d{1,3}\.){3}\d{1,3}\])|(([0-9a-z][-0-9a-z]*[0-9a-z]*\.)+[a-z0-9][\-a-z0-9]{0,22}[a-z0-9]))$",
                RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(250));
    }
    catch (RegexMatchTimeoutException)
    {
        return false;
    }
}
Section references:

Remove the EmailAddress attribute from the Email property. The html input type will be rendered as text not email.

With EmailAddress attribute:
<input class="form-control" type="email" data-val="true"
       data-val-email="The Email field is not a valid e-mail address."
       data-val-required="The Email field is required."
       id="Input_Email" name="Input.Email" value="">
Without EmailAddress attribute:
<input class="form-control" type="text" data-val="true"
       data-val-required="The Email field is required."
       id="Input_Email" name="Input.Email" value="">

Create a static RegexUtilities class in the Services folder and add the IsValidEmail method. This is server side validation, but you can return a Model error.

Register.cshtml.cs > OnPostAsync, after if (ModelState.IsValid):
if (!RegexUtilities.IsValidEmail(Input.Email))
{
    ModelState.AddModelError(string.Empty, new IdentityErrorDescriber().InvalidEmail(Input.Email).Description);
    return Page();
}

Remove the EmailAddress attribute and add the IsValidEmail check to ExternalLogin, ForgotPassword and ResetPassword in the Account folder. Also ChangeEmail and UnconfirmedEmail if you implemented Require A Confirmed Email.

I developed a comparison test with a list of emails from three articles which should pass or fail.

Section references:
Edit Index.cshtml.cs > IndexModel:
public IList<string> AttributeShouldPass { get; set; }
public IList<string> AttributeShouldFail { get; set; }
public IList<string> RegexShouldPass { get; set; }
public IList<string> RegexShouldFail { get; set; }

public void OnGet()
{
    var emailsToPass = new string[] { "david.jones@proseware.com", "d.j@server1.proseware.com",
        "jones@ms1.proseware.com", "j@proseware.com9", "js#internal@proseware.com",
        "j_9@[129.126.118.1]", "j@proseware.com9", "js#internal@proseware.com",
        "j_9@[129.126.118.1]", "js@proseware.com9", "j.s@server1.proseware.com",
        "\"j\\\"s\\\"\"@proseware.com", "email@example.com",
        "firstname.lastname@example.com", "email@subdomain.example.com",
        "firstname+lastname@example.com", "email@123.123.123.123",
        "email@[123.123.123.123]", "\"email\"@example.com", "1234567890@example.com",
        "email@example-one.com", "_______@example.com", "email@example.name",
        "email@example.museum", "email@example.co.jp", "firstname-lastname@example.com",
        "firstname_lastname@example.com", "test@test.com", "test@test.co.uk", "test+t@test.co.uk" };

    var emailsToFail = new string[] { "a@b", "j.@server1.proseware.com", "j..s@proseware.com",
        "js*@proseware.com", "js@proseware..com","plainaddress", "#@%^%#$@#$@#.com",
        "@example.com", "Joe Smith <email@example.com>", "email.example.com",
        "email @example@example.com", ".email@example.com", "email.@example.com",
        "email..email@example.com", "email@example.com(Joe Smith)","email @example",
        "email@-example.com","email @example.web","email@111.222.333.44444",
        "email@example..com","Abc..123@example.com", "test", "test.com", "test@test", "test@test.",
        ".something@example.com", "something.@example.com", "somebody..something@example.com",
        "something@example..com" };

    var emailAttribute = new EmailAddressAttribute();

    AttributeShouldPass = new List<string>();
    foreach (var email in emailsToPass)
    {
        if (!emailAttribute.IsValid(email))
        AttributeShouldPass.Add(email);
    }

    AttributeShouldFail = new List<string>();
    foreach (var email in emailsToFail)
    {
        if (emailAttribute.IsValid(email))
        AttributeShouldFail.Add(email);
    }

    RegexShouldPass = new List<string>();
    foreach (var email in emailsToPass)
    {
        if (!RegexUtilities.IsValidEmail(email))
        RegexShouldPass.Add(email);
    }

    RegexShouldFail = new List<string>();
    foreach (var email in emailsToFail)
    {
        if (RegexUtilities.IsValidEmail(email))
        RegexShouldFail.Add(email);
    }
}
Add to Index.cshtml:
<div class="row">
    <div class="col-12 text-center">
        <h2>Should Pass</h2>
    </div>
</div>
<div class="row">
    <div class="col-6">
        @{ int count = Model.AttributeShouldPass.Count();
        <h4>Attribute - Count = @count</h4>
        for (var i = 0; i < count; i++)
        {
        <h6>@Model.AttributeShouldPass[i]</h6>
        }
        }
    </div>
    <div class="col-6">
        @{ count = Model.RegexShouldPass.Count();
        <h4>RegEx - Count = @count</h4>
        @for (var i = 0; i < count; i++)
        {
        <h6>@Model.RegexShouldPass[i]</h6>
        }
        }
    </div>
</div>
<hr />
<div class="row">
    <div class="col-12 text-center">
        <h2>Should Fail</h2>
    </div>
</div>
<div class="row">
    <div class="col-6">
        @{ count = Model.AttributeShouldFail.Count();
        <h4>Attribute - Count = @count</h4>
        @for (var i = 0; i < count; i++)
        {
        <h6>@Model.AttributeShouldFail[i]</h6>
        }
        }
    </div>
    <div class="col-6">
        @{ count = Model.RegexShouldFail.Count();
        <h4>RegEx - Count = @count</h4>
        @for (var i = 0; i < count; i++)
        {
        <h6>@Model.RegexShouldFail[i]</h6>
        }
        }
    </div>
</div>

Here are the test results.

Email Pass Fail Lists

An agree to terms of service checkbox with validation works for server model validation but is not included in the client-side jquery-validate. Of course, you should add a link to a Terms of Service page.

Edit Register.cshtml.cs > InputModel, add TOSAgree:
[Range(typeof(bool), "true", "true", ErrorMessage = "You must accept the Terms of Service.")]
public bool TOSAgree { get; set; }
Edit Register.cshtml, add TOSAgree checkbox:
<div class="form-group">
    <div class="custom-control custom-checkbox">
        <input class="custom-control-input" id="Input_TOSAgree" asp-for="Input.TOSAgree" />
        <label class="custom-control-label" for="Input_TOSAgree">
            I accept the Terms of Service.
        </label>
    </div>
    <span asp-validation-for="Input.TOSAgree" class="text-danger"></span>
</div>

This must make a trip to the server to display the validation message. You can extend the range validator in jquery-validate.

Edit Register.cshtml.cs, add Script below <partial name="_ValidationScriptsPartial" />:
<script>
    // extend range validator method to treat checkboxes differently
    var defaultRangeValidator = $.validator.methods.range;
    $.validator.methods.range = function (value, element, param) {
        if (element.type === 'checkbox') {
            // if it's a checkbox return true if it is checked
            return element.checked;
        } else {
            // otherwise run the default validation function
            return defaultRangeValidator.call(this, value, element, param);
        }
    }
</script>
Section references:
Update 11/17/2019

I added a couple of spaces to the list of default allowed characters for proper line break/wrapping on small screens.



Comment Count = 0

Please log in to comment or follow.

Login Register
Follow to get web notifications when new comments are posted to this article.
Logged in users receive web notifications for new articles, topics and assets.
Web Notifications