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]
|
[Authorize]
|
||||||
public class VehicleController : Controller
|
public class VehicleController : Controller
|
||||||
{
|
{
|
||||||
private readonly ILogger<HomeController> _logger;
|
private readonly ILogger<VehicleController> _logger;
|
||||||
private readonly IVehicleDataAccess _dataAccess;
|
private readonly IVehicleDataAccess _dataAccess;
|
||||||
private readonly INoteDataAccess _noteDataAccess;
|
private readonly INoteDataAccess _noteDataAccess;
|
||||||
private readonly IServiceRecordDataAccess _serviceRecordDataAccess;
|
private readonly IServiceRecordDataAccess _serviceRecordDataAccess;
|
||||||
@@ -24,7 +24,7 @@ namespace CarCareTracker.Controllers
|
|||||||
private readonly IConfiguration _config;
|
private readonly IConfiguration _config;
|
||||||
private readonly IFileHelper _fileHelper;
|
private readonly IFileHelper _fileHelper;
|
||||||
|
|
||||||
public VehicleController(ILogger<HomeController> logger,
|
public VehicleController(ILogger<VehicleController> logger,
|
||||||
IFileHelper fileHelper,
|
IFileHelper fileHelper,
|
||||||
IVehicleDataAccess dataAccess,
|
IVehicleDataAccess dataAccess,
|
||||||
INoteDataAccess noteDataAccess,
|
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.Components.Web;
|
||||||
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace CarCareTracker.Middleware
|
namespace CarCareTracker.Middleware
|
||||||
{
|
{
|
||||||
public class Authen : AuthenticationHandler<AuthenticationSchemeOptions>
|
public class Authen : AuthenticationHandler<AuthenticationSchemeOptions>
|
||||||
{
|
{
|
||||||
private IHttpContextAccessor _httpContext;
|
private IHttpContextAccessor _httpContext;
|
||||||
|
private IDataProtector _dataProtector;
|
||||||
private bool enableAuth;
|
private bool enableAuth;
|
||||||
public Authen(
|
public Authen(
|
||||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||||
UrlEncoder encoder,
|
UrlEncoder encoder,
|
||||||
ILoggerFactory logger,
|
ILoggerFactory logger,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
IHttpContextAccessor httpContext): base(options, logger, encoder)
|
IDataProtectionProvider securityProvider,
|
||||||
|
IHttpContextAccessor httpContext) : base(options, logger, encoder)
|
||||||
{
|
{
|
||||||
_httpContext = httpContext;
|
_httpContext = httpContext;
|
||||||
|
_dataProtector = securityProvider.CreateProtector("login");
|
||||||
enableAuth = bool.Parse(configuration["EnableAuth"]);
|
enableAuth = bool.Parse(configuration["EnableAuth"]);
|
||||||
}
|
}
|
||||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||||
@@ -34,9 +40,45 @@ namespace CarCareTracker.Middleware
|
|||||||
appIdentity.AddClaims(userIdentity);
|
appIdentity.AddClaims(userIdentity);
|
||||||
AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), this.Scheme.Name);
|
AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), this.Scheme.Name);
|
||||||
return AuthenticateResult.Success(ticket);
|
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");
|
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);
|
builder.Configuration.AddJsonFile("userConfig.json", optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
//Configure Auth
|
//Configure Auth
|
||||||
|
builder.Services.AddDataProtection();
|
||||||
builder.Services.AddHttpContextAccessor();
|
builder.Services.AddHttpContextAccessor();
|
||||||
builder.Services.AddAuthentication("AuthN").AddScheme<AuthenticationSchemeOptions, Authen>("AuthN", opts => { });
|
builder.Services.AddAuthentication("AuthN").AddScheme<AuthenticationSchemeOptions, Authen>("AuthN", opts => { });
|
||||||
builder.Services.AddAuthorization(options =>
|
builder.Services.AddAuthorization(options =>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
<label class="form-check-label" for="useDescending">Sort lists in Descending Order(Newest to Oldest)</label>
|
<label class="form-check-label" for="useDescending">Sort lists in Descending Order(Newest to Oldest)</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check form-switch">
|
<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>
|
<label class="form-check-label" for="enableAuth">Enable Authentication</label>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</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