http-api/src/Application/Authentication/Queries/Login/LoginQueryHandler.cs
2025-05-28 12:33:49 +03:00

141 lines
4.9 KiB
C#

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using cuqmbr.TravelGuide.Application.Common.Services;
using cuqmbr.TravelGuide.Domain.Entities;
using MediatR;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login;
public class LoginQueryHandler : IRequestHandler<LoginQuery, TokensModel>
{
private readonly UnitOfWork _unitOfWork;
private readonly PasswordHasherService _passwordHasher;
private readonly JsonWebTokenConfigurationOptions _jwtConfiguration;
public LoginQueryHandler(UnitOfWork unitOfWork,
PasswordHasherService passwordHasher,
IOptions<ConfigurationOptions> configurationOptions)
{
_unitOfWork = unitOfWork;
_passwordHasher = passwordHasher;
_jwtConfiguration = configurationOptions.Value.JsonWebToken;
}
public async Task<TokensModel> Handle(
LoginQuery request, CancellationToken cancellationToken)
{
var account = await _unitOfWork.AccountRepository
.GetOneAsync(
a =>
a.Email == request.EmailOrUsername ||
a.Username == request.EmailOrUsername,
a => a.AccountRoles, cancellationToken);
if (account == null)
{
throw new LoginException("No users registered with given email.");
}
var hash = Convert.FromBase64String(account.PasswordHash);
var salt = Convert.FromBase64String(account.PasswordSalt);
var password = Encoding.UTF8.GetBytes(request.Password);
var isValidPassword = await _passwordHasher
.IsValidHashAsync(hash, password, salt, cancellationToken);
if (!isValidPassword)
{
throw new LoginException("Given password is incorrect.");
}
var jwtSecurityToken = await CreateJwtAsync(account, cancellationToken);
var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
var refreshToken = (await _unitOfWork.RefreshTokenRepository
.GetPageAsync(
e =>
e.AccountId == account.Id &&
e.RevocationTime == null &&
e.ExpirationTime > DateTimeOffset.UtcNow,
1, int.MaxValue, cancellationToken))
.Items.FirstOrDefault();
if (refreshToken == null)
{
refreshToken = CreateRefreshToken();
refreshToken.AccountId = account.Id;
await _unitOfWork.RefreshTokenRepository
.AddOneAsync(refreshToken, cancellationToken);
}
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
return new TokensModel(accessToken, refreshToken.Value);
}
private async Task<JwtSecurityToken> CreateJwtAsync(
Account account, CancellationToken cancellationToken)
{
var roleIds = account.AccountRoles.Select(ar => ar.RoleId);
var roles = (await _unitOfWork.RoleRepository
.GetPageAsync(
r => roleIds.Contains(r.Id),
1, roleIds.Count(), cancellationToken))
.Items.Select(r => r.Value);
var roleClaims = new List<Claim>();
foreach (var role in roles)
{
roleClaims.Add(new Claim("roles", role.Name));
}
var claims = new List<Claim>()
{
new Claim(JwtRegisteredClaimNames.Sub, account.Guid.ToString()),
new Claim(JwtRegisteredClaimNames.Nickname, account.Username),
new Claim(JwtRegisteredClaimNames.Email, account.Email)
}
.Union(roleClaims);
var expirationDateTimeUtc = DateTime.UtcNow.Add(
_jwtConfiguration.AccessTokenValidity);
var symmetricSecurityKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_jwtConfiguration.IssuerSigningKey));
var signingCredentials = new SigningCredentials(
symmetricSecurityKey, SecurityAlgorithms.HmacSha256);
var jwtSecurityToken = new JwtSecurityToken(
issuer: _jwtConfiguration.Issuer,
audience: _jwtConfiguration.Audience,
claims: claims,
expires: expirationDateTimeUtc,
signingCredentials: signingCredentials);
return jwtSecurityToken;
}
private RefreshToken CreateRefreshToken()
{
var token = RandomNumberGenerator.GetBytes(128 / 8);
return new RefreshToken
{
Guid = Guid.NewGuid(),
Value = Convert.ToBase64String(token),
CreationTime = DateTimeOffset.UtcNow,
ExpirationTime = DateTimeOffset.UtcNow.Add(
_jwtConfiguration.RefreshTokenValidity)
};
}
}