// // Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. // using System.IdentityModel.Tokens.Jwt; using System.Net; using System.Security.Claims; using System.Text; using Astral.Core.Constants; using Astral.Core.Entities; using Astral.Core.Extensions; using Astral.Core.Infrastructure; using Astral.Core.Options; using Astral.Core.RepositoryInterfaces; using Astral.Services.Constants; using Astral.Services.Dtos; using Astral.Services.Exceptions; using Astral.Services.Interfaces; using FluentValidation; using Injectio.Attributes; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; namespace Astral.Services.Services; /// [RegisterScoped] public class AuthenticationService : IAuthenticationService { private const int RefreshTokenLength = 128; private readonly ICryptographyService _cryptographyService; private readonly ITransactionProvider _transactionProvider; private readonly IUserRepository _userRepository; private readonly IRefreshTokenRepository _refreshTokenRepository; private readonly IValidator _passwordAuthValidator; private readonly JwtOptions _jwtOptions; /// /// Initializes a new instance of the class. /// /// Instance of . /// Instance of . /// Instance of . /// Instance of . /// Instance of . /// Instance of . public AuthenticationService( ICryptographyService cryptographyService, ITransactionProvider transactionProvider, IUserRepository userRepository, IRefreshTokenRepository refreshTokenRepository, IValidator passwordAuthValidator, IOptions jwtOptions) { _cryptographyService = cryptographyService; _transactionProvider = transactionProvider; _userRepository = userRepository; _refreshTokenRepository = refreshTokenRepository; _passwordAuthValidator = passwordAuthValidator; _jwtOptions = jwtOptions.Value; } /// public async Task AuthenticateSession(PasswordAuthenticateDto passwordAuthenticateDto) { await _passwordAuthValidator.ValidateAndThrowAsync(passwordAuthenticateDto); // Find user from provided username. var user = await _userRepository.FindByUsername(passwordAuthenticateDto.Username); if (user is null) { throw new InvalidCredentialsException(); } if (!_cryptographyService.VerifyPassword( passwordAuthenticateDto.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(); } using var transaction = await _transactionProvider.BeginTransactionAsync(); // Record last logged in. // TODO store successful login in future user.LastLoggedIn = DateTime.UtcNow; await _userRepository.UpdateAsync(user); // Generate token. var result = await GenerateAccessTokens(user, passwordAuthenticateDto.Scope, passwordAuthenticateDto.IpAddress); transaction.Commit(); return result; } /// /// Generate a new access token and refresh token for user. /// /// Instance of . /// Instance of . /// The IP address of the agent requesting a user access token. /// Instance of . private async Task GenerateAccessTokens( User user, TokenScope scope, IPAddress ipAddress) { var handler = new JwtSecurityTokenHandler(); var claims = new List { new (ClaimIds.UserId, user.Id.ToString()), new (ClaimIds.UserName, user.Username), new (ClaimIds.Role, user.Role.ToString()), new (ClaimIds.Scope, scope.ToString()), }; var key = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_jwtOptions.SecretKey)); var accessExpires = _jwtOptions.AccessTokenExpirationDateTime(); var refreshExpires = _jwtOptions.RefreshTokenExpirationDateTime(); var descriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(claims), Expires = accessExpires, SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature), Audience = _jwtOptions.Audience, Issuer = _jwtOptions.Issuer, }; var result = new SessionDto(); var token = handler.CreateToken(descriptor); result.UserId = user.Id; result.UserName = user.Username; result.CreatedAt = DateTime.UtcNow; result.AccessToken = handler.WriteToken(token); result.RefreshToken = _cryptographyService.GenerateRandomString(RefreshTokenLength); result.AccessTokenExpires = accessExpires; result.RefreshTokenExpires = refreshExpires; result.Scope = scope; result.Role = user.Role; // 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, Role = user.Role, Scope = scope, Expires = refreshExpires, IpAddress = ipAddress.ToString(), CreatedAt = DateTime.UtcNow, }); return result; } }