classlib/ExpenseTracker.Infrastructure/Identity/Services/AuthenticationService.cs
2024-08-07 21:12:02 +03:00

252 lines
8.9 KiB
C#

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<ApplicationUser> _userManager;
private readonly RoleManager<ApplicationRole> _roleManager;
private readonly IEmailSenderService _emailSenderService;
public AuthenticationService(
IConfiguration configuration,
UserManager<ApplicationUser> userManager,
RoleManager<ApplicationRole> 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<string>[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<string>[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<TokensModel> 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<TokensModel> 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<JwtSecurityToken> CreateJwtAsync(ApplicationUser user, CancellationToken cancellationToken)
{
var userClaims = await _userManager.GetClaimsAsync(user);
var roles = await _userManager.GetRolesAsync(user);
var roleClaims = new List<Claim>();
foreach (var role in roles)
{
roleClaims.Add(new Claim("roles", role));
}
var claims = new List<Claim>()
{
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<string> CreateRefreshToken()
{
var randomNumber = new byte[32];
using var rng = RandomNumberGenerator.Create();
rng.GetNonZeroBytes(randomNumber);
var validityInDays = Double.Parse(_configuration["Jwt:RefreshTokenValidityInDays"]);
return new RefreshToken<string>
{
Id = Guid.NewGuid().ToString(),
Value = Convert.ToBase64String(randomNumber),
CreationDateTimeUtc = DateTime.UtcNow,
ExpirationDateTimeUtc = DateTime.UtcNow.AddDays(validityInDays)
};
}
}