using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Security.Cryptography; using System.Text; using AutobusApi.Application.Common.Exceptions; using AutobusApi.Application.Common.Interfaces; using AutobusApi.Application.Common.Models.Identity; using AutobusApi.Domain.Enums; using AutobusApi.Infrastructure.Identity; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; namespace AutobusApi.Infrastructure.Services; public class IdentityService : IIdentityService { private readonly UserManager _userManager; private readonly IConfiguration _configuration; public IdentityService( UserManager userManager, IConfiguration configuration) { _userManager = userManager; _configuration = configuration; } public async Task RegisterAsync( string email, string password, CancellationToken cancellationToken) { var userWithSameEmail = await _userManager.FindByEmailAsync(email); if (userWithSameEmail is not null) { throw new RegistrationException("User with given email already registered."); } var newUser = new ApplicationUser { UserName = email, Email = email }; var createUserResult = await _userManager.CreateAsync(newUser, password); var addToRoleResult = await _userManager.AddToRoleAsync(newUser, IdentityRoles.User.ToString()); } public async Task LoginAsync( string email, string password, CancellationToken cancellationToken) { var user = await _userManager.FindByEmailAsync(email); if (user is null) { throw new LoginException("No users registered with given email."); } var isPasswordCorrect = await _userManager.CheckPasswordAsync(user, password); if (!isPasswordCorrect) { throw new LoginException("Given password is incorrect."); } var jwtSecurityToken = await CreateJwtAsync(user, cancellationToken); var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); var refreshToken = user.RefreshTokens.FirstOrDefault(t => t.IsActive); if (refreshToken is null) { refreshToken = CreateRefreshToken(); user.RefreshTokens.Add(refreshToken); await _userManager.UpdateAsync(user); } return new TokensModel(accessToken, refreshToken.Value); } public async Task RenewAccessTokenAsync( string refreshToken, CancellationToken cancellationToken) { var user = await _userManager.Users.SingleOrDefaultAsync(u => u.RefreshTokens.Any(rt => rt.Value == refreshToken)); if (user is null) { throw new RenewAccessTokenException($"Refresh token {refreshToken} was not found."); } var refreshTokenObject = user.RefreshTokens.Single(rt => rt.Value == refreshToken); if (!refreshTokenObject.IsActive) { throw new RenewAccessTokenException("Refresh token is inactive."); } var jwtSecurityToken = await CreateJwtAsync(user, cancellationToken); var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); return new TokensModel(accessToken, refreshToken); } public async Task RevokeRefreshTokenAsync( string refreshToken, CancellationToken cancellationToken) { var user = await _userManager.Users.SingleOrDefaultAsync(u => u.RefreshTokens.Any(t => t.Value == refreshToken)); if (user is null) { throw new RevokeRefreshTokenException("Invalid refreshToken"); } var refreshTokenObject = user.RefreshTokens.Single(x => x.Value == refreshToken); if (!refreshTokenObject.IsActive) { throw new RevokeRefreshTokenException("RefreshToken already revoked"); } refreshTokenObject.RevokationDateTimeUtc = DateTime.UtcNow; await _userManager.UpdateAsync(user); } private async Task CreateJwtAsync( ApplicationUser user, CancellationToken cancellationToken) { var userClaims = await _userManager.GetClaimsAsync(user); var roles = await _userManager.GetRolesAsync(user); var roleClaims = new List(); foreach (var role in roles) { roleClaims.Add(new Claim("roles", role)); } var claims = new List() { new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()), new Claim(JwtRegisteredClaimNames.Email, user.Email) } .Union(userClaims) .Union(roleClaims); var jwtExpirationDateTimeUtc = DateTime.UtcNow.AddMinutes(Double.Parse(_configuration["Jwt:AccessTokenValidityInMinutes"])); var symmetricSecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:IssuerSigningKey"])); var signingCredentials = new SigningCredentials(symmetricSecurityKey, SecurityAlgorithms.HmacSha256); var jwtSecurityToken = new JwtSecurityToken( issuer: _configuration["Jwt:Issuer"], audience: _configuration["Jwt:Audience"], claims: claims, expires: jwtExpirationDateTimeUtc, signingCredentials: signingCredentials); return jwtSecurityToken; } private RefreshToken CreateRefreshToken() { var randomNumber = new byte[32]; using var rng = RandomNumberGenerator.Create(); rng.GetNonZeroBytes(randomNumber); return new RefreshToken { Value = Convert.ToBase64String(randomNumber), CreationDateTimeUtc = DateTime.UtcNow, ExpirationDateTimeUtc = DateTime.UtcNow.AddDays(Double.Parse(_configuration["Jwt:RefreshTokenValidityInDays"])) }; } }