PacktPublishing / ASP.NET-Core-2-and-Angular-5

ASP.NET Core 2 and Angular 5, published by Packt
MIT License
79 stars 75 forks source link

Validation of the registration form #11

Closed Belka383 closed 6 years ago

Belka383 commented 6 years ago

A more logical solution to some problems (errors during registration) can be solved by adding checks during registration. It is necessary somehow to validate the given parameters (in file Startup.cs):

opts.Password.RequireDigit = true; opts.Password.RequireLowercase = true; opts.Password.RequireUppercase = true; opts.Password.RequireNonAlphanumeric = false; opts.Password.RequiredLength = 7;

Darkseal commented 6 years ago

You're absolutely right, I should've put some client-side and server-side checks for the password in case of new users.

The funny thing about that is that I published a password validating class for ASP.NET (which also works in .NET Core) in November 2017 which would've been the perfect way to achieve such outcome, at least from the server-side... too bad it didn't make it through the book.

However, maybe I'm not too late to add it to the _UpdatedProject in GitHub... In fact, I actually just did that!

Here's the relevant code if you don't want to just pull it out.

1) Add the following PasswordCheck.cs class file in your project's /Data/ folder:

using System;
using System.Linq;
using Microsoft.AspNetCore.Identity;

namespace TestMakerFreeWebApp.Data
{
    public enum PasswordStrength
    {
        /// <summary>
        /// Blank Password (empty and/or space chars only)
        /// </summary>
        Blank = 0,
        /// <summary>
        /// Either too short (less than 5 chars), one-case letters only or digits only
        /// </summary>
        VeryWeak = 1,
        /// <summary>
        /// At least 5 characters, one strong condition met (>= 8 chars with 1 or more UC letters, LC letters, digits & special chars)
        /// </summary>
        Weak = 2,
        /// <summary>
        /// At least 5 characters, two strong conditions met (>= 8 chars with 1 or more UC letters, LC letters, digits & special chars)
        /// </summary>
        Medium = 3,
        /// <summary>
        /// At least 8 characters, three strong conditions met (>= 8 chars with 1 or more UC letters, LC letters, digits & special chars)
        /// </summary>
        Strong = 4,
        /// <summary>
        /// At least 8 characters, all strong conditions met (>= 8 chars with 1 or more UC letters, LC letters, digits & special chars)
        /// </summary>
        VeryStrong = 5
    }

    public static class PasswordCheck
    {
        /// <summary>
        /// Generic method to retrieve password strength: use this for general purpose scenarios, 
        /// i.e. when you don't have a strict policy to follow.
        /// </summary>
        /// <param name="password"></param>
        /// <returns></returns>
        public static PasswordStrength GetPasswordStrength(string password)
        {
            int score = 0;
            if (String.IsNullOrEmpty(password) || String.IsNullOrEmpty(password.Trim())) return PasswordStrength.Blank;
            if (!HasMinimumLength(password, 5)) return PasswordStrength.VeryWeak;
            if (HasMinimumLength(password, 8)) score++;
            if (HasUpperCaseLetter(password) && HasLowerCaseLetter(password)) score++;
            if (HasDigit(password)) score++;
            if (HasSpecialChar(password)) score++;
            return (PasswordStrength)score;
        }

        /// <summary>
        /// Sample password policy implementation:
        /// - minimum 8 characters
        /// - at lease one UC letter
        /// - at least one LC letter
        /// - at least one non-letter char (digit OR special char)
        /// </summary>
        public static bool IsStrongPassword(string password)
        {
            return HasMinimumLength(password, 8)
                && HasUpperCaseLetter(password)
                && HasLowerCaseLetter(password)
                && (HasDigit(password) || HasSpecialChar(password));
        }

        /// <summary>
        /// Sample password policy implementation following the Microsoft.AspNetCore.Identity.PasswordOptions standard.
        /// </summary>
        public static bool IsValidPassword(string password, PasswordOptions opts)
        {
            return IsValidPassword(
                password,
                opts.RequiredLength,
                opts.RequiredUniqueChars,
                opts.RequireNonAlphanumeric,
                opts.RequireLowercase,
                opts.RequireUppercase,
                opts.RequireDigit);
        }

        /// <summary>
        /// Sample password policy implementation following the Microsoft.AspNetCore.Identity.PasswordOptions standard.
        /// </summary>
        public static bool IsValidPassword(
            string password,
            int requiredLength,
            int requiredUniqueChars,
            bool requireNonAlphanumeric,
            bool requireLowercase,
            bool requireUppercase,
            bool requireDigit)
        {
            if (!HasMinimumLength(password, requiredLength)) return false;
            if (!HasMinimumUniqueChars(password, requiredUniqueChars)) return false;
            if (requireNonAlphanumeric && !HasSpecialChar(password)) return false;
            if (requireLowercase && !HasLowerCaseLetter(password)) return false;
            if (requireUppercase && !HasUpperCaseLetter(password)) return false;
            if (requireDigit && !HasDigit(password)) return false;
            return true;
        }

        #region Helper Methods

        public static bool HasMinimumLength(string password, int minLength)
        {
            return password.Length >= minLength;
        }

        public static bool HasMinimumUniqueChars(string password, int minUniqueChars)
        {
            return password.Distinct().Count() >= minUniqueChars;
        }

        /// <summary>
        /// Returns TRUE if the password has at least one digit
        /// </summary>
        public static bool HasDigit(string password)
        {
            return password.Any(c => char.IsDigit(c));
        }

        /// <summary>
        /// Returns TRUE if the password has at least one special character
        /// </summary>
        public static bool HasSpecialChar(string password)
        {
            // return password.Any(c => char.IsPunctuation(c)) || password.Any(c => char.IsSeparator(c)) || password.Any(c => char.IsSymbol(c));
            return password.IndexOfAny("!@#$%^&*?_~-£().,".ToCharArray()) != -1;
        }

        /// <summary>
        /// Returns TRUE if the password has at least one uppercase letter
        /// </summary>
        public static bool HasUpperCaseLetter(string password)
        {
            return password.Any(c => char.IsUpper(c));
        }

        /// <summary>
        /// Returns TRUE if the password has at least one lowercase letter
        /// </summary>
        public static bool HasLowerCaseLetter(string password)
        {
            return password.Any(c => char.IsLower(c));
        }
        #endregion
    }
}

2) Implement it within the /Controllers/UserController.cs file in the following way (last 2 lines of code added, plus comments):

/// <summary>
/// PUT: api/user
/// </summary>
/// <returns>Creates a new User and return it accordingly.</returns>
[HttpPut()]
public async Task<IActionResult> Put([FromBody]UserViewModel model)
{
    // return a generic HTTP Status 500 (Server Error)
    // if the client payload is invalid.
    if (model == null) return new StatusCodeResult(500);

    // check if the Username/Email already exists
    ApplicationUser user = await UserManager.FindByNameAsync(model.UserName);
    if (user != null) return BadRequest("Username already exists");

    user = await UserManager.FindByEmailAsync(model.Email);
    if (user != null) return BadRequest("Email already exists.");

    // added in 2018.01.06 to fix GitHub issue #11
    // ref.: https://github.com/PacktPublishing/ASP.NET-Core-2-and-Angular-5/issues/11
    if (!PasswordCheck.IsValidPassword(model.Password, UserManager.Options.Password)) 
        return BadRequest("Password is too weak.");

    [...]

3) Update the onSubmit() method in the /app/components/user/register-component.ts file to show the password check outcome to the client in the following way (few lines of code added near the end):

onSubmit() {
    // build a temporary user object from form values
    var tempUser = <User>{};
    tempUser.Username = this.form.value.Username;
    tempUser.Email = this.form.value.Email;
    tempUser.Password = this.form.value.Password;
    tempUser.DisplayName = this.form.value.DisplayName;

    var url = this.baseUrl + "api/user";

    this.http
        .put<User>(url, tempUser)
        .subscribe(res => {
            if (res) {
                var v = res;
                console.log("User " + v.Username + " has been created.");
                // redirect to login page
                this.router.navigate(["login"]);
            }
            else {
                // registration failed
                this.form.setErrors({
                    "register": "User registration failed."
                });
            }
        }, error => {
            console.log(error);
            // added in 2018.01.06 to fix GitHub issue #11
            // ref.: https://github.com/PacktPublishing/ASP.NET-Core-2-and-Angular-5/issues/11
            this.getFormControl("Password").setErrors({
                "Password": true
            });
        });
}

I just tested it and it seems to work decently fine:

password-check

Needless to say, this is a server-side only approach, which gets the job done in the most secure way but it's probably not the most efficient way to deal with the issue: the best thing to do would be to put it beside a client-side check: I'll leave that to the reader :)

ADDITIONAL NOTE: If you want to show the actual error instead of the generic "Please insert a valid password", you can dinamically replace that text with the error.error variable who contains the relevant server-side error text.

Belka383 commented 6 years ago

@Darkseal Sorry, this error occurs. What could be the problem? image

Belka383 commented 6 years ago

image

Darkseal commented 6 years ago

Hello @Belka383 , first of all, thank you for all the efforts you're putting into this project!

Regarding your specific issue, you shouldn't experience such compiler error because you should have "strictNullChecks": false in your tsconfig.json file: however, you should be able to fix it by putting an exclamation mark in the following way:

.getFormControl("Password")!.setErrors({

I cannot verify it first-hand, will do later today (and update the project GitHub repo accordingly): please let me know if it works.

Belka383 commented 6 years ago

Thank you! So it works correctly!