Allow APIs to use JWT Bearer auth.

This commit is contained in:
DESKTOP-T0O5CDB\DESK-555BD
2025-02-12 12:59:10 -07:00
parent efa2bbf6cc
commit 21846c8957
5 changed files with 99 additions and 38 deletions

View File

@@ -4,7 +4,6 @@ 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 System.IdentityModel.Tokens.Jwt;
using System.Text.Json; using System.Text.Json;
namespace CarCareTracker.Controllers namespace CarCareTracker.Controllers
@@ -134,12 +133,10 @@ namespace CarCareTracker.Controllers
if (!string.IsNullOrWhiteSpace(userJwt)) if (!string.IsNullOrWhiteSpace(userJwt))
{ {
//validate JWT token //validate JWT token
var tokenParser = new JwtSecurityTokenHandler(); var jwtResult = _loginLogic.ValidateOAuthToken(userJwt);
var parsedToken = tokenParser.ReadJwtToken(userJwt); if (jwtResult.Success && !string.IsNullOrWhiteSpace(jwtResult.EmailAddress))
var userEmailAddress = parsedToken.Claims.First(x => x.Type == "email").Value;
if (!string.IsNullOrWhiteSpace(userEmailAddress))
{ {
var userData = _loginLogic.ValidateOpenIDUser(new LoginModel() { EmailAddress = userEmailAddress }); var userData = _loginLogic.ValidateOpenIDUser(new LoginModel { EmailAddress = jwtResult.EmailAddress });
if (userData.Id != default) if (userData.Id != default)
{ {
AuthCookie authCookie = new AuthCookie AuthCookie authCookie = new AuthCookie
@@ -153,12 +150,15 @@ namespace CarCareTracker.Controllers
return new RedirectResult("/Home"); return new RedirectResult("/Home");
} else } else
{ {
_logger.LogInformation($"User {userEmailAddress} tried to login via OpenID but is not a registered user in LubeLogger."); _logger.LogInformation($"User {jwtResult.EmailAddress} tried to login via OpenID but is not a registered user in LubeLogger.");
return View("OpenIDRegistration", model: userEmailAddress); 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"); _logger.LogInformation("OpenID Provider did not provide a valid email address for the user");
} else
{
_logger.LogError("OpenID Token Failed Validation");
} }
} else } else
{ {

View File

@@ -303,6 +303,10 @@ namespace CarCareTracker.Helper
{ {
return new DateTimeOffset(date).ToUnixTimeMilliseconds(); return new DateTimeOffset(date).ToUnixTimeMilliseconds();
} }
public static long GetEpochFromDateTimeSeconds(DateTime date)
{
return new DateTimeOffset(date).ToUnixTimeSeconds();
}
public static void InitMessage(IConfiguration config) public static void InitMessage(IConfiguration config)
{ {
Console.WriteLine($"LubeLogger {VersionNumber}"); Console.WriteLine($"LubeLogger {VersionNumber}");

View File

@@ -3,6 +3,7 @@ using CarCareTracker.Helper;
using CarCareTracker.Models; using CarCareTracker.Models;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
@@ -23,6 +24,7 @@ namespace CarCareTracker.Logic
OperationResponse ResetUserPassword(LoginModel credentials); OperationResponse ResetUserPassword(LoginModel credentials);
OperationResponse SendRegistrationToken(LoginModel credentials); OperationResponse SendRegistrationToken(LoginModel credentials);
UserData ValidateUserCredentials(LoginModel credentials); UserData ValidateUserCredentials(LoginModel credentials);
JWTValidateResult ValidateOAuthToken(string jwtToken);
UserData ValidateOpenIDUser(LoginModel credentials); UserData ValidateOpenIDUser(LoginModel credentials);
bool CheckIfUserIsValid(int userId); bool CheckIfUserIsValid(int userId);
bool CreateRootUserCredentials(LoginModel credentials); bool CreateRootUserCredentials(LoginModel credentials);
@@ -38,18 +40,20 @@ namespace CarCareTracker.Logic
private readonly ITokenRecordDataAccess _tokenData; private readonly ITokenRecordDataAccess _tokenData;
private readonly IMailHelper _mailHelper; private readonly IMailHelper _mailHelper;
private readonly IConfigHelper _configHelper; private readonly IConfigHelper _configHelper;
private readonly ILogger<LoginLogic> _logger;
private IMemoryCache _cache; private IMemoryCache _cache;
public LoginLogic(IUserRecordDataAccess userData, public LoginLogic(IUserRecordDataAccess userData,
ITokenRecordDataAccess tokenData, ITokenRecordDataAccess tokenData,
IMailHelper mailHelper, IMailHelper mailHelper,
IConfigHelper configHelper, IConfigHelper configHelper,
IMemoryCache memoryCache) IMemoryCache memoryCache, ILogger<LoginLogic> logger)
{ {
_userData = userData; _userData = userData;
_tokenData = tokenData; _tokenData = tokenData;
_mailHelper = mailHelper; _mailHelper = mailHelper;
_configHelper = configHelper; _configHelper = configHelper;
_cache = memoryCache; _cache = memoryCache;
_logger = logger;
} }
public bool CheckIfUserIsValid(int userId) 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) public UserData ValidateOpenIDUser(LoginModel credentials)
{ {
//validate for root user //validate for root user

View File

@@ -58,39 +58,51 @@ namespace CarCareTracker.Middleware
} }
else if (!string.IsNullOrWhiteSpace(request_header)) else if (!string.IsNullOrWhiteSpace(request_header))
{ {
var cleanedHeader = request_header.ToString().Replace("Basic ", "").Trim(); bool useBearerAuth = request_header.ToString().Contains("Bearer");
byte[] data = Convert.FromBase64String(cleanedHeader); var cleanedHeader = useBearerAuth ? request_header.ToString().Replace("Bearer ", "").Trim() : request_header.ToString().Replace("Basic ", "").Trim();
string decodedString = Encoding.UTF8.GetString(data); var userData = new UserData();
var splitString = decodedString.Split(":"); if (useBearerAuth)
if (splitString.Count() != 2)
{ {
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 else
{ {
var userData = _loginLogic.ValidateUserCredentials(new LoginModel { UserName = splitString[0], Password = splitString[1] }); //perform basic auth.
if (userData.Id != default) byte[] data = Convert.FromBase64String(cleanedHeader);
string decodedString = Encoding.UTF8.GetString(data);
var splitString = decodedString.Split(":");
if (splitString.Count() != 2)
{ {
var appIdentity = new ClaimsIdentity("Custom"); return AuthenticateResult.Fail("Invalid credentials");
var userIdentity = new List<Claim>
{
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);
} }
userData = _loginLogic.ValidateUserCredentials(new LoginModel { UserName = splitString[0], Password = splitString[1] });
}
if (userData.Id != default)
{
var appIdentity = new ClaimsIdentity("Custom");
var userIdentity = new List<Claim>
{
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)) else if (!string.IsNullOrWhiteSpace(access_token))

View File

@@ -0,0 +1,8 @@
namespace CarCareTracker.Models
{
public class JWTValidateResult
{
public bool Success { get; set; }
public string EmailAddress { get; set; }
}
}