Added OpenID login.

This commit is contained in:
DESKTOP-T0O5CDB\DESK-555BD 2024-02-15 18:57:22 -07:00
parent ac4ea07319
commit d5f0e57c3b
9 changed files with 127 additions and 5 deletions

View File

@ -13,7 +13,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="CsvHelper" Version="30.0.1" /> <PackageReference Include="CsvHelper" Version="30.0.1" />
<PackageReference Include="LiteDB" Version="5.0.17" /> <PackageReference Include="LiteDB" Version="5.0.17" />
<PackageReference Include="Npgsql" Version="8.0.1" /> <PackageReference Include="Npgsql" Version="8.0.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.3.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -4,6 +4,8 @@ using CarCareTracker.Models;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Net; using System.Net;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
@ -15,16 +17,19 @@ namespace CarCareTracker.Controllers
{ {
private IDataProtector _dataProtector; private IDataProtector _dataProtector;
private ILoginLogic _loginLogic; private ILoginLogic _loginLogic;
private IConfigHelper _config;
private readonly ILogger<LoginController> _logger; private readonly ILogger<LoginController> _logger;
public LoginController( public LoginController(
ILogger<LoginController> logger, ILogger<LoginController> logger,
IDataProtectionProvider securityProvider, IDataProtectionProvider securityProvider,
ILoginLogic loginLogic ILoginLogic loginLogic,
) IConfigHelper config
)
{ {
_dataProtector = securityProvider.CreateProtector("login"); _dataProtector = securityProvider.CreateProtector("login");
_logger = logger; _logger = logger;
_loginLogic = loginLogic; _loginLogic = loginLogic;
_config = config;
} }
public IActionResult Index(string redirectURL = "") public IActionResult Index(string redirectURL = "")
{ {
@ -42,6 +47,66 @@ namespace CarCareTracker.Controllers
{ {
return View(); return View();
} }
public IActionResult GetRemoteLoginLink()
{
var remoteAuthURL = _config.GetOpenIDConfig().RemoteAuthURL;
return Json(remoteAuthURL);
}
public async Task<IActionResult> RemoteAuth(string code)
{
try
{
if (!string.IsNullOrWhiteSpace(code))
{
//received code from OIDC provider
//create http client to retrieve user token from OIDC
var httpClient = new HttpClient();
var openIdConfig = _config.GetOpenIDConfig();
var httpParams = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("code", code),
new KeyValuePair<string, string>("grant_type", "authorization_code"),
new KeyValuePair<string, string>("client_id", openIdConfig.ClientId),
new KeyValuePair<string, string>("client_secret", openIdConfig.ClientSecret),
new KeyValuePair<string, string>("redirect_uri", openIdConfig.RedirectURL)
};
var httpRequest = new HttpRequestMessage(HttpMethod.Post, openIdConfig.TokenURL)
{
Content = new FormUrlEncodedContent(httpParams)
};
var tokenResult = await httpClient.SendAsync(httpRequest).Result.Content.ReadAsStringAsync();
var userJwt = JsonSerializer.Deserialize<OpenIDResult>(tokenResult)?.id_token ?? string.Empty;
if (!string.IsNullOrWhiteSpace(userJwt))
{
//validate JWT token
var tokenParser = new JwtSecurityTokenHandler();
var parsedToken = tokenParser.ReadJwtToken(userJwt);
var userEmailAddress = parsedToken.Claims.First(x => x.Type == "email").Value;
if (!string.IsNullOrWhiteSpace(userEmailAddress))
{
var userData = _loginLogic.ValidateOpenIDUser(new LoginModel() { EmailAddress = userEmailAddress });
if (userData.Id != default)
{
AuthCookie authCookie = new AuthCookie
{
UserData = userData,
ExpiresOn = DateTime.Now.AddDays(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 new RedirectResult("/Home");
}
}
}
}
} catch (Exception ex)
{
_logger.LogError(ex.Message);
return new RedirectResult("/Login");
}
return new RedirectResult("/Login");
}
[HttpPost] [HttpPost]
public IActionResult Login(LoginModel credentials) public IActionResult Login(LoginModel credentials)
{ {

View File

@ -7,6 +7,7 @@ namespace CarCareTracker.Helper
{ {
public interface IConfigHelper public interface IConfigHelper
{ {
OpenIDConfig GetOpenIDConfig();
UserConfig GetUserConfig(ClaimsPrincipal user); UserConfig GetUserConfig(ClaimsPrincipal user);
bool SaveUserConfig(ClaimsPrincipal user, UserConfig configData); bool SaveUserConfig(ClaimsPrincipal user, UserConfig configData);
string GetLogoUrl(); string GetLogoUrl();
@ -28,6 +29,11 @@ namespace CarCareTracker.Helper
_userConfig = userConfig; _userConfig = userConfig;
_cache = memoryCache; _cache = memoryCache;
} }
public OpenIDConfig GetOpenIDConfig()
{
OpenIDConfig openIdConfig = _config.GetSection("OpenID").Get<OpenIDConfig>() ?? new OpenIDConfig();
return openIdConfig;
}
public string GetLogoUrl() public string GetLogoUrl()
{ {
var logoUrl = _config["LUBELOGGER_LOGO_URL"]; var logoUrl = _config["LUBELOGGER_LOGO_URL"];

View File

@ -21,6 +21,7 @@ namespace CarCareTracker.Logic
OperationResponse ResetPasswordByUser(LoginModel credentials); OperationResponse ResetPasswordByUser(LoginModel credentials);
OperationResponse ResetUserPassword(LoginModel credentials); OperationResponse ResetUserPassword(LoginModel credentials);
UserData ValidateUserCredentials(LoginModel credentials); UserData ValidateUserCredentials(LoginModel credentials);
UserData ValidateOpenIDUser(LoginModel credentials);
bool CheckIfUserIsValid(int userId); bool CheckIfUserIsValid(int userId);
bool CreateRootUserCredentials(LoginModel credentials); bool CreateRootUserCredentials(LoginModel credentials);
bool DeleteRootUserCredentials(); bool DeleteRootUserCredentials();
@ -193,6 +194,19 @@ namespace CarCareTracker.Logic
} }
} }
} }
public UserData ValidateOpenIDUser(LoginModel credentials)
{
var result = _userData.GetUserRecordByEmailAddress(credentials.EmailAddress);
if (result.Id != default)
{
result.Password = string.Empty;
return result;
}
else
{
return new UserData();
}
}
#region "Admin Functions" #region "Admin Functions"
public bool MakeUserAdmin(int userId, bool isAdmin) public bool MakeUserAdmin(int userId, bool isAdmin)
{ {

View File

@ -0,0 +1,14 @@
namespace CarCareTracker.Models
{
public class OpenIDConfig
{
public string Name { get; set; }
public string ClientId { get; set; }
public string ClientSecret { get; set; }
public string AuthURL { get; set; }
public string TokenURL { get; set; }
public string RedirectURL { get; set; }
public string Scope { get; set; }
public string RemoteAuthURL { get { return $"{AuthURL}?client_id={ClientId}&response_type=code&redirect_uri={RedirectURL}&scope={Scope}"; } }
}
}

View File

@ -0,0 +1,7 @@
namespace CarCareTracker.Models
{
public class OpenIDResult
{
public string id_token { get; set; }
}
}

View File

@ -5,6 +5,7 @@
@{ @{
var logoUrl = config.GetLogoUrl(); var logoUrl = config.GetLogoUrl();
var userLanguage = config.GetServerLanguage(); var userLanguage = config.GetServerLanguage();
var openIdConfigName = config.GetOpenIDConfig().Name;
} }
@{ @{
ViewData["Title"] = "LubeLogger - Login"; ViewData["Title"] = "LubeLogger - Login";
@ -31,6 +32,12 @@
<div class="d-grid"> <div class="d-grid">
<button type="button" class="btn btn-warning mt-2" onclick="performLogin()"><i class="bi bi-box-arrow-in-right me-2"></i>@translator.Translate(userLanguage, "Login")</button> <button type="button" class="btn btn-warning mt-2" onclick="performLogin()"><i class="bi bi-box-arrow-in-right me-2"></i>@translator.Translate(userLanguage, "Login")</button>
</div> </div>
@if (!string.IsNullOrWhiteSpace(openIdConfigName))
{
<div class="d-grid">
<button type="button" class="btn btn-secondary mt-2" onclick="remoteLogin()"><i class="bi bi-box-arrow-in-right me-2"></i>@($"{translator.Translate(userLanguage, "Login via")} {openIdConfigName}")</button>
</div>
}
<div class="d-grid"> <div class="d-grid">
<a href="/Login/ForgotPassword" class="btn btn-link mt-2">@translator.Translate(userLanguage, "Forgot Password")</a> <a href="/Login/ForgotPassword" class="btn btn-link mt-2">@translator.Translate(userLanguage, "Forgot Password")</a>
</div> </div>
@ -41,7 +48,7 @@
</div> </div>
</div> </div>
<script> <script>
function getRedirectURL(){ function getRedirectURL() {
return { url: decodeHTMLEntities('@Model') }; return { url: decodeHTMLEntities('@Model') };
} }
</script> </script>

File diff suppressed because one or more lines are too long

View File

@ -59,4 +59,12 @@ function handlePasswordKeyPress(event) {
if (event.keyCode == 13) { if (event.keyCode == 13) {
performLogin(); performLogin();
} }
}
function remoteLogin() {
$.get('/Login/GetRemoteLoginLink', function (data) {
if (data) {
window.location.href = data;
}
})
} }