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; /// [RegisterScoped] public class AuthenticationService : IAuthenticationService { /// /// The length of the refresh token. /// 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 _authenticateUserValidator; private readonly JwtConfiguration _jwtConfiguration; /// /// Initializes a new instance of the class. /// /// Instance of . /// Instance of . /// Instance of . /// Instance of . /// Instance of . /// Instance of . /// Instance of . public AuthenticationService( ICryptographyService cryptographyService, IRefreshTokenRepository refreshTokenRepository, IIdentityProvider identityProvider, IUserRepository userRepository, ITransactionProvider transactionProvider, IValidator authenticateUserValidator, IOptions jwtConfig) { _cryptographyService = cryptographyService; _refreshTokenRepository = refreshTokenRepository; _identityProvider = identityProvider; _userRepository = userRepository; _transactionProvider = transactionProvider; _authenticateUserValidator = authenticateUserValidator; _jwtConfiguration = jwtConfig.Value; } /// public async Task 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; } /// public async Task 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; } /// public async Task 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; } /// /// Generate a new access token and refresh token for user. /// /// Instance of . /// Information about the requesting user agent. /// The IP address of the agent requesting a user access token. /// Instance of . private async Task GenerateUserAccessTokens( User user, HttpUserAgentInformation userAgentInformation, IPAddress ipAddress) { var handler = new JwtSecurityTokenHandler(); var claims = new List { 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; } }