Closed Belka383 closed 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:
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.
@Darkseal Sorry, this error occurs. What could be the problem?
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.
Thank you! So it works correctly!
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;