added authentication.
This commit is contained in:
138
Controllers/LoginController.cs
Normal file
138
Controllers/LoginController.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
9
Models/Login/AuthCookie.cs
Normal file
9
Models/Login/AuthCookie.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
9
Models/Login/LoginModel.cs
Normal file
9
Models/Login/LoginModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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
28
Views/Login/Index.cshtml
Normal 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
12
wwwroot/js/login.js
Normal 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.");
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user