Cleaned up and refactored the code to make things much easier to understand.

This commit is contained in:
2021-01-13 11:24:19 -06:00
parent 4721a70ea7
commit e198838a32
6 changed files with 149 additions and 267 deletions
Binary file not shown.
+1 -5
View File
@@ -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;
+37 -25
View File
@@ -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
{
+47 -21
View File
@@ -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();
}
}
}
+46 -35
View File
@@ -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);
+18 -181
View File
@@ -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<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 (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();
// }
// }
//}
}
}