255 lines
9.5 KiB
C#
255 lines
9.5 KiB
C#
|
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;
|
||
|
}
|
||
|
}
|