diff --git a/Controllers/LoginController.cs b/Controllers/LoginController.cs index a102f5d..b9abf2a 100644 --- a/Controllers/LoginController.cs +++ b/Controllers/LoginController.cs @@ -4,7 +4,6 @@ using CarCareTracker.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Mvc; -using System.IdentityModel.Tokens.Jwt; using System.Text.Json; namespace CarCareTracker.Controllers @@ -134,12 +133,10 @@ namespace CarCareTracker.Controllers 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 jwtResult = _loginLogic.ValidateOAuthToken(userJwt); + if (jwtResult.Success && !string.IsNullOrWhiteSpace(jwtResult.EmailAddress)) { - var userData = _loginLogic.ValidateOpenIDUser(new LoginModel() { EmailAddress = userEmailAddress }); + var userData = _loginLogic.ValidateOpenIDUser(new LoginModel { EmailAddress = jwtResult.EmailAddress }); if (userData.Id != default) { AuthCookie authCookie = new AuthCookie @@ -153,12 +150,15 @@ namespace CarCareTracker.Controllers return new RedirectResult("/Home"); } else { - _logger.LogInformation($"User {userEmailAddress} tried to login via OpenID but is not a registered user in LubeLogger."); - return View("OpenIDRegistration", model: userEmailAddress); + _logger.LogInformation($"User {jwtResult.EmailAddress} tried to login via OpenID but is not a registered user in LubeLogger."); + return View("OpenIDRegistration", model: jwtResult.EmailAddress); } - } else + } else if (jwtResult.Success) { _logger.LogInformation("OpenID Provider did not provide a valid email address for the user"); + } else + { + _logger.LogError("OpenID Token Failed Validation"); } } else { diff --git a/Helper/StaticHelper.cs b/Helper/StaticHelper.cs index 7c30a9b..ff5dc5b 100644 --- a/Helper/StaticHelper.cs +++ b/Helper/StaticHelper.cs @@ -303,6 +303,10 @@ namespace CarCareTracker.Helper { return new DateTimeOffset(date).ToUnixTimeMilliseconds(); } + public static long GetEpochFromDateTimeSeconds(DateTime date) + { + return new DateTimeOffset(date).ToUnixTimeSeconds(); + } public static void InitMessage(IConfiguration config) { Console.WriteLine($"LubeLogger {VersionNumber}"); diff --git a/Logic/LoginLogic.cs b/Logic/LoginLogic.cs index 22d2491..ffb65f0 100644 --- a/Logic/LoginLogic.cs +++ b/Logic/LoginLogic.cs @@ -3,6 +3,7 @@ using CarCareTracker.Helper; using CarCareTracker.Models; using Microsoft.Extensions.Caching.Memory; using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; using System.Security.Cryptography; using System.Text; using System.Text.Json; @@ -23,6 +24,7 @@ namespace CarCareTracker.Logic OperationResponse ResetUserPassword(LoginModel credentials); OperationResponse SendRegistrationToken(LoginModel credentials); UserData ValidateUserCredentials(LoginModel credentials); + JWTValidateResult ValidateOAuthToken(string jwtToken); UserData ValidateOpenIDUser(LoginModel credentials); bool CheckIfUserIsValid(int userId); bool CreateRootUserCredentials(LoginModel credentials); @@ -38,18 +40,20 @@ namespace CarCareTracker.Logic private readonly ITokenRecordDataAccess _tokenData; private readonly IMailHelper _mailHelper; private readonly IConfigHelper _configHelper; + private readonly ILogger _logger; private IMemoryCache _cache; public LoginLogic(IUserRecordDataAccess userData, ITokenRecordDataAccess tokenData, IMailHelper mailHelper, IConfigHelper configHelper, - IMemoryCache memoryCache) + IMemoryCache memoryCache, ILogger logger) { _userData = userData; _tokenData = tokenData; _mailHelper = mailHelper; _configHelper = configHelper; _cache = memoryCache; + _logger = logger; } public bool CheckIfUserIsValid(int userId) { @@ -273,6 +277,39 @@ namespace CarCareTracker.Logic } } } + public JWTValidateResult ValidateOAuthToken(string jwtToken) + { + var jwtResult = new JWTValidateResult(); + var tokenParser = new JwtSecurityTokenHandler(); + var openIdConfig = _configHelper.GetOpenIDConfig(); + try + { + var parsedToken = tokenParser.ReadJwtToken(jwtToken); + //Validate Token + var expiration = long.Parse(parsedToken.Claims.First(x => x.Type == "exp").Value); + var audience = parsedToken.Claims.First(x => x.Type == "aud").Value; + if (audience != openIdConfig.ClientId) + { + _logger.LogError($"Error Validating JWT Token: mismatch audience, expecting {openIdConfig.ClientId} but received {audience}"); + jwtResult.Success = false; + return jwtResult; + } + if (expiration < StaticHelper.GetEpochFromDateTimeSeconds(DateTime.Now)) + { + _logger.LogError($"Error Validating JWT Token: expired token"); + jwtResult.Success = false; + return jwtResult; + } + var userEmailAddress = parsedToken.Claims.First(x => x.Type == "email").Value; + jwtResult.EmailAddress = userEmailAddress; + jwtResult.Success = true; + } catch (Exception ex) + { + _logger.LogError($"Error Validating JWT Token: {ex.Message}"); + jwtResult.Success = false; + } + return jwtResult; + } public UserData ValidateOpenIDUser(LoginModel credentials) { //validate for root user diff --git a/Middleware/Authen.cs b/Middleware/Authen.cs index 32e86ab..f381af3 100644 --- a/Middleware/Authen.cs +++ b/Middleware/Authen.cs @@ -58,39 +58,51 @@ namespace CarCareTracker.Middleware } else if (!string.IsNullOrWhiteSpace(request_header)) { - var cleanedHeader = request_header.ToString().Replace("Basic ", "").Trim(); - byte[] data = Convert.FromBase64String(cleanedHeader); - string decodedString = Encoding.UTF8.GetString(data); - var splitString = decodedString.Split(":"); - if (splitString.Count() != 2) + bool useBearerAuth = request_header.ToString().Contains("Bearer"); + var cleanedHeader = useBearerAuth ? request_header.ToString().Replace("Bearer ", "").Trim() : request_header.ToString().Replace("Basic ", "").Trim(); + var userData = new UserData(); + if (useBearerAuth) { - return AuthenticateResult.Fail("Invalid credentials"); - } + //validate OpenID User from Bearer token + var jwtResult = _loginLogic.ValidateOAuthToken(cleanedHeader); + if (jwtResult.Success) + { + userData = _loginLogic.ValidateOpenIDUser(new LoginModel { EmailAddress = jwtResult.EmailAddress }); + } + } else { - var userData = _loginLogic.ValidateUserCredentials(new LoginModel { UserName = splitString[0], Password = splitString[1] }); - if (userData.Id != default) + //perform basic auth. + byte[] data = Convert.FromBase64String(cleanedHeader); + string decodedString = Encoding.UTF8.GetString(data); + var splitString = decodedString.Split(":"); + if (splitString.Count() != 2) { - var appIdentity = new ClaimsIdentity("Custom"); - var userIdentity = new List - { - new(ClaimTypes.Name, splitString[0]), - new(ClaimTypes.NameIdentifier, userData.Id.ToString()), - new(ClaimTypes.Email, userData.EmailAddress), - new(ClaimTypes.Role, "APIAuth") - }; - if (userData.IsAdmin) - { - userIdentity.Add(new(ClaimTypes.Role, nameof(UserData.IsAdmin))); - } - if (userData.IsRootUser) - { - userIdentity.Add(new(ClaimTypes.Role, nameof(UserData.IsRootUser))); - } - appIdentity.AddClaims(userIdentity); - AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), Scheme.Name); - return AuthenticateResult.Success(ticket); + return AuthenticateResult.Fail("Invalid credentials"); } + userData = _loginLogic.ValidateUserCredentials(new LoginModel { UserName = splitString[0], Password = splitString[1] }); + } + if (userData.Id != default) + { + var appIdentity = new ClaimsIdentity("Custom"); + var userIdentity = new List + { + new(ClaimTypes.Name, userData.UserName), + new(ClaimTypes.NameIdentifier, userData.Id.ToString()), + new(ClaimTypes.Email, userData.EmailAddress), + new(ClaimTypes.Role, "APIAuth") + }; + if (userData.IsAdmin) + { + userIdentity.Add(new(ClaimTypes.Role, nameof(UserData.IsAdmin))); + } + if (userData.IsRootUser) + { + userIdentity.Add(new(ClaimTypes.Role, nameof(UserData.IsRootUser))); + } + appIdentity.AddClaims(userIdentity); + AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), Scheme.Name); + return AuthenticateResult.Success(ticket); } } else if (!string.IsNullOrWhiteSpace(access_token)) diff --git a/Models/OIDC/JWTValidateResult.cs b/Models/OIDC/JWTValidateResult.cs new file mode 100644 index 0000000..135238a --- /dev/null +++ b/Models/OIDC/JWTValidateResult.cs @@ -0,0 +1,8 @@ +namespace CarCareTracker.Models +{ + public class JWTValidateResult + { + public bool Success { get; set; } + public string EmailAddress { get; set; } + } +}