diff --git a/Controllers/LoginController.cs b/Controllers/LoginController.cs new file mode 100644 index 0000000..da212ee --- /dev/null +++ b/Controllers/LoginController.cs @@ -0,0 +1,138 @@ +using CarCareTracker.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Mvc; +using System.Net; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; + +namespace CarCareTracker.Controllers +{ + public class LoginController : Controller + { + private IDataProtector _dataProtector; + private readonly ILogger _logger; + public LoginController( + ILogger logger, + IDataProtectionProvider securityProvider + ) + { + _dataProtector = securityProvider.CreateProtector("login"); + _logger = logger; + } + public IActionResult Index() + { + return View(); + } + [HttpPost] + public IActionResult Login(LoginModel credentials) + { + if (string.IsNullOrWhiteSpace(credentials.UserName) || + string.IsNullOrWhiteSpace(credentials.Password)) + { + return Json(false); + } + //compare it against hashed credentials + try + { + var configFileContents = System.IO.File.ReadAllText("userConfig.json"); + var existingUserConfig = System.Text.Json.JsonSerializer.Deserialize(configFileContents); + if (existingUserConfig is not null) + { + //create hashes of the login credentials. + var hashedUserName = Sha256_hash(credentials.UserName); + var hashedPassword = Sha256_hash(credentials.Password); + //compare against stored hash. + if (hashedUserName == existingUserConfig.UserNameHash && + hashedPassword == existingUserConfig.UserPasswordHash) + { + //auth success, create auth cookie + //encrypt stuff. + AuthCookie authCookie = new AuthCookie + { + Id = 1, //this is hardcoded for now + UserName = credentials.UserName, + ExpiresOn = DateTime.Now.AddDays(credentials.IsPersistent ? 30 : 1) + }; + var serializedCookie = JsonSerializer.Serialize(authCookie); + var encryptedCookie = _dataProtector.Protect(serializedCookie); + Response.Cookies.Append("ACCESS_TOKEN", encryptedCookie, new CookieOptions { Expires = new DateTimeOffset(authCookie.ExpiresOn) }); + return Json(true); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error on saving config file."); + } + return Json(false); + } + [Authorize] //User must already be logged in to do this. + [HttpPost] + public IActionResult CreateLoginCreds(LoginModel credentials) + { + try + { + var configFileContents = System.IO.File.ReadAllText("userConfig.json"); + var existingUserConfig = JsonSerializer.Deserialize(configFileContents); + if (existingUserConfig is not null) + { + //create hashes of the login credentials. + var hashedUserName = Sha256_hash(credentials.UserName); + var hashedPassword = Sha256_hash(credentials.Password); + //copy over settings that are off limits on the settings page. + existingUserConfig.EnableAuth = true; + existingUserConfig.UserNameHash = hashedUserName; + existingUserConfig.UserPasswordHash = hashedPassword; + } + System.IO.File.WriteAllText("userConfig.json", JsonSerializer.Serialize(existingUserConfig)); + return Json(true); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error on saving config file."); + } + return Json(false); + } + [Authorize] + [HttpPost] + public IActionResult DestroyLoginCreds() + { + try + { + var configFileContents = System.IO.File.ReadAllText("userConfig.json"); + var existingUserConfig = JsonSerializer.Deserialize(configFileContents); + if (existingUserConfig is not null) + { + //copy over settings that are off limits on the settings page. + existingUserConfig.EnableAuth = false; + existingUserConfig.UserNameHash = string.Empty; + existingUserConfig.UserPasswordHash = string.Empty; + } + System.IO.File.WriteAllText("userConfig.json", JsonSerializer.Serialize(existingUserConfig)); + return Json(true); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error on saving config file."); + } + return Json(false); + } + private static string Sha256_hash(string value) + { + StringBuilder Sb = new StringBuilder(); + + using (var hash = SHA256.Create()) + { + Encoding enc = Encoding.UTF8; + byte[] result = hash.ComputeHash(enc.GetBytes(value)); + + foreach (byte b in result) + Sb.Append(b.ToString("x2")); + } + + return Sb.ToString(); + } + } +} diff --git a/Controllers/VehicleController.cs b/Controllers/VehicleController.cs index 1199e35..b48ed9a 100644 --- a/Controllers/VehicleController.cs +++ b/Controllers/VehicleController.cs @@ -12,7 +12,7 @@ namespace CarCareTracker.Controllers [Authorize] public class VehicleController : Controller { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IVehicleDataAccess _dataAccess; private readonly INoteDataAccess _noteDataAccess; private readonly IServiceRecordDataAccess _serviceRecordDataAccess; @@ -24,7 +24,7 @@ namespace CarCareTracker.Controllers private readonly IConfiguration _config; private readonly IFileHelper _fileHelper; - public VehicleController(ILogger logger, + public VehicleController(ILogger logger, IFileHelper fileHelper, IVehicleDataAccess dataAccess, INoteDataAccess noteDataAccess, diff --git a/Middleware/Authen.cs b/Middleware/Authen.cs index d4bc263..8fede1e 100644 --- a/Middleware/Authen.cs +++ b/Middleware/Authen.cs @@ -1,24 +1,30 @@ -using Microsoft.AspNetCore.Authentication; +using CarCareTracker.Models; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; using System.Security.Claims; using System.Text.Encodings.Web; +using System.Text.Json; namespace CarCareTracker.Middleware { public class Authen : AuthenticationHandler { private IHttpContextAccessor _httpContext; + private IDataProtector _dataProtector; private bool enableAuth; public Authen( IOptionsMonitor options, UrlEncoder encoder, ILoggerFactory logger, - IConfiguration configuration, - IHttpContextAccessor httpContext): base(options, logger, encoder) + IConfiguration configuration, + IDataProtectionProvider securityProvider, + IHttpContextAccessor httpContext) : base(options, logger, encoder) { _httpContext = httpContext; + _dataProtector = securityProvider.CreateProtector("login"); enableAuth = bool.Parse(configuration["EnableAuth"]); } protected override async Task HandleAuthenticateAsync() @@ -34,9 +40,45 @@ namespace CarCareTracker.Middleware appIdentity.AddClaims(userIdentity); AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), this.Scheme.Name); return AuthenticateResult.Success(ticket); - } else + } + else { - //auth is enabled by user, we will have to authenticate the user via a ticket. + //auth is enabled by user, we will have to authenticate the user via a ticket retrieved from the auth cookie. + var access_token = _httpContext.HttpContext.Request.Cookies["ACCESS_TOKEN"]; + if (string.IsNullOrWhiteSpace(access_token)) + { + return AuthenticateResult.Fail("Cookie is invalid or does not exist."); + } + else + { + //decrypt the access token. + var decryptedCookie = _dataProtector.Unprotect(access_token); + AuthCookie authCookie = JsonSerializer.Deserialize(decryptedCookie); + if (authCookie != null) + { + //validate auth cookie + if (authCookie.ExpiresOn < DateTime.Now) + { + //if cookie is expired + return AuthenticateResult.Fail("Expired credentials"); + } + else if (authCookie.Id == default || string.IsNullOrWhiteSpace(authCookie.UserName)) + { + return AuthenticateResult.Fail("Corrupted credentials"); + } + else + { + var appIdentity = new ClaimsIdentity("Custom"); + var userIdentity = new List + { + new(ClaimTypes.Name, authCookie.UserName) + }; + appIdentity.AddClaims(userIdentity); + AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), this.Scheme.Name); + return AuthenticateResult.Success(ticket); + } + } + } return AuthenticateResult.Fail("Invalid credentials"); } } diff --git a/Models/Login/AuthCookie.cs b/Models/Login/AuthCookie.cs new file mode 100644 index 0000000..0a3356a --- /dev/null +++ b/Models/Login/AuthCookie.cs @@ -0,0 +1,9 @@ +namespace CarCareTracker.Models +{ + public class AuthCookie + { + public int Id { get; set; } + public string UserName { get; set; } + public DateTime ExpiresOn { get; set; } + } +} diff --git a/Models/Login/LoginModel.cs b/Models/Login/LoginModel.cs new file mode 100644 index 0000000..8afa0fb --- /dev/null +++ b/Models/Login/LoginModel.cs @@ -0,0 +1,9 @@ +namespace CarCareTracker.Models +{ + public class LoginModel + { + public string UserName { get; set; } + public string Password { get; set; } + public bool IsPersistent { get; set; } = false; + } +} diff --git a/Program.cs b/Program.cs index d98e43d..ebf558f 100644 --- a/Program.cs +++ b/Program.cs @@ -21,6 +21,7 @@ builder.Services.AddSingleton(); builder.Configuration.AddJsonFile("userConfig.json", optional: true, reloadOnChange: true); //Configure Auth +builder.Services.AddDataProtection(); builder.Services.AddHttpContextAccessor(); builder.Services.AddAuthentication("AuthN").AddScheme("AuthN", opts => { }); builder.Services.AddAuthorization(options => diff --git a/Views/Home/_Settings.cshtml b/Views/Home/_Settings.cshtml index e065e74..1143b89 100644 --- a/Views/Home/_Settings.cshtml +++ b/Views/Home/_Settings.cshtml @@ -25,7 +25,7 @@
- +
@@ -86,4 +86,45 @@ } }) } + function enableAuthCheckChanged(){ + var enableAuth = $("#enableAuth").is(":checked"); + if (enableAuth) { + //swal dialog to set up username and password. + Swal.fire({ + title: 'Setup Credentials', + html: ` + + + `, + confirmButtonText: 'Setup', + focusConfirm: false, + preConfirm: () => { + const username = $("#authUsername").val(); + const password = $("#authPassword").val(); + if (!username || !password) { + Swal.showValidationMessage(`Please enter username and password`) + } + return { username, password } + }, + }).then(function (result) { + if (result.isConfirmed) { + $.post('/Login/CreateLoginCreds', { userName: result.value.username, password: result.value.password }, function (data) { + if (data) { + window.location.href = '/Home'; + } else { + errorToast("An error occurred, please try again later."); + } + }) + } + }); + } else { + $.post('/Login/DestroyLoginCreds', function (data) { + if (data) { + setTimeout(function () { window.location.href = '/Home' }, 1000); + } else { + errorToast("An error occurred, please try again later."); + } + }); + } + } \ No newline at end of file diff --git a/Views/Login/Index.cshtml b/Views/Login/Index.cshtml new file mode 100644 index 0000000..4df9a74 --- /dev/null +++ b/Views/Login/Index.cshtml @@ -0,0 +1,28 @@ +@{ + ViewData["Title"] = "LubeLogger - Login"; +} +@section Scripts { + +} +
+
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
\ No newline at end of file diff --git a/wwwroot/js/login.js b/wwwroot/js/login.js new file mode 100644 index 0000000..a1c5690 --- /dev/null +++ b/wwwroot/js/login.js @@ -0,0 +1,12 @@ +function performLogin() { + var userName = $("#inputUserName").val(); + var userPassword = $("#inputUserPassword").val(); + var isPersistent = $("#inputPersistent").is(":checked"); + $.post('/Login/Login', {userName: userName, password: userPassword, isPersistent: isPersistent}, function (data) { + if (data) { + window.location.href = '/Home'; + } else { + errorToast("Invalid Login Credentials, please try again."); + } + }) +} \ No newline at end of file