diff --git a/.vs/SecureCore/v16/.suo b/.vs/SecureCore/v16/.suo index f2d8eb6..21a296f 100644 Binary files a/.vs/SecureCore/v16/.suo and b/.vs/SecureCore/v16/.suo differ diff --git a/SecureCore/AppSettingsManager.cs b/SecureCore/AppSettingsManager.cs index fce2ab7..f0deaed 100644 --- a/SecureCore/AppSettingsManager.cs +++ b/SecureCore/AppSettingsManager.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using System.IO; +using System.IO; using Microsoft.Extensions.Configuration; //using Microsoft.Extensions.Configuration.Binder; diff --git a/SecureCore/Authentication/PasswordManager.cs b/SecureCore/Authentication/PasswordManager.cs index ff58a33..97c0096 100644 --- a/SecureCore/Authentication/PasswordManager.cs +++ b/SecureCore/Authentication/PasswordManager.cs @@ -1,17 +1,15 @@ using System; +using System.Data; using System.Data.SqlClient; using Microsoft.AspNetCore.Cryptography.KeyDerivation; namespace SecureCore.Authentication { - public class PasswordManager + public static class PasswordManager { //Add some pepper to the passwords for good measure: //https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html private static string Pepper { get; } = "rVk/OwQUw01qy76Q+5WimPk+NdqUMMghftMXyJzzckOj/+eFn056PDYzBD61E/ZNjRdgiMK6RhcHEcdfpJdbcw=="; - - private static string ConnectionString = @"Server=DESKTOP-OEDDVKC\SQLEXPRESS;Database=main;Integrated Security=true;"; - private static int Iterations { get; } = 100000; private static KeyDerivationPrf KeyType { get; } = KeyDerivationPrf.HMACSHA512; private static int KeySize { get; } = 512 / 8; @@ -22,10 +20,6 @@ namespace SecureCore.Authentication public static int MaxPasswordLength { get; } = 128; public static int MinPasswordLength { get; } = 16; - public PasswordManager() - { - } - public static (string Hash, string Salt) HashPassword(string password) { var salt = new byte[SaltSize]; @@ -35,7 +29,7 @@ namespace SecureCore.Authentication return (GetHash(password, salt), Convert.ToBase64String(salt)); } - public static (bool IsValid, string Message) PasswordIsValid(string password, string salt, string passwordHash) + public static (bool IsValid, string Message) IsPasswordValid(string password, string salt, string passwordHash) { if (password.Length < MinPasswordLength) return (false, $"Your password is too short, it must be at least {MinPasswordLength} characters long and not exceed {MaxPasswordLength} characters."); if (password.Length > MaxPasswordLength) return (false, $"Your password is too long, it must not exceed {MaxPasswordLength} characters and must contain at least {MinPasswordLength} characters."); @@ -45,35 +39,53 @@ namespace SecureCore.Authentication if (passwordHash == GetHash(password, saltBytes)) return (true, string.Empty); else - return (false, string.Empty); + return (false, "User name or password is not correct."); } - public static (string PasswordHash, string SaltHash) GetUserPasswordHash(int userId) + public static void InsertPasswordResetRequest(string userName, string sessionToken, DateTime expirationDate, string userAgent, string ipAddress, string connectionString) { - using (var connection = new SqlConnection(ConnectionString)) - { - using (var command = new SqlCommand("SELECT [Password Hash], [Salt Hash] FROM Login WHERE [User Key] = @UserId", connection)) - { - command.Parameters.AddWithValue("UserId", userId); + using var connection = new SqlConnection(connectionString); - connection.Open(); + using var command = new SqlCommand("LogPasswordResetRequest", connection) { CommandType = CommandType.StoredProcedure }; - var reader = command.ExecuteReader(); + command.Parameters.AddWithValue("SessionToken", sessionToken); + command.Parameters.AddWithValue("ExpirationDate", expirationDate); + command.Parameters.AddWithValue("UserName", userName); + command.Parameters.AddWithValue("UserAgent", userAgent); + command.Parameters.AddWithValue("IpAddress", ipAddress); - if (!reader.HasRows) throw new MissingFieldException("No login records exist for this user."); + connection.Open(); - reader.Read(); + command.ExecuteNonQuery(); + } - return (reader["Password Hash"].ToString(), reader["Salt Hash"].ToString()); - } - } + //TODO: Reset Password Function Here + + public static (string PasswordHash, string Salt) GetPasswordHashAndSalt(string username, string connectionString) + { + using var connection = new SqlConnection(connectionString); + + using var command = new SqlCommand("SELECT [Password Hash], [Salt] FROM dbo.GetUserPasswordHashAndSalt(@Username)", connection); + + command.Parameters.AddWithValue("Username", username); + + connection.Open(); + + var reader = command.ExecuteReader(); + + reader.Read(); + + if (!reader.HasRows) return (string.Empty, string.Empty); + + return (reader["Password Hash"].ToString(), reader["Salt"].ToString()); } public static string HashStringData(string data, string salt = "") { - var saltBytes = new byte[0]; + _ = new byte[0]; + byte[] saltBytes; - if(!string.IsNullOrEmpty(salt)) + if (!string.IsNullOrEmpty(salt)) saltBytes = Convert.FromBase64String(salt); else { diff --git a/SecureCore/Authentication/SessionManager.cs b/SecureCore/Authentication/SessionManager.cs index 53de0b2..81e1f65 100644 --- a/SecureCore/Authentication/SessionManager.cs +++ b/SecureCore/Authentication/SessionManager.cs @@ -1,25 +1,14 @@ using System; +using System.Data; using System.Data.SqlClient; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace SecureCore.Authentication { - public class SessionManager + public static class SessionManager { - private int SessionKeySize { get; } = 64; //n bytes - private string ConnectionString { get; set; } + private static int SessionKeySize { get; } = 64; //n bytes - public SessionManager() - { - if (AppSettingsManager.TryGetConnectionStringByName("MainDataConnectionString", out string connection)) - ConnectionString = connection; - else - throw new Exception("Failed to get connection string."); - } - - public string CreateSessionToken() + public static string CreateSessionToken() { var token = new byte[SessionKeySize]; @@ -28,13 +17,50 @@ namespace SecureCore.Authentication return Convert.ToBase64String(token); } - //public string CreatePasswordRecoveryKey(string userName) - //{ + public static bool IsSessionTokenValid(string sessionToken, string connectionString, bool isResetToken = false) + { + using var connection = new SqlConnection(connectionString); - // using (var connection = new SqlConnection(ConnectionString)) - // { + using var command = new SqlCommand("ValidateSessionToken", connection) { CommandType = CommandType.StoredProcedure }; - // } - //} + command.Parameters.AddWithValue("SessionToken", sessionToken); + command.Parameters.AddWithValue("IsResetToken", isResetToken); + + connection.Open(); + + var result = command.ExecuteScalar(); + + return Convert.ToBoolean(result); + } + + public static void Logout(string sessionToken, string connectionString) + { + using var connection = new SqlConnection(connectionString); + + using var command = new SqlCommand("LogoutUser", connection) { CommandType = CommandType.StoredProcedure }; + + command.Parameters.AddWithValue("SessionToken", sessionToken); + + connection.Open(); + + command.ExecuteNonQuery(); + } + + public static void Login(string userName, string sessionToken, DateTime expirationDate, string userAgent, string ipAddress, string connectionString) + { + using var connection = new SqlConnection(connectionString); + + using var command = new SqlCommand("LoginUser", connection) { CommandType = CommandType.StoredProcedure }; + + command.Parameters.AddWithValue("SessionToken", sessionToken); + command.Parameters.AddWithValue("ExpirationDate", expirationDate); + command.Parameters.AddWithValue("UserName", userName); + command.Parameters.AddWithValue("UserAgent", userAgent); + command.Parameters.AddWithValue("IpAddress", ipAddress); + + connection.Open(); + + command.ExecuteNonQuery(); + } } } diff --git a/SecureCore/Controllers/AuthController.cs b/SecureCore/Controllers/AuthController.cs index 81b0a63..ac48bf4 100644 --- a/SecureCore/Controllers/AuthController.cs +++ b/SecureCore/Controllers/AuthController.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Http; using System; using System.Web; using System.Collections.Generic; -using System.Linq; +using System.Data.SqlClient; using System.Threading.Tasks; using SecureCore.Services; using SecureCore.Authentication; @@ -19,39 +19,44 @@ namespace SecureCore.Controllers [AcceptVerbs("POST")] public IActionResult Login([FromBody] LoginInfo info) { - //NOTE: password length should be at most 64 - 128 characters long. - //Very the user has login data. - //if (!UserDataService.UserHasLoginData(info.UserName)) return Unauthorized("User doesn't have login creds"); - var result = UserDataService.UserNameIsValid(info.UserName); - - if (!result.IsValid) return Unauthorized(result.Message); - - //if (HttpContext.Request.Cookies.ContainsKey("Session")) - //{ - // if(UserDataService.IsSessionTokenValid(HttpContext.Request.Cookies["Session"]))//, info.UserName)) - // return Ok($"Session is live{Environment.NewLine}"); - //} - - var (password, salt) = UserDataService.GetUserPasswordHash(info.UserName); - var agent = HttpContext.Request.Headers[Microsoft.Net.Http.Headers.HeaderNames.UserAgent]; - var ip = PasswordManager.HashStringData(Request.HttpContext.Connection.RemoteIpAddress.MapToIPv4().ToString(), salt); - result = PasswordManager.PasswordIsValid(info.Password, salt, password); - - if (result.IsValid) + AppSettingsManager.TryGetConnectionStringByName("MainDataConnectionString", out string connectionString); + //TODO: see if the request has an active session key so we don't spam out new keys like mad + try { - var session = new SessionManager(); + //Verify that the username provided is valid, i.e. no whitespace, special characters, etc. + var result = UserDataService.IsUsernameValid(info.UserName); + //If the name isn't valid, return the error message to the user. + if (!result.IsValid) return Unauthorized(result.Message); - var sessionToken = session.CreateSessionToken(); + //if (HttpContext.Request.Cookies.ContainsKey("Session")) + //{ + // if(UserDataService.IsSessionTokenValid(HttpContext.Request.Cookies["Session"]))//, info.UserName)) + // return Ok($"Session is live{Environment.NewLine}"); + //} + //Next try to get the user's login data, if the fuction returns empty strings, then the user isn't a registered name. + var (password, salt) = PasswordManager.GetPasswordHashAndSalt(info.UserName, connectionString); + //If the user name isn't in the system, then simply return a generic error message about something not being right. + if (password == string.Empty) return Unauthorized("User name or password is not correct."); - UserDataService.SetUserSessionToken(info.UserName, sessionToken, DateTime.Now.AddDays(7), agent, ip); + result = PasswordManager.IsPasswordValid(info.Password, salt, password); - HttpContext.Response.Cookies.Append("Session", sessionToken, GetCookieOptions()); + if (!result.IsValid) return Unauthorized(result.Message); + + var sessionToken = SessionManager.CreateSessionToken(); + var agent = HttpContext.Request.Headers[Microsoft.Net.Http.Headers.HeaderNames.UserAgent]; + var ip = PasswordManager.HashStringData(Request.HttpContext.Connection.RemoteIpAddress.MapToIPv4().ToString(), salt); + var cookieOptions = GetCookieOptions(); + + SessionManager.Login(info.UserName, sessionToken, cookieOptions.Expires.Value.UtcDateTime, agent, ip, connectionString); + + HttpContext.Response.Cookies.Append("Session", sessionToken, cookieOptions); return Ok($"Logged in success{Environment.NewLine}"); } - else + catch(Exception ex) { - return Unauthorized(result.Message); + //TODO: Log this event. + return Unauthorized("An error has occured trying to process your request, please try again in a few minutes."); } } @@ -59,17 +64,19 @@ namespace SecureCore.Controllers [AcceptVerbs("POST")] public IActionResult Register([FromBody] LoginInfo info) { + AppSettingsManager.TryGetConnectionStringByName("MainDataConnectionString", out string connectionString); var (hash, salt) = PasswordManager.HashPassword(info.Password); - var session = new SessionManager(); - var sessionToken = session.CreateSessionToken(); + var sessionToken = SessionManager.CreateSessionToken(); var agent = HttpContext.Request.Headers[Microsoft.Net.Http.Headers.HeaderNames.UserAgent]; var ip = PasswordManager.HashStringData(Request.HttpContext.Connection.RemoteIpAddress.MapToIPv4().ToString(), salt); try { - var i = UserDataService.RegisterNewUser(info.UserName, info.Email, hash, salt, sessionToken, DateTime.Now.AddDays(7),agent, ip); + var cookieOptions = GetCookieOptions(); - HttpContext.Response.Cookies.Append("Session", sessionToken, GetCookieOptions()); + var i = UserDataService.RegisterNewUser(info.UserName, info.Email, hash, salt, sessionToken, cookieOptions.Expires.Value.UtcDateTime, agent, ip, connectionString); + + HttpContext.Response.Cookies.Append("Session", sessionToken, cookieOptions); return Ok($"New User ID: {i}"); } @@ -83,7 +90,9 @@ namespace SecureCore.Controllers [AcceptVerbs("POST")] public IActionResult Logout() { - UserDataService.DestroySession(HttpContext.Request.Cookies["Session"]); + AppSettingsManager.TryGetConnectionStringByName("MainDataConnectionString", out string connectionString); + + SessionManager.Logout(HttpContext.Request.Cookies["Session"], connectionString); HttpContext.Response.Cookies.Delete("Session"); @@ -94,9 +103,11 @@ namespace SecureCore.Controllers //[AcceptVerbs("GET")] public IActionResult ResetPassword([FromQuery] string token) { + AppSettingsManager.TryGetConnectionStringByName("MainDataConnectionString", out string connectionString); + try { - if (!UserDataService.IsResetTokenValid(token)) return Unauthorized("Token invalid"); + if (!SessionManager.IsSessionTokenValid(token, connectionString, true)) return Unauthorized("Token invalid"); } catch (Exception e) @@ -110,14 +121,14 @@ namespace SecureCore.Controllers [AcceptVerbs("POST")] 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 token = sessionManager.CreateSessionToken(); + AppSettingsManager.TryGetConnectionStringByName("MainDataConnectionString", out string connectionString); + var token = SessionManager.CreateSessionToken(); var agent = HttpContext.Request.Headers[Microsoft.Net.Http.Headers.HeaderNames.UserAgent]; var ip = PasswordManager.HashStringData(Request.HttpContext.Connection.RemoteIpAddress.MapToIPv4().ToString()); try { - UserDataService.SetUserSessionToken(userName, token, DateTime.Now.AddHours(1), agent, ip, true); + PasswordManager.InsertPasswordResetRequest(userName, token, DateTime.Now.AddHours(1), agent, ip, connectionString); token = HttpUtility.UrlEncode(token); diff --git a/SecureCore/Services/UserDataService.cs b/SecureCore/Services/UserDataService.cs index 59dabb1..cb307ff 100644 --- a/SecureCore/Services/UserDataService.cs +++ b/SecureCore/Services/UserDataService.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Data; -using System.Transactions; using System.Data.SqlClient; namespace SecureCore.Services @@ -13,202 +12,40 @@ namespace SecureCore.Services // public static int UserNameMaxLength { get; } = 64; - private static string ConnectionString = @"Server=DESKTOP-OEDDVKC\SQLEXPRESS;Database=main;Integrated Security=true;"; - - public static (bool IsValid, string Message) UserNameIsValid(string userName) + public static (bool IsValid, string Message) IsUsernameValid(string userName) { - if (userName.Length > UserNameMaxLength) return (false, $"Username to long, must not exceed {UserNameMaxLength} characters."); + if (userName.Length > UserNameMaxLength) return (false, $"Username too long, must not exceed {UserNameMaxLength} characters."); var invalidChars = new List(); 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 (char.IsPunctuation(c) || char.IsSymbol(c) || char.IsControl(c) || char.IsSeparator(c) || char.IsWhiteSpace(c)) invalidChars.Add(c); - if (invalidChars.Count > 0) return (false, $"The characters '{string.Join(",", invalidChars)}' are not allowed in a user name."); + if (invalidChars.Count > 0) return (false, $"The character(s) '{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 int RegisterNewUser(string userName, string email, string passwordHash, string saltHash, string sessionToken, DateTime expirationDate, string userAgent, string ipAddress, string connectionString) { - var userId = GetUserId(userName); + using var connection = new SqlConnection(connectionString); - using(var connection = new SqlConnection(ConnectionString)) - { - using(var command = new SqlCommand("SELECT [Password Hash], [Salt] FROM Login WHERE [User Key] = @UserId", connection)) - { - command.Parameters.AddWithValue("UserId", userId); + using var command = new SqlCommand("RegisterNewUser", connection) { CommandType = CommandType.StoredProcedure }; - connection.Open(); + command.Parameters.AddWithValue("UserName", userName); + command.Parameters.AddWithValue("Email", email); + command.Parameters.AddWithValue("Password", passwordHash); + command.Parameters.AddWithValue("Salt", saltHash); + command.Parameters.AddWithValue("SessionToken", sessionToken); + command.Parameters.AddWithValue("ExpirationDate", expirationDate); + command.Parameters.AddWithValue("UserAgent", userAgent); + command.Parameters.AddWithValue("IpAddress", ipAddress); - var reader = command.ExecuteReader(); + connection.Open(); - reader.Read(); + var result = command.ExecuteScalar(); - return (reader["Password Hash"].ToString(), reader["Salt"].ToString()); - } - } + return Convert.ToInt32(result); } - - public static bool UserHasLoginData(string userName) - { - var userId = GetUserId(userName); - - using (var connection = new SqlConnection(ConnectionString)) - { - using (var command = new SqlCommand("SELECT [Password Hash], [Salt Hash] FROM Login WHERE [User Key] = @UserId", connection)) - { - command.Parameters.AddWithValue("UserId", userId); - - connection.Open(); - - var reader = command.ExecuteReader(); - - return reader.HasRows; - } - } - } - - public static int GetUserId(string userName) - { - using (var connection = new SqlConnection(ConnectionString)) - { - using (var command = new SqlCommand("SELECT [User ID] FROM [User] WHERE Name = @UserName", connection)) - { - command.Parameters.AddWithValue("UserName", userName); - - connection.Open(); - - return Convert.ToInt32(command.ExecuteScalar()); - } - } - } - - public static void WriteUserPasswordAndSalt(string password, string salt, int userId) - { - using (var connection = new SqlConnection(ConnectionString)) - { - using (var command = new SqlCommand("INSERT INTO [Login]([Password Hash], [Salt Hash], [User Key]) VALUES (@PasswordHash, @SaltHash, @UserKey)", connection)) - { - command.Parameters.AddWithValue("PasswordHash", password); - command.Parameters.AddWithValue("SaltHash", salt); - command.Parameters.AddWithValue("UserKey", userId); - - connection.Open(); - - command.ExecuteNonQuery(); - } - } - } - - public static void SetUserSessionToken(string userName, string sessionToken, DateTime expirationDate, string userAgent, string ipAddress, bool isResetToken = false) - { - using (var connection = new SqlConnection(ConnectionString)) - { - using (var command = new SqlCommand("InsertSessionToken", connection) { CommandType = CommandType.StoredProcedure }) - { - command.Parameters.AddWithValue("SessionToken", sessionToken); - command.Parameters.AddWithValue("ExpirationDate", expirationDate); - command.Parameters.AddWithValue("UserName", userName); - command.Parameters.AddWithValue("UserAgent", userAgent); - command.Parameters.AddWithValue("IpAddress", ipAddress); - command.Parameters.AddWithValue("PendingReset", isResetToken); - - connection.Open(); - - command.ExecuteNonQuery(); - } - } - } - - public static int RegisterNewUser(string userName, string email, string passwordHash, string saltHash, string sessionToken, DateTime expirationDate, string userAgent, string ipAddress) - { - using (var connection = new SqlConnection(ConnectionString)) - { - using (var command = new SqlCommand("RegisterNewUser", connection) { CommandType = CommandType.StoredProcedure }) - { - command.Parameters.AddWithValue("UserName", userName); - command.Parameters.AddWithValue("Email", email); - command.Parameters.AddWithValue("Password", passwordHash); - command.Parameters.AddWithValue("Salt", saltHash); - command.Parameters.AddWithValue("SessionToken", sessionToken); - command.Parameters.AddWithValue("ExpirationDate", expirationDate); - command.Parameters.AddWithValue("UserAgent", userAgent); - command.Parameters.AddWithValue("IpAddress", ipAddress); - - connection.Open(); - - var result = command.ExecuteScalar(); - - return Convert.ToInt32(result); - } - } - } - - public static bool IsSessionTokenValid(string sessionToken)//, string userName) - { - using (var connection = new SqlConnection(ConnectionString)) - { - using (var command = new SqlCommand("ValidateSessionToken", connection) { CommandType = CommandType.StoredProcedure }) - { - command.Parameters.AddWithValue("SessionToken", sessionToken); - - connection.Open(); - - var result = command.ExecuteScalar(); - - return Convert.ToBoolean(result); - } - } - } - - public static void DestroySession(string sessionToken) - { - using (var connection = new SqlConnection(ConnectionString)) - { - using (var command = new SqlCommand("DestroySession", connection) { CommandType = CommandType.StoredProcedure }) - { - command.Parameters.AddWithValue("SessionToken", sessionToken); - - connection.Open(); - - command.ExecuteNonQuery(); - } - } - } - - public static bool IsResetTokenValid(string token) - { - using (var connection = new SqlConnection(ConnectionString)) - { - using (var command = new SqlCommand("ValidateResetToken", connection) { CommandType = CommandType.StoredProcedure }) - { - command.Parameters.AddWithValue("SessionToken", token); - - connection.Open(); - - var reader = command.ExecuteScalar(); - - return Convert.ToBoolean(reader); - } - } - } - - //public static void ResetPassword(string email) - //{ - // using (var connection = new SqlConnection(ConnectionString)) - // { - // using (var command = new SqlCommand("DestroySession", connection) { CommandType = CommandType.StoredProcedure }) - // { - // command.Parameters.AddWithValue("SessionToken", sessionToken); - - // connection.Open(); - - // command.ExecuteNonQuery(); - // } - // } - //} } }