< Summary

Information
Class: LGDXRobotCloud.API.Services.Identity.AuthService
Assembly: LGDXRobotCloud.API
File(s): /builds/yukaitung/lgdxrobot2-cloud/LGDXRobotCloud.API/Services/Identity/AuthService.cs
Line coverage
98%
Covered lines: 179
Uncovered lines: 3
Coverable lines: 182
Total lines: 253
Line coverage: 98.3%
Branch coverage
83%
Covered branches: 45
Total branches: 54
Branch coverage: 83.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%1010100%
GenerateJwtToken(...)100%11100%
GenerateAccessTokenAsync()60%101090%
GenerateRefreshToken(...)100%11100%
LoginAsync()100%1818100%
ForgotPasswordAsync()100%22100%
ResetPasswordAsync()100%44100%
RefreshTokenAsync()80%1010100%
UpdatePasswordAsync()100%44100%

File(s)

/builds/yukaitung/lgdxrobot2-cloud/LGDXRobotCloud.API/Services/Identity/AuthService.cs

#LineLine coverage
 1using System.IdentityModel.Tokens.Jwt;
 2using System.Security.Claims;
 3using System.Text;
 4using LGDXRobotCloud.API.Configurations;
 5using LGDXRobotCloud.API.Exceptions;
 6using LGDXRobotCloud.API.Services.Common;
 7using LGDXRobotCloud.Data.DbContexts;
 8using LGDXRobotCloud.Data.Entities;
 9using LGDXRobotCloud.Data.Models.Business.Identity;
 10using LGDXRobotCloud.Utilities.Helpers;
 11using Microsoft.AspNetCore.Identity;
 12using Microsoft.EntityFrameworkCore;
 13using Microsoft.Extensions.Options;
 14using Microsoft.IdentityModel.Tokens;
 15
 16namespace LGDXRobotCloud.API.Services.Identity;
 17
 18public interface IAuthService
 19{
 20  Task<LoginResponseBusinessModel> LoginAsync(LoginRequestBusinessModel loginRequestBusinessModel);
 21  Task ForgotPasswordAsync(ForgotPasswordRequestBusinessModel forgotPasswordRequestBusinessModel);
 22  Task ResetPasswordAsync(ResetPasswordRequestBusinessModel resetPasswordRequestBusinessModel);
 23  Task<RefreshTokenResponseBusinessModel> RefreshTokenAsync(RefreshTokenRequestBusinessModel refreshTokenRequestBusiness
 24  Task<bool> UpdatePasswordAsync(string userId, UpdatePasswordRequestBusinessModel updatePasswordRequestBusinessModel);
 25}
 26
 2227public class AuthService(
 2228    LgdxContext context,
 2229    IEmailService emailService,
 2230    IOptionsSnapshot<LgdxRobotCloudSecretConfiguration> lgdxRobotCloudSecretConfiguration,
 2231    SignInManager<LgdxUser> signInManager,
 2232    UserManager<LgdxUser> userManager
 2233  ) : IAuthService
 34{
 2235  private readonly LgdxContext _context = context ?? throw new ArgumentNullException(nameof(context));
 2236  private readonly IEmailService _emailService = emailService ?? throw new ArgumentNullException(nameof(emailService));
 2237  private readonly LgdxRobotCloudSecretConfiguration _lgdxRobotCloudSecretConfiguration = lgdxRobotCloudSecretConfigurat
 2238  private readonly SignInManager<LgdxUser> _signInManager = signInManager ?? throw new ArgumentNullException(nameof(sign
 2239  private readonly UserManager<LgdxUser> _userManager = userManager ?? throw new ArgumentNullException(nameof(userManage
 40
 41  private JwtSecurityToken GenerateJwtToken(List<Claim> claims, DateTime notBefore, DateTime expires)
 2042  {
 2043    var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_lgdxRobotCloudSecretConfiguration.LgdxUserJwtSecr
 2044    var credentials = new SigningCredentials(securityKey, _lgdxRobotCloudSecretConfiguration.LgdxUserJwtAlgorithm);
 2045    return new JwtSecurityToken(
 2046      _lgdxRobotCloudSecretConfiguration.LgdxUserJwtIssuer,
 2047      _lgdxRobotCloudSecretConfiguration.LgdxUserJwtIssuer,
 2048      claims,
 2049      notBefore,
 2050      expires,
 2051      credentials);
 2052  }
 53
 54  private async Task<string> GenerateAccessTokenAsync(LgdxUser user, DateTime notBefore, DateTime expires)
 1055  {
 1056    var userRoles = await _userManager.GetRolesAsync(user);
 1057    var Claims = new List<Claim>
 1058    {
 1059      new (JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
 1060      new (JwtRegisteredClaimNames.Sub, user.Id.ToString()),
 1061      new (ClaimTypes.Name, user.UserName ?? string.Empty),
 1062      new (ClaimTypes.Email, user.Email ?? string.Empty),
 1063      new ("fullname", user.Name ?? string.Empty),
 1064    };
 65    // Add Roles
 7066    foreach (var userRole in userRoles)
 2067    {
 2068      Claims.Add(new Claim(ClaimTypes.Role, userRole));
 2069    }
 70    // Add Role Claims
 1071    {
 1072      List<string> roleIds = await _context.Roles.AsNoTracking()
 1073        .Where(r => userRoles.Select(ur => ur.ToUpper()).Contains(r.NormalizedName!))
 1074        .Select(r => r.Id )
 1075        .ToListAsync();
 1076      var roleClaims = await _context.RoleClaims.AsNoTracking()
 1077        .Where(r => roleIds.Contains(r.RoleId))
 1078        .ToListAsync();
 3079      foreach (var roleClaim in roleClaims)
 080      {
 081        Claims.Add(new Claim(roleClaim.ClaimType!, roleClaim.ClaimValue!));
 082      }
 1083    }
 1084    var token = GenerateJwtToken(Claims, notBefore, expires);
 1085    return new JwtSecurityTokenHandler().WriteToken(token);
 1086  }
 87
 88  private string GenerateRefreshToken(LgdxUser user, DateTime notBefore, DateTime expires)
 1089  {
 1090    var Claims = new List<Claim>
 1091    {
 1092      new (JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
 1093      new (JwtRegisteredClaimNames.Sub, user.Id.ToString()),
 1094    };
 1095    var token = GenerateJwtToken(Claims, notBefore, expires);
 1096    var tokenStr = new JwtSecurityTokenHandler().WriteToken(token);
 1097    return tokenStr;
 1098  }
 99
 100  public async Task<LoginResponseBusinessModel> LoginAsync(LoginRequestBusinessModel loginRequestBusinessModel)
 14101  {
 14102    var user = await _userManager.FindByNameAsync(loginRequestBusinessModel.Username)
 14103      ?? throw new LgdxValidation400Expection(nameof(loginRequestBusinessModel.Username), "The username or password is i
 104
 13105    var loginResult = await _signInManager.PasswordSignInAsync(user, loginRequestBusinessModel.Password, false, true);
 13106    if (loginResult.RequiresTwoFactor)
 5107    {
 5108      if (!string.IsNullOrEmpty(loginRequestBusinessModel.TwoFactorCode))
 2109      {
 2110        loginResult = await _signInManager.TwoFactorAuthenticatorSignInAsync(loginRequestBusinessModel.TwoFactorCode, fa
 2111        if (!loginResult.Succeeded)
 1112        {
 1113          throw new LgdxValidation400Expection(nameof(loginRequestBusinessModel.Username), "The 2FA code is invalid.");
 114        }
 1115      }
 3116      else if (!string.IsNullOrEmpty(loginRequestBusinessModel.TwoFactorRecoveryCode))
 2117      {
 2118        loginResult = await _signInManager.TwoFactorRecoveryCodeSignInAsync(loginRequestBusinessModel.TwoFactorRecoveryC
 2119        if (!loginResult.Succeeded)
 1120        {
 1121          throw new LgdxValidation400Expection(nameof(loginRequestBusinessModel.Username), "The recovery code is invalid
 122        }
 1123      }
 124      else
 1125      {
 1126        return new LoginResponseBusinessModel
 1127        {
 1128          AccessToken = string.Empty,
 1129          RefreshToken = string.Empty,
 1130          ExpiresMins = 0,
 1131          RequiresTwoFactor = true
 1132        };
 133      }
 2134    }
 10135    if (!loginResult.Succeeded)
 2136    {
 2137      if (loginResult.IsLockedOut)
 1138      {
 1139        throw new LgdxValidation400Expection(nameof(loginRequestBusinessModel.Username), "The account is lockedout.");
 140      }
 141      else
 1142      {
 1143        throw new LgdxValidation400Expection(nameof(loginRequestBusinessModel.Username), "The username or password is in
 144      }
 145    }
 146
 8147    var notBefore = DateTime.UtcNow;
 8148    var accessExpires = notBefore.AddMinutes(_lgdxRobotCloudSecretConfiguration.LgdxUserAccessTokenExpiresMins);
 8149    var refreshExpires = notBefore.AddMinutes(_lgdxRobotCloudSecretConfiguration.LgdxUserRefreshTokenExpiresMins);
 8150    var accessToken = await GenerateAccessTokenAsync(user, notBefore, accessExpires);
 8151    var refreshToken = GenerateRefreshToken(user, notBefore, refreshExpires);
 8152    user.RefreshTokenHash = LgdxHelper.GenerateSha256Hash(refreshToken);
 8153    var updateTokenResult = await _userManager.UpdateAsync(user);
 8154    if (!updateTokenResult.Succeeded)
 1155    {
 1156      throw new LgdxIdentity400Expection(updateTokenResult.Errors);
 157    }
 7158    return new LoginResponseBusinessModel
 7159    {
 7160      AccessToken = accessToken,
 7161      RefreshToken = refreshToken,
 7162      ExpiresMins = _lgdxRobotCloudSecretConfiguration.LgdxUserAccessTokenExpiresMins,
 7163      RequiresTwoFactor = false
 7164    };
 8165  }
 166
 167  public async Task ForgotPasswordAsync(ForgotPasswordRequestBusinessModel forgotPasswordRequestBusinessModel)
 2168  {
 2169    var user = await _userManager.FindByEmailAsync(forgotPasswordRequestBusinessModel.Email);
 2170    if (user != null)
 1171    {
 1172      var token = await _userManager.GeneratePasswordResetTokenAsync(user);
 1173      await _emailService.SendPasswordResetEmailAsync(user.Email!, user.Name!, user.UserName!, token);
 1174    }
 175    // For security reasons, we do not return a 404 status code.
 2176  }
 177
 178  public async Task ResetPasswordAsync(ResetPasswordRequestBusinessModel resetPasswordRequestBusinessModel)
 3179  {
 3180    var user = await _userManager.FindByEmailAsync(resetPasswordRequestBusinessModel.Email)
 3181      ?? throw new LgdxValidation400Expection(nameof(resetPasswordRequestBusinessModel.Token), "");
 182
 2183    var result = await _userManager.ResetPasswordAsync(user, resetPasswordRequestBusinessModel.Token, resetPasswordReque
 2184    if (!result.Succeeded)
 1185    {
 1186      throw new LgdxIdentity400Expection(result.Errors);
 187    }
 1188    await _emailService.SendPasswordUpdateEmailAsync(user.Email!, user.Name!, user.UserName!);
 1189  }
 190
 191  public async Task<RefreshTokenResponseBusinessModel> RefreshTokenAsync(RefreshTokenRequestBusinessModel refreshTokenRe
 4192  {
 193    // Validate the refresh token
 4194    var tokenHandler = new JwtSecurityTokenHandler();
 4195    TokenValidationParameters validationParameters = new()
 4196    {
 4197      ValidateIssuer = true,
 4198      ValidateAudience = true,
 4199      ValidateLifetime = true,
 4200      ValidateIssuerSigningKey = true,
 4201      ValidIssuer = _lgdxRobotCloudSecretConfiguration.LgdxUserJwtIssuer,
 4202      ValidAudience = _lgdxRobotCloudSecretConfiguration.LgdxUserJwtIssuer,
 4203      IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_lgdxRobotCloudSecretConfiguration.LgdxUserJwtS
 4204      ClockSkew = TimeSpan.Zero
 4205    };
 4206    ClaimsPrincipal principal = tokenHandler.ValidateToken(refreshTokenRequestBusinessModel.RefreshToken, validationPara
 207
 208    // The token is valid, check the database
 12209    var userId = principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value
 4210      ?? throw new LgdxValidation400Expection(nameof(refreshTokenRequestBusinessModel.RefreshToken), "The user ID is not
 4211    var user = await _userManager.FindByIdAsync(userId)
 4212      ?? throw new LgdxValidation400Expection(nameof(refreshTokenRequestBusinessModel.RefreshToken), "User not found.");
 3213    if (user.RefreshTokenHash != LgdxHelper.GenerateSha256Hash(refreshTokenRequestBusinessModel.RefreshToken))
 1214    {
 1215      throw new LgdxValidation400Expection(nameof(refreshTokenRequestBusinessModel.RefreshToken), "The refresh token is 
 216    }
 217
 218    // Generate new token pair and update the database
 2219    var notBefore = DateTime.UtcNow;
 2220    var accessExpires = notBefore.AddMinutes(_lgdxRobotCloudSecretConfiguration.LgdxUserAccessTokenExpiresMins);
 2221    var refreshExpires = validatedToken.ValidTo; // Reauthenticate to extend the expiration time
 2222    var accessToken = await GenerateAccessTokenAsync(user, notBefore, accessExpires);
 2223    var refreshToken = GenerateRefreshToken(user, notBefore, refreshExpires);
 2224    user.RefreshTokenHash = LgdxHelper.GenerateSha256Hash(refreshToken);
 2225    var result = await _userManager.UpdateAsync(user);
 2226    if (!result.Succeeded)
 1227    {
 1228      throw new LgdxIdentity400Expection(result.Errors);
 229    }
 230
 231    // Generate new token pair
 1232    return new RefreshTokenResponseBusinessModel()
 1233    {
 1234      AccessToken = accessToken,
 1235      RefreshToken = refreshToken,
 1236      ExpiresMins = _lgdxRobotCloudSecretConfiguration.LgdxUserAccessTokenExpiresMins
 1237    };
 1238  }
 239
 240  public async Task<bool> UpdatePasswordAsync(string userId, UpdatePasswordRequestBusinessModel updatePasswordRequestBus
 3241  {
 3242    var user = await userManager.FindByIdAsync(userId)
 3243      ?? throw new LgdxNotFound404Exception();
 244
 2245    var result = await _userManager.ChangePasswordAsync(user, updatePasswordRequestBusinessModel.CurrentPassword, update
 2246    if (!result.Succeeded)
 1247    {
 1248      throw new LgdxIdentity400Expection(result.Errors);
 249    }
 1250    await _emailService.SendPasswordUpdateEmailAsync(user.Email!, user.Name!, user.UserName!);
 1251    return true;
 1252  }
 253}