mesa-fab-approval/Fab2ApprovalMKLink/Services/AuthenticationService.cs
Mike Phares b586da5c82 Removed PdfViewController, HtmlViewRenderer and FakeView to be replaced with ViewEngineResult Render method
Added HttpException class for missing HttpException for net8

Wrapped HttpContext.Session, GetJsonResult, IsAjaxRequest and GetUserIdentityName in controllers for net8

Added AuthenticationService to test Fab2ApprovalMKLink code for net8

Compile conditionally flags to debug in dotnet core
2025-05-23 12:27:09 -07:00

266 lines
9.8 KiB
C#

using System;
using System.Collections.Generic;
using System.DirectoryServices.AccountManagement;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Authentication;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;
using Fab2ApprovalSystem.Models;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
namespace Fab2ApprovalSystem.Services;
public interface IAuthenticationService {
public Task<LoginResult> AuthenticateUser(AuthAttempt login);
public Task<LoginResult> AttemptLocalUserAuth(WindowsIdentity identity);
public AuthTokens GenerateAuthTokens(AuthAttempt authAttempt, IEnumerable<string> roles);
public Task<LoginResult> RefreshAuthTokens(AuthAttempt authAttempt);
}
public class AuthenticationService : IAuthenticationService {
private readonly ILogger<AuthenticationService> _logger;
private readonly IMemoryCache _cache;
private readonly IUserService _userService;
private readonly string? _jwtIssuer;
private readonly string? _jwtAudience;
private readonly string? _jwtKey;
public AuthenticationService(ILogger<AuthenticationService> logger, IMemoryCache cache, IUserService userService, AppSettings appSettings) {
_logger = logger ?? throw new ArgumentNullException("ILogger not injected");
_cache = cache ?? throw new ArgumentNullException("IMemoryCache not injected");
_userService = userService ?? throw new ArgumentNullException("IUserService not injected");
_jwtKey = appSettings.JwtKey;
_jwtIssuer = appSettings.JwtIssuer;
_jwtAudience = appSettings.JwtAudience;
}
public async Task<LoginResult> AuthenticateUser(AuthAttempt login) {
try {
_logger.LogInformation("Attempting to authenticate user");
if (login is null)
throw new ArgumentNullException("Login cannot be null");
string domain = "infineon.com";
using (PrincipalContext pc = new(ContextType.Domain, domain)) {
bool isValid = pc.ValidateCredentials(login.LoginID, login.Password);
if (isValid) {
User? user = _cache.Get<User>($"user{login.LoginID}");
if (user is null) {
user = await _userService.GetUserByLoginId(login.LoginID);
_cache.Set($"user{login.LoginID}", user, DateTimeOffset.Now.AddDays(1));
}
List<string> roles = new();
if (user.IsManager)
roles.Add("manager");
if (user.IsAdmin)
roles.Add("admin");
AuthTokens tokens = GenerateAuthTokens(login, roles);
return new LoginResult {
IsAuthenticated = true,
AuthTokens = tokens,
User = user
};
} else {
return new LoginResult() {
IsAuthenticated = false,
AuthTokens = new() {
JwtToken = "",
RefreshToken = ""
},
User = null
};
}
}
} catch (Exception ex) {
_logger.LogError($"An exception occurred when attempting to authenticate user. Exception: {ex.Message}");
throw;
}
}
public async Task<LoginResult> AttemptLocalUserAuth(WindowsIdentity identity) {
try {
_logger.LogInformation("Attempting to authenticate local Windows system user");
if (identity is null)
throw new ArgumentNullException("WindowsIdentity cannot be null");
User user = await _userService.GetUserByLoginId(identity.Name);
List<string> roles = new();
if (user.IsManager)
roles.Add("manager");
if (user.IsAdmin)
roles.Add("admin");
AuthAttempt authAttempt = new() {
LoginID = user.LoginID,
};
AuthTokens tokens = GenerateAuthTokens(authAttempt, roles);
return new LoginResult {
IsAuthenticated = true,
AuthTokens = tokens,
User = user
};
} catch (Exception ex) {
_logger.LogError($"Unable to authenticate local Windows system user, because {ex.Message}");
throw;
}
}
public AuthTokens GenerateAuthTokens(AuthAttempt authAttempt, IEnumerable<string> roles) {
try {
_logger.LogInformation("Attempting to generate JWT");
if (authAttempt is null)
throw new ArgumentNullException("AuthAttempt cannot be null");
if (string.IsNullOrWhiteSpace(authAttempt.LoginID))
throw new ArgumentException("UserName cannot be null or empty");
if (roles is null)
throw new ArgumentNullException("roles cannot be null");
byte[] key = Encoding.ASCII.GetBytes(_jwtKey);
List<Claim> claims = new() {
new Claim(nameof(authAttempt.LoginID), authAttempt.LoginID)
};
foreach (string role in roles) {
claims.Add(new Claim(ClaimTypes.Role, role));
}
ClaimsIdentity identity = new(claims);
SecurityTokenDescriptor tokenDescriptor = new() {
Issuer = _jwtIssuer,
Audience = _jwtAudience,
Subject = identity,
NotBefore = DateTime.Now,
Expires = DateTime.Now.AddHours(8),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
JwtSecurityTokenHandler tokenHandler = new();
JwtSecurityToken token = tokenHandler.CreateJwtSecurityToken(tokenDescriptor);
string jwt = tokenHandler.WriteToken(token);
string refreshToken = GenerateRefreshToken();
List<string>? refreshTokensForUser = _cache.Get<List<string>>(authAttempt.LoginID);
refreshTokensForUser ??= new List<string>();
if (refreshTokensForUser.Count > 9)
refreshTokensForUser.RemoveRange(9, refreshTokensForUser.Count - 9);
refreshTokensForUser.Insert(0, refreshToken);
_cache.Set(authAttempt.LoginID, refreshTokensForUser, DateTimeOffset.Now.AddHours(4));
return new AuthTokens {
JwtToken = jwt,
RefreshToken = refreshToken
};
} catch (Exception ex) {
_logger.LogError($"An exception occurred when attempting to generate JWT. Exception: {ex.Message}");
throw;
}
}
public async Task<LoginResult> RefreshAuthTokens(AuthAttempt authAttempt) {
try {
_logger.LogInformation("Attempting to refresh auth tokens");
if (authAttempt is null)
throw new ArgumentNullException("AuthAttempt cannot be null");
if (authAttempt.AuthTokens is null)
throw new ArgumentNullException("AuthTokens cannot be null");
bool refreshTokenIsValid = IsRefreshTokenValid(authAttempt.LoginID, authAttempt.AuthTokens.RefreshToken);
if (refreshTokenIsValid) {
User? user = _cache.Get<User>($"user{authAttempt.LoginID}");
if (user is null) {
user = await _userService.GetUserByLoginId(authAttempt.LoginID);
_cache.Set($"user{authAttempt.LoginID}", user, DateTimeOffset.Now.AddDays(1));
}
List<string> roles = new();
if (user.IsManager)
roles.Add("manager");
if (user.IsAdmin)
roles.Add("admin");
AuthTokens refreshedTokens = GenerateAuthTokens(authAttempt, roles);
LoginResult loginResult = new() {
IsAuthenticated = true,
AuthTokens = refreshedTokens,
User = user
};
return loginResult;
} else {
throw new AuthenticationException("Invalid refresh token");
}
} catch (Exception ex) {
_logger.LogError($"An exception occurred when attempting to refresh auth tokens. Exception: {ex.Message}");
throw;
}
}
private string GenerateRefreshToken() {
byte[] randomNumber = new byte[32];
using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) {
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
}
private bool IsRefreshTokenValid(string loginId, string refreshToken) {
try {
_logger.LogInformation("Attempting to determine if refresh token is valid");
if (string.IsNullOrWhiteSpace(loginId))
throw new ArgumentNullException("LoginID cannot be null or empty");
if (string.IsNullOrWhiteSpace(refreshToken))
throw new ArgumentNullException("Refresh token cannot be null or empty");
List<string>? cachedRefreshTokensForUser = _cache.Get<List<string>>(loginId);
if (cachedRefreshTokensForUser is null || !cachedRefreshTokensForUser.Contains(refreshToken)) {
_logger.LogInformation($"Could not find cached refresh tokens for user {loginId}");
return false;
}
return true;
} catch (Exception ex) {
_logger.LogError($"An exception occurred when attempting to validate refresh token. Exception: {ex.Message}");
throw;
}
}
}