added authentication.

This commit is contained in:
ivancheahhh
2024-01-04 15:41:43 -07:00
parent 3306fefd4d
commit bb82f1ab72
9 changed files with 288 additions and 8 deletions

View File

@@ -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<LoginController> _logger;
public LoginController(
ILogger<LoginController> 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<UserConfig>(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<UserConfig>(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<UserConfig>(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();
}
}
}

View File

@@ -12,7 +12,7 @@ namespace CarCareTracker.Controllers
[Authorize]
public class VehicleController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly ILogger<VehicleController> _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<HomeController> logger,
public VehicleController(ILogger<VehicleController> logger,
IFileHelper fileHelper,
IVehicleDataAccess dataAccess,
INoteDataAccess noteDataAccess,

View File

@@ -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<AuthenticationSchemeOptions>
{
private IHttpContextAccessor _httpContext;
private IDataProtector _dataProtector;
private bool enableAuth;
public Authen(
IOptionsMonitor<AuthenticationSchemeOptions> options,
UrlEncoder encoder,
ILoggerFactory logger,
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<AuthenticateResult> 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<AuthCookie>(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<Claim>
{
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");
}
}

View File

@@ -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; }
}
}

View File

@@ -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;
}
}

View File

@@ -21,6 +21,7 @@ builder.Services.AddSingleton<IFileHelper, FileHelper>();
builder.Configuration.AddJsonFile("userConfig.json", optional: true, reloadOnChange: true);
//Configure Auth
builder.Services.AddDataProtection();
builder.Services.AddHttpContextAccessor();
builder.Services.AddAuthentication("AuthN").AddScheme<AuthenticationSchemeOptions, Authen>("AuthN", opts => { });
builder.Services.AddAuthorization(options =>

View File

@@ -25,7 +25,7 @@
<label class="form-check-label" for="useDescending">Sort lists in Descending Order(Newest to Oldest)</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableAuth" checked="@Model.EnableAuth">
<input class="form-check-input" onChange="enableAuthCheckChanged()" type="checkbox" role="switch" id="enableAuth" checked="@Model.EnableAuth">
<label class="form-check-label" for="enableAuth">Enable Authentication</label>
</div>
</div>
@@ -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: `
<input type="text" id="authUsername" class="swal2-input" placeholder="Username">
<input type="password" id="authPassword" class="swal2-input" placeholder="Password">
`,
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.");
}
});
}
}
</script>

28
Views/Login/Index.cshtml Normal file
View File

@@ -0,0 +1,28 @@
@{
ViewData["Title"] = "LubeLogger - Login";
}
@section Scripts {
<script src="~/js/login.js" asp-append-version="true"></script>
}
<div class="container chartContainer d-flex align-items-center justify-content-center">
<div class="row">
<div class="col-12">
<img src="/defaults/lubelogger_logo.png" />
<div class="form-group">
<label for="inputUserName">Username</label>
<input type="text" id="inputUserName" class="form-control">
</div>
<div class="form-group">
<label for="inputUserPassword">Password</label>
<input type="password" id="inputUserPassword" class="form-control">
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="inputPersistent">
<label class="form-check-label" for="inputPersistent">Remember Me</label>
</div>
<div class="d-grid">
<button type="button" class="btn btn-warning mt-2" onclick="performLogin()">Login</button>
</div>
</div>
</div>
</div>

12
wwwroot/js/login.js Normal file
View File

@@ -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.");
}
})
}