using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Security.Cryptography; using System.Text; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; using ExpenseTracker.Application.Common.Exceptions; using ExpenseTracker.Application.Common.Interfaces.Services; using ExpenseTracker.Application.Common.Models; using ExpenseTracker.Application.Authentication; using ExpenseTracker.Infrastructure.Identity.Models; namespace ExpenseTracker.Infrastructure.Identity.Services; public class AuthenticationService : IAuthenticationService { private readonly IConfiguration _configuration; private readonly UserManager _userManager; private readonly RoleManager _roleManager; private readonly IEmailSenderService _emailSenderService; public AuthenticationService( IConfiguration configuration, UserManager userManager, RoleManager roleManager, IEmailSenderService emailSenderService) { _userManager = userManager; _userManager.UserValidators.Clear(); _userManager.PasswordValidators.Clear(); _roleManager = roleManager; _configuration = configuration; _emailSenderService = emailSenderService; } public async Task RegisterWithEmailAndPasswordAsync(string email, string password, CancellationToken cancellationToken) { var userWithSameEmail = await _userManager.FindByEmailAsync(email); if (userWithSameEmail is not null) { throw new RegistrationException("User with given email already registered."); } var roles = _roleManager.Roles .Where(r => r.Name == IdentityRoles.User.ToString()) .Select(r => r.Id) .ToList(); var newUser = new ApplicationUser { Id = Guid.NewGuid().ToString(), Email = email, Roles = roles, RefreshTokens = new RefreshToken[0] }; var createUserResult = await _userManager.CreateAsync(newUser, password); if (createUserResult.Errors.Any()) { throw new Exception(); } } public async Task RegisterWithEmailAsync(string email, CancellationToken cancellationToken) { var userWithSameEmail = await _userManager.FindByEmailAsync(email); if (userWithSameEmail is not null) { throw new RegistrationException("User with given email already registered."); } var roles = _roleManager.Roles .Where(r => r.Name == IdentityRoles.User.ToString()) .Select(r => r.Id) .ToList(); var newUser = new ApplicationUser { Id = Guid.NewGuid().ToString(), Email = email, EmailConfirmed = true, Roles = roles, RefreshTokens =new RefreshToken[0] }; var randomPassword = GenerateRandomPassword(16); var createUserResult = await _userManager.CreateAsync(newUser, randomPassword); if (createUserResult.Errors.Any()) { throw new Exception(); } await _emailSenderService.SendAsync( new string[] { email }, "Expense Tracker Account Registration", $"Account registered successfuly.\n\n" + $"Your login credentials are:\n" + $" - email: {email}\n" + $" - password: {randomPassword}\n\n" + $"Do not worry if you did not register the account." + " All accounts without activity are being cleaned up periodically.", cancellationToken); string GenerateRandomPassword(int length) { string allowedCharacters = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "1234567890" + "`-=~!@#$%^&*()_+" + "[]{};:',<.>/?|\\\""; var rng = new Random(DateTime.UtcNow.Microsecond); var sb = new StringBuilder(); int randomIndex; for (int i = 0; i < length; i++) { randomIndex = rng.Next(allowedCharacters.Length); sb.Append(allowedCharacters[randomIndex]); } return sb.ToString(); } } public async Task LoginAsync(string email, string password, CancellationToken cancellationToken) { var user = await _userManager.FindByEmailAsync(email); if (user is null) { throw new LoginException("No users registered with given email."); } var isPasswordCorrect = await _userManager.CheckPasswordAsync(user, password); if (!isPasswordCorrect) { throw new LoginException("Given password is incorrect."); } var jwtSecurityToken = await CreateJwtAsync(user, cancellationToken); var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); var refreshToken = user.RefreshTokens.FirstOrDefault(t => t.IsActive); if (refreshToken is null) { refreshToken = CreateRefreshToken(); refreshToken.ApplicationUserId = user.Id; user.RefreshTokens.Add(refreshToken); var result = await _userManager.UpdateAsync(user); } return new TokensModel(accessToken, refreshToken.Value); } public async Task RenewAccessTokenAsync(string refreshToken, CancellationToken cancellationToken) { var user = _userManager.Users.SingleOrDefault(u => u.RefreshTokens.Any(rt => rt.Value == refreshToken)); if (user is null) { throw new RenewAccessTokenException($"Refresh token {refreshToken} was not found."); } var refreshTokenObject = user.RefreshTokens.Single(rt => rt.Value == refreshToken); if (!refreshTokenObject.IsActive) { throw new RenewAccessTokenException("Refresh token is inactive."); } var jwtSecurityToken = await CreateJwtAsync(user, cancellationToken); var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); return new TokensModel(accessToken, refreshToken); } public async Task RevokeRefreshTokenAsync(string refreshToken, CancellationToken cancellationToken) { var user = _userManager.Users.SingleOrDefault(u => u.RefreshTokens.Any(t => t.Value == refreshToken)); if (user is null) { throw new RevokeRefreshTokenException("Invalid refreshToken"); } var refreshTokenObject = user.RefreshTokens.Single(x => x.Value == refreshToken); if (!refreshTokenObject.IsActive) { throw new RevokeRefreshTokenException("RefreshToken already revoked"); } refreshTokenObject.RevokationDateTimeUtc = DateTime.UtcNow; await _userManager.UpdateAsync(user); } private async Task CreateJwtAsync(ApplicationUser user, CancellationToken cancellationToken) { var userClaims = await _userManager.GetClaimsAsync(user); var roles = await _userManager.GetRolesAsync(user); var roleClaims = new List(); foreach (var role in roles) { roleClaims.Add(new Claim("roles", role)); } var claims = new List() { new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()), new Claim(JwtRegisteredClaimNames.Email, user.Email) } .Union(userClaims) .Union(roleClaims); var validityInMinutes = Double.Parse(_configuration["Jwt:AccessTokenValidityInMinutes"]); var expirationDateTimeUtc = DateTime.UtcNow.AddMinutes(validityInMinutes); var symmetricSecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:IssuerSigningKey"])); var signingCredentials = new SigningCredentials(symmetricSecurityKey, SecurityAlgorithms.HmacSha256); var jwtSecurityToken = new JwtSecurityToken( issuer: _configuration["Jwt:Issuer"], audience: _configuration["Jwt:Audience"], claims: claims, expires: expirationDateTimeUtc, signingCredentials: signingCredentials); return jwtSecurityToken; } private RefreshToken CreateRefreshToken() { var randomNumber = new byte[32]; using var rng = RandomNumberGenerator.Create(); rng.GetNonZeroBytes(randomNumber); var validityInDays = Double.Parse(_configuration["Jwt:RefreshTokenValidityInDays"]); return new RefreshToken { Id = Guid.NewGuid().ToString(), Value = Convert.ToBase64String(randomNumber), CreationDateTimeUtc = DateTime.UtcNow, ExpirationDateTimeUtc = DateTime.UtcNow.AddDays(validityInDays) }; } }