252 lines
8.9 KiB
C#
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)
|
|
};
|
|
}
|
|
}
|