Updated the password validation and created a username validation function.
This commit is contained in:
Binary file not shown.
@@ -16,6 +16,10 @@ namespace SecureCore.Authentication
|
|||||||
private static KeyDerivationPrf KeyType { get; } = KeyDerivationPrf.HMACSHA512;
|
private static KeyDerivationPrf KeyType { get; } = KeyDerivationPrf.HMACSHA512;
|
||||||
private static int KeySize { get; } = 512 / 8;
|
private static int KeySize { get; } = 512 / 8;
|
||||||
private static int SaltSize { get; } = 128 / 8; //128 bit salt
|
private static int SaltSize { get; } = 128 / 8; //128 bit salt
|
||||||
|
//As noted here: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#maximum-password-lengths
|
||||||
|
//allowing passwords that are too long can result in a denial-of-service attack. So we must enforce password length limits.
|
||||||
|
//The recommended length is between 64 and 128, so I decided to go for the upper bounds.
|
||||||
|
public static int MaxPasswordLength { get; } = 128;
|
||||||
|
|
||||||
public PasswordManager()
|
public PasswordManager()
|
||||||
{
|
{
|
||||||
@@ -30,11 +34,16 @@ namespace SecureCore.Authentication
|
|||||||
return (GetHash(password, salt), Convert.ToBase64String(salt));
|
return (GetHash(password, salt), Convert.ToBase64String(salt));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool PasswordIsValid(string password, string salt, string passwordHash)
|
public static (bool IsValid, string Message) PasswordIsValid(string password, string salt, string passwordHash)
|
||||||
{
|
{
|
||||||
|
if (password.Length > MaxPasswordLength) return (false, $"Password length exceeds {MaxPasswordLength} characters.");
|
||||||
|
|
||||||
var saltBytes = Convert.FromBase64String(salt);
|
var saltBytes = Convert.FromBase64String(salt);
|
||||||
|
|
||||||
return passwordHash == GetHash(password, saltBytes);
|
if (passwordHash == GetHash(password, saltBytes))
|
||||||
|
return (true, string.Empty);
|
||||||
|
else
|
||||||
|
return (false, string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static (string PasswordHash, string SaltHash) GetUserPasswordHash(int userId)
|
public static (string PasswordHash, string SaltHash) GetUserPasswordHash(int userId)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ namespace SecureCore.Controllers
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
public class AuthController : Controller
|
public class AuthController : Controller
|
||||||
{
|
{
|
||||||
|
//TODO: Login will only ever return messages like "Wrong username / password." whereas register can return messages like "User exists.", "Password to weak", or "Password in top 100 most used.".
|
||||||
[HttpPost("login")]
|
[HttpPost("login")]
|
||||||
[AcceptVerbs("POST")]
|
[AcceptVerbs("POST")]
|
||||||
public IActionResult Login([FromBody] LoginInfo info)
|
public IActionResult Login([FromBody] LoginInfo info)
|
||||||
@@ -21,6 +22,11 @@ namespace SecureCore.Controllers
|
|||||||
//NOTE: password length should be at most 64 - 128 characters long.
|
//NOTE: password length should be at most 64 - 128 characters long.
|
||||||
//Very the user has login data.
|
//Very the user has login data.
|
||||||
//if (!UserDataService.UserHasLoginData(info.UserName)) return Unauthorized("User doesn't have login creds");
|
//if (!UserDataService.UserHasLoginData(info.UserName)) return Unauthorized("User doesn't have login creds");
|
||||||
|
if (info.Password.Length > PasswordManager.MaxPasswordLength) return Unauthorized($"Password exceeds maxium length of {PasswordManager.MaxPasswordLength} characters.");
|
||||||
|
|
||||||
|
var result = UserDataService.UserNameIsValid(info.UserName);
|
||||||
|
|
||||||
|
if (!result.IsValid) return Unauthorized(result.Message);
|
||||||
|
|
||||||
if (HttpContext.Request.Cookies.ContainsKey("Session"))
|
if (HttpContext.Request.Cookies.ContainsKey("Session"))
|
||||||
{
|
{
|
||||||
@@ -31,8 +37,9 @@ namespace SecureCore.Controllers
|
|||||||
var (password, salt) = UserDataService.GetUserPasswordHash(info.UserName);
|
var (password, salt) = UserDataService.GetUserPasswordHash(info.UserName);
|
||||||
var agent = HttpContext.Request.Headers[Microsoft.Net.Http.Headers.HeaderNames.UserAgent];
|
var agent = HttpContext.Request.Headers[Microsoft.Net.Http.Headers.HeaderNames.UserAgent];
|
||||||
var ip = PasswordManager.HashStringData(Request.HttpContext.Connection.RemoteIpAddress.MapToIPv4().ToString(), salt);
|
var ip = PasswordManager.HashStringData(Request.HttpContext.Connection.RemoteIpAddress.MapToIPv4().ToString(), salt);
|
||||||
|
result = PasswordManager.PasswordIsValid(info.Password, salt, password);
|
||||||
|
|
||||||
if (PasswordManager.PasswordIsValid(info.Password, salt, password))
|
if (result.IsValid)
|
||||||
{
|
{
|
||||||
var session = new SessionManager();
|
var session = new SessionManager();
|
||||||
|
|
||||||
@@ -89,14 +96,21 @@ namespace SecureCore.Controllers
|
|||||||
//[AcceptVerbs("GET")]
|
//[AcceptVerbs("GET")]
|
||||||
public IActionResult ResetPassword([FromQuery] string token)
|
public IActionResult ResetPassword([FromQuery] string token)
|
||||||
{
|
{
|
||||||
if (!UserDataService.IsResetTokenValid(token)) return Unauthorized("Token invalid");
|
try
|
||||||
|
{
|
||||||
|
if (!UserDataService.IsResetTokenValid(token)) return Unauthorized("Token invalid");
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
return Ok("Done");
|
return Ok("Done");
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("CreatePasswordResetLink")]
|
[HttpPost("CreatePasswordResetLink")]
|
||||||
[AcceptVerbs("POST")]
|
[AcceptVerbs("POST")]
|
||||||
public IActionResult CreatePasswordResetLink([FromBody] string userName)
|
public IActionResult CreatePasswordResetLink([FromBody] string userName) //TODO: this sig should only accept an email, so the link can be sent there.
|
||||||
{
|
{
|
||||||
var sessionManager = new SessionManager();
|
var sessionManager = new SessionManager();
|
||||||
var token = sessionManager.CreateSessionToken();
|
var token = sessionManager.CreateSessionToken();
|
||||||
|
|||||||
@@ -10,8 +10,27 @@ namespace SecureCore.Services
|
|||||||
{
|
{
|
||||||
public static class UserDataService
|
public static class UserDataService
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
public static int UserNameMaxLength { get; } = 64;
|
||||||
|
|
||||||
private static string ConnectionString = @"Server=DESKTOP-OEDDVKC\SQLEXPRESS;Database=main;Integrated Security=true;";
|
private static string ConnectionString = @"Server=DESKTOP-OEDDVKC\SQLEXPRESS;Database=main;Integrated Security=true;";
|
||||||
|
|
||||||
|
public static (bool IsValid, string Message) UserNameIsValid(string userName)
|
||||||
|
{
|
||||||
|
if (userName.Length > UserNameMaxLength) return (false, $"Username to long, must not exceed {UserNameMaxLength} characters.");
|
||||||
|
|
||||||
|
var invalidChars = new List<char>();
|
||||||
|
|
||||||
|
foreach(var c in userName)
|
||||||
|
{
|
||||||
|
if (char.IsPunctuation(c) || char.IsSymbol(c) || char.IsControl(c) || char.IsSeparator(c) || char.IsWhiteSpace(c)) invalidChars.Add(c);//return (false, $"The character '{c}' is not allowed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invalidChars.Count > 0) return (false, $"The characters '{string.Join(",", invalidChars)}' are not allowed in a user name.");
|
||||||
|
|
||||||
|
return (true, string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
public static (string PasswordHash, string SaltHash) GetUserPasswordHash(string userName)
|
public static (string PasswordHash, string SaltHash) GetUserPasswordHash(string userName)
|
||||||
{
|
{
|
||||||
var userId = GetUserId(userName);
|
var userId = GetUserId(userName);
|
||||||
|
|||||||
Reference in New Issue
Block a user