255 lines
10 KiB
C#
255 lines
10 KiB
C#
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 MesaFabApproval.API.Services;
|
|
using MesaFabApproval.Shared.Models;
|
|
|
|
using Microsoft.Extensions.Caching.Memory;
|
|
using Microsoft.IdentityModel.Tokens;
|
|
|
|
namespace MesaFabApprovalAPI.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) {
|
|
_logger = logger ?? throw new ArgumentNullException("ILogger not injected");
|
|
_cache = cache ?? throw new ArgumentNullException("IMemoryCache not injected");
|
|
_userService = userService ?? throw new ArgumentNullException("IUserService not injected");
|
|
|
|
_jwtIssuer = Environment.GetEnvironmentVariable("FabApprovalJwtIssuer") ??
|
|
throw new ArgumentNullException("FabApprovalJwtIssuer environment variable not found");
|
|
_jwtAudience = Environment.GetEnvironmentVariable("FabApprovalJwtAudience") ??
|
|
throw new ArgumentNullException("FabApprovalJwtAudience environment variable not found");
|
|
_jwtKey = Environment.GetEnvironmentVariable("FabApprovalJwtKey") ??
|
|
throw new ArgumentNullException("FabApprovalJwtKey environment variable not found");
|
|
}
|
|
|
|
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 PrincipalContext(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>($"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 ClaimsIdentity(claims);
|
|
|
|
SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor {
|
|
Issuer = _jwtIssuer,
|
|
Audience = _jwtAudience,
|
|
Subject = identity,
|
|
NotBefore = DateTime.Now,
|
|
Expires = DateTime.Now.AddHours(2),
|
|
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
|
|
};
|
|
|
|
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
|
|
|
|
JwtSecurityToken token = tokenHandler.CreateJwtSecurityToken(tokenDescriptor);
|
|
|
|
string jwt = tokenHandler.WriteToken(token);
|
|
|
|
string refreshToken = GenerateRefreshToken();
|
|
|
|
List<string>? refreshTokensForUser = _cache.Get<List<string>>(authAttempt.LoginID);
|
|
|
|
if (refreshTokensForUser is null)
|
|
refreshTokensForUser = new List<string>();
|
|
|
|
if (refreshTokensForUser.Count > 9)
|
|
refreshTokensForUser.RemoveRange(9, refreshTokensForUser.Count - 9);
|
|
|
|
refreshTokensForUser.Insert(0, refreshToken);
|
|
|
|
_cache.Set<List<string>>(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>($"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 LoginResult() {
|
|
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;
|
|
}
|
|
}
|
|
}
|