galaeth-draft/Galaeth.Services/Services/AuthenticationService.cs

255 lines
9.5 KiB
C#
Raw Permalink Normal View History

2024-11-17 10:31:01 +01:00
using System.IdentityModel.Tokens.Jwt;
using System.Net;
using System.Security.Claims;
using System.Text;
using FluentValidation;
using Galaeth.Core.Configuration;
using Galaeth.Core.Constants;
using Galaeth.Core.Entities;
using Galaeth.Core.Extensions;
using Galaeth.Core.Infrastructure;
using Galaeth.Core.RepositoryInterfaces;
using Galaeth.Services.Constants;
using Galaeth.Services.Dtos;
using Galaeth.Services.Exceptions;
using Galaeth.Services.Interfaces;
using Injectio.Attributes;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using MyCSharp.HttpUserAgentParser;
namespace Galaeth.Services.Services;
/// <inheritdoc />
[RegisterScoped]
public class AuthenticationService : IAuthenticationService
{
/// <summary>
/// The length of the refresh token.
/// </summary>
private const int RefreshTokenLength = 128;
private readonly ICryptographyService _cryptographyService;
private readonly IRefreshTokenRepository _refreshTokenRepository;
private readonly IIdentityProvider _identityProvider;
private readonly IUserRepository _userRepository;
private readonly ITransactionProvider _transactionProvider;
private readonly IValidator<AuthenticateUserDto> _authenticateUserValidator;
private readonly JwtConfiguration _jwtConfiguration;
/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationService"/> class.
/// </summary>
/// <param name="cryptographyService">Instance of <see cref="ICryptographyService"/>.</param>
/// <param name="refreshTokenRepository">Instance of <see cref="IRefreshTokenRepository"/>.</param>
/// <param name="identityProvider">Instance of <see cref="IIdentityProvider"/>.</param>
/// <param name="userRepository">Instance of <see cref="IUserRepository"/>.</param>
/// <param name="transactionProvider">Instance of <see cref="ITransactionProvider"/>.</param>
/// <param name="authenticateUserValidator">Instance of <see cref="IValidator{AuthenticateUserDto}"/>.</param>
/// <param name="jwtConfig">Instance of <see cref="IOptions{JwtConfiguration}"/>.</param>
public AuthenticationService(
ICryptographyService cryptographyService,
IRefreshTokenRepository refreshTokenRepository,
IIdentityProvider identityProvider,
IUserRepository userRepository,
ITransactionProvider transactionProvider,
IValidator<AuthenticateUserDto> authenticateUserValidator,
IOptions<JwtConfiguration> jwtConfig)
{
_cryptographyService = cryptographyService;
_refreshTokenRepository = refreshTokenRepository;
_identityProvider = identityProvider;
_userRepository = userRepository;
_transactionProvider = transactionProvider;
_authenticateUserValidator = authenticateUserValidator;
_jwtConfiguration = jwtConfig.Value;
}
/// <inheritdoc />
public async Task<AccessTokensDto> AuthenticateUser(AuthenticateUserDto request)
{
await _authenticateUserValidator.ValidateAndThrowAsync(request);
// Find user from provided username.
var user = await _userRepository.FindByUsername(request.Username);
if (user is null)
{
throw new InvalidCredentialsException();
}
if (!_cryptographyService.VerifyPassword(
request.Password,
user.PasswordSalt,
user.PasswordHash))
{
// TODO store failed logins in future
throw new InvalidCredentialsException();
}
switch (user.State)
{
case UserState.Suspended:
throw new UserSuspendedException();
case UserState.Banned:
throw new UserBannedException();
case UserState.AwaitingEmailActivation:
throw new UserNotActivatedException();
}
// Record last logged in.
// TODO store successful login in future
user.LastLoggedIn = DateTime.UtcNow;
await _userRepository.UpdateAsync(user);
var accessToken = await GenerateUserAccessTokens(user, request.UserAgent, request.IpAddress);
return accessToken;
}
/// <inheritdoc />
public async Task<AccessTokensDto> AuthenticateUser(RefreshAuthenticationDto request)
{
if (request.RefreshToken.Length != RefreshTokenLength)
{
throw new UnauthorizedException();
}
// Attempt to find refresh token.
var refreshToken = await _refreshTokenRepository.FindByIdAsync(request.RefreshToken);
if (refreshToken is null || refreshToken.Expires < DateTime.UtcNow ||
request.UserAgent.UserAgent != refreshToken.UserAgent ||
refreshToken.TokenContext != RefreshTokenContext.UserAccess)
{
throw new UnauthorizedException();
}
// Get user to determine state.
var user = await _userRepository.FindByIdAsync(refreshToken.UserId);
if (user is null || user.State != UserState.Normal)
{
throw new UnauthorizedException();
}
// Consider this request valid and delete old refresh token.
await _refreshTokenRepository.DeleteAsync(refreshToken.Token);
var newTokens = await GenerateUserAccessTokens(user, request.UserAgent, request.IpAddress);
return newTokens;
}
/// <inheritdoc />
public async Task<AccessTokensDto> ChangeUserPassword(ChangePasswordDto request)
{
// Check to make sure a user is logged in.
var userId = _identityProvider.GetRequestingUserId();
if (string.IsNullOrWhiteSpace(userId) || !Guid.TryParse(userId, out var guid))
{
throw new UnauthorizedException();
}
var user = await _userRepository.FindByIdAsync(guid);
if (user is null || user.State == UserState.Banned || user.State == UserState.Suspended)
{
throw new UnauthorizedException();
}
// Verify current password is correct.
if (!_cryptographyService.VerifyPassword(
request.OldPassword,
user.PasswordSalt,
user.PasswordHash))
{
// TODO store failed request in future
throw new InvalidCredentialsException();
}
var salt = _cryptographyService.GenerateSalt();
var hash = _cryptographyService.HashPassword(request.NewPassword, salt);
user.PasswordHash = Convert.ToBase64String(hash);
user.PasswordSalt = Convert.ToBase64String(salt);
// TODO store log in future
using var transaction = await _transactionProvider.BeginTransactionAsync();
await _userRepository.UpdateAsync(user);
await _refreshTokenRepository.RevokeAllForUser(user.Id);
var tokens = await GenerateUserAccessTokens(user, request.UserAgent, request.IpAddress);
transaction.Commit();
return tokens;
}
/// <summary>
/// Generate a new access token and refresh token for user.
/// </summary>
/// <param name="user">Instance of <see cref="User"/>.</param>
/// <param name="userAgentInformation">Information about the requesting user agent.</param>
/// <param name="ipAddress">The IP address of the agent requesting a user access token.</param>
/// <returns>Instance of <see cref="AccessTokensDto"/>.</returns>
private async Task<AccessTokensDto> GenerateUserAccessTokens(
User user,
HttpUserAgentInformation userAgentInformation,
IPAddress ipAddress)
{
var handler = new JwtSecurityTokenHandler();
var claims = new List<Claim>
{
new (ClaimIds.UserId, user.Id.ToString()),
new (ClaimIds.Role, user.Role.ToString()),
};
var key = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_jwtConfiguration.SecretKey));
var accessExpires = _jwtConfiguration.AccessTokenExpirationDateTime();
var refreshExpires = _jwtConfiguration.RefreshTokenExpirationDateTime();
var descriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = accessExpires,
SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature),
Audience = _jwtConfiguration.Audience,
Issuer = _jwtConfiguration.Issuer,
};
var result = new AccessTokensDto();
var token = handler.CreateToken(descriptor);
result.AccessToken = handler.WriteToken(token);
result.RefreshToken = _cryptographyService.GenerateRandomString(RefreshTokenLength);
result.AccessTokenExpires = accessExpires;
result.AccessTokenExpiresTicks = accessExpires.Ticks;
result.RefreshTokenExpires = refreshExpires;
result.RefreshTokenExpiresTicks = refreshExpires.Ticks;
// Make sure the refresh token is unique.
while (await _refreshTokenRepository.FindByIdAsync(result.RefreshToken) is not null)
{
result.RefreshToken = _cryptographyService.GenerateRandomString(RefreshTokenLength);
}
// Store refresh token.
await _refreshTokenRepository.AddAsync(new RefreshToken()
{
Token = result.RefreshToken,
UserId = user.Id,
TokenContext = RefreshTokenContext.UserAccess,
Expires = refreshExpires,
UserAgent = userAgentInformation.UserAgent,
UserAgentType = userAgentInformation.Type,
IpAddress = ipAddress.ToString(),
CreatedAt = DateTime.UtcNow,
});
return result;
}
}