< 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: 219
Uncovered lines: 3
Coverable lines: 222
Total lines: 296
Line coverage: 98.6%
Branch coverage
82%
Covered branches: 46
Total branches: 56
Branch coverage: 82.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%1212100%
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.Administration;
 7using LGDXRobotCloud.API.Services.Common;
 8using LGDXRobotCloud.Data.DbContexts;
 9using LGDXRobotCloud.Data.Entities;
 10using LGDXRobotCloud.Data.Models.Business.Administration;
 11using LGDXRobotCloud.Data.Models.Business.Identity;
 12using LGDXRobotCloud.Utilities.Enums;
 13using LGDXRobotCloud.Utilities.Helpers;
 14using Microsoft.AspNetCore.Identity;
 15using Microsoft.EntityFrameworkCore;
 16using Microsoft.Extensions.Options;
 17using Microsoft.IdentityModel.Tokens;
 18
 19namespace LGDXRobotCloud.API.Services.Identity;
 20
 21public interface IAuthService
 22{
 23  Task<LoginResponseBusinessModel> LoginAsync(LoginRequestBusinessModel loginRequestBusinessModel);
 24  Task ForgotPasswordAsync(ForgotPasswordRequestBusinessModel forgotPasswordRequestBusinessModel);
 25  Task ResetPasswordAsync(ResetPasswordRequestBusinessModel resetPasswordRequestBusinessModel);
 26  Task<RefreshTokenResponseBusinessModel> RefreshTokenAsync(RefreshTokenRequestBusinessModel refreshTokenRequestBusiness
 27  Task<bool> UpdatePasswordAsync(string userId, UpdatePasswordRequestBusinessModel updatePasswordRequestBusinessModel);
 28}
 29
 2230public class AuthService(
 2231    IActivityLogService activityLogService,
 2232    LgdxContext context,
 2233    IEmailService emailService,
 2234    IOptionsSnapshot<LgdxRobotCloudSecretConfiguration> lgdxRobotCloudSecretConfiguration,
 2235    SignInManager<LgdxUser> signInManager,
 2236    UserManager<LgdxUser> userManager
 2237  ) : IAuthService
 38{
 2239  private readonly IActivityLogService _activityLogService = activityLogService ?? throw new ArgumentNullException(nameo
 2240  private readonly LgdxContext _context = context ?? throw new ArgumentNullException(nameof(context));
 2241  private readonly IEmailService _emailService = emailService ?? throw new ArgumentNullException(nameof(emailService));
 2242  private readonly LgdxRobotCloudSecretConfiguration _lgdxRobotCloudSecretConfiguration = lgdxRobotCloudSecretConfigurat
 2243  private readonly SignInManager<LgdxUser> _signInManager = signInManager ?? throw new ArgumentNullException(nameof(sign
 2244  private readonly UserManager<LgdxUser> _userManager = userManager ?? throw new ArgumentNullException(nameof(userManage
 45
 46  private JwtSecurityToken GenerateJwtToken(List<Claim> claims, DateTime notBefore, DateTime expires)
 2047  {
 2048    var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_lgdxRobotCloudSecretConfiguration.LgdxUserJwtSecr
 2049    var credentials = new SigningCredentials(securityKey, _lgdxRobotCloudSecretConfiguration.LgdxUserJwtAlgorithm);
 2050    return new JwtSecurityToken(
 2051      _lgdxRobotCloudSecretConfiguration.LgdxUserJwtIssuer,
 2052      _lgdxRobotCloudSecretConfiguration.LgdxUserJwtIssuer,
 2053      claims,
 2054      notBefore,
 2055      expires,
 2056      credentials);
 2057  }
 58
 59  private async Task<string> GenerateAccessTokenAsync(LgdxUser user, DateTime notBefore, DateTime expires)
 1060  {
 1061    var userRoles = await _userManager.GetRolesAsync(user);
 1062    var Claims = new List<Claim>
 1063    {
 1064      new (JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
 1065      new (JwtRegisteredClaimNames.Sub, user.Id.ToString()),
 1066      new (ClaimTypes.Name, user.UserName ?? string.Empty),
 1067      new (ClaimTypes.Email, user.Email ?? string.Empty),
 1068      new ("fullname", user.Name ?? string.Empty),
 1069    };
 70    // Add Roles
 7071    foreach (var userRole in userRoles)
 2072    {
 2073      Claims.Add(new Claim(ClaimTypes.Role, userRole));
 2074    }
 75    // Add Role Claims
 1076    {
 1077      List<string> roleIds = await _context.Roles.AsNoTracking()
 1078        .Where(r => userRoles.Select(ur => ur.ToUpper()).Contains(r.NormalizedName!))
 1079        .Select(r => r.Id )
 1080        .ToListAsync();
 1081      var roleClaims = await _context.RoleClaims.AsNoTracking()
 1082        .Where(r => roleIds.Contains(r.RoleId))
 1083        .ToListAsync();
 3084      foreach (var roleClaim in roleClaims)
 085      {
 086        Claims.Add(new Claim(roleClaim.ClaimType!, roleClaim.ClaimValue!));
 087      }
 1088    }
 1089    var token = GenerateJwtToken(Claims, notBefore, expires);
 1090    return new JwtSecurityTokenHandler().WriteToken(token);
 1091  }
 92
 93  private string GenerateRefreshToken(LgdxUser user, DateTime notBefore, DateTime expires)
 1094  {
 1095    var Claims = new List<Claim>
 1096    {
 1097      new (JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
 1098      new (JwtRegisteredClaimNames.Sub, user.Id.ToString()),
 1099    };
 10100    var token = GenerateJwtToken(Claims, notBefore, expires);
 10101    var tokenStr = new JwtSecurityTokenHandler().WriteToken(token);
 10102    return tokenStr;
 10103  }
 104
 105  public async Task<LoginResponseBusinessModel> LoginAsync(LoginRequestBusinessModel loginRequestBusinessModel)
 14106  {
 14107    var user = await _userManager.FindByNameAsync(loginRequestBusinessModel.Username)
 14108      ?? throw new LgdxValidation400Expection(nameof(loginRequestBusinessModel.Username), "The username or password is i
 109
 13110    var loginResult = await _signInManager.PasswordSignInAsync(user, loginRequestBusinessModel.Password, false, true);
 13111    if (loginResult.RequiresTwoFactor)
 5112    {
 5113      if (!string.IsNullOrEmpty(loginRequestBusinessModel.TwoFactorCode))
 2114      {
 2115        loginResult = await _signInManager.TwoFactorAuthenticatorSignInAsync(loginRequestBusinessModel.TwoFactorCode, fa
 2116        if (!loginResult.Succeeded)
 1117        {
 1118          throw new LgdxValidation400Expection(nameof(loginRequestBusinessModel.Username), "The 2FA code is invalid.");
 119        }
 1120      }
 3121      else if (!string.IsNullOrEmpty(loginRequestBusinessModel.TwoFactorRecoveryCode))
 2122      {
 2123        loginResult = await _signInManager.TwoFactorRecoveryCodeSignInAsync(loginRequestBusinessModel.TwoFactorRecoveryC
 2124        if (!loginResult.Succeeded)
 1125        {
 1126          throw new LgdxValidation400Expection(nameof(loginRequestBusinessModel.Username), "The recovery code is invalid
 127        }
 1128      }
 129      else
 1130      {
 1131        return new LoginResponseBusinessModel
 1132        {
 1133          AccessToken = string.Empty,
 1134          RefreshToken = string.Empty,
 1135          ExpiresMins = 0,
 1136          RequiresTwoFactor = true
 1137        };
 138      }
 2139    }
 10140    if (!loginResult.Succeeded)
 2141    {
 2142      if (loginResult.IsLockedOut)
 1143      {
 1144        await _activityLogService.CreateActivityLogAsync(new ActivityLogCreateBusinessModel
 1145        {
 1146          EntityName = nameof(LgdxUser),
 1147          EntityId = user.Id.ToString(),
 1148          Action = ActivityAction.LoginFailed,
 1149          Note = "The account is locked out."
 1150        });
 1151        throw new LgdxValidation400Expection(nameof(loginRequestBusinessModel.Username), "The account is locked out.");
 152      }
 153      else
 1154      {
 1155        await _activityLogService.CreateActivityLogAsync(new ActivityLogCreateBusinessModel
 1156        {
 1157          EntityName = nameof(LgdxUser),
 1158          EntityId = user.Id.ToString(),
 1159          Action = ActivityAction.LoginFailed,
 1160          Note = "The username or password is invalid."
 1161        });
 1162        throw new LgdxValidation400Expection(nameof(loginRequestBusinessModel.Username), "The username or password is in
 163      }
 164    }
 165
 8166    var notBefore = DateTime.UtcNow;
 8167    var accessExpires = notBefore.AddMinutes(_lgdxRobotCloudSecretConfiguration.LgdxUserAccessTokenExpiresMins);
 8168    var refreshExpires = notBefore.AddMinutes(_lgdxRobotCloudSecretConfiguration.LgdxUserRefreshTokenExpiresMins);
 8169    var accessToken = await GenerateAccessTokenAsync(user, notBefore, accessExpires);
 8170    var refreshToken = GenerateRefreshToken(user, notBefore, refreshExpires);
 8171    user.RefreshTokenHash = LgdxHelper.GenerateSha256Hash(refreshToken);
 8172    var updateTokenResult = await _userManager.UpdateAsync(user);
 8173    if (!updateTokenResult.Succeeded)
 1174    {
 1175      throw new LgdxIdentity400Expection(updateTokenResult.Errors);
 176    }
 7177    await _activityLogService.CreateActivityLogAsync(new ActivityLogCreateBusinessModel
 7178    {
 7179      EntityName = nameof(LgdxUser),
 7180      EntityId = user.Id.ToString(),
 7181      Action = ActivityAction.LoginSuccess,
 7182    });
 7183    return new LoginResponseBusinessModel
 7184    {
 7185      AccessToken = accessToken,
 7186      RefreshToken = refreshToken,
 7187      ExpiresMins = _lgdxRobotCloudSecretConfiguration.LgdxUserAccessTokenExpiresMins,
 7188      RequiresTwoFactor = false
 7189    };
 8190  }
 191
 192  public async Task ForgotPasswordAsync(ForgotPasswordRequestBusinessModel forgotPasswordRequestBusinessModel)
 2193  {
 2194    var user = await _userManager.FindByEmailAsync(forgotPasswordRequestBusinessModel.Email);
 2195    if (user != null)
 1196    {
 1197      var token = await _userManager.GeneratePasswordResetTokenAsync(user);
 1198      await _emailService.SendPasswordResetEmailAsync(user.Email!, user.Name!, user.UserName!, token);
 1199      await _activityLogService.CreateActivityLogAsync(new ActivityLogCreateBusinessModel
 1200      {
 1201        EntityName = nameof(LgdxUser),
 1202        EntityId = user.Id.ToString(),
 1203        Action = ActivityAction.UserPasswordReset,
 1204      });
 1205    }
 206    // For security reasons, we do not return a 404 status code.
 2207  }
 208
 209  public async Task ResetPasswordAsync(ResetPasswordRequestBusinessModel resetPasswordRequestBusinessModel)
 3210  {
 3211    var user = await _userManager.FindByEmailAsync(resetPasswordRequestBusinessModel.Email)
 3212      ?? throw new LgdxValidation400Expection(nameof(resetPasswordRequestBusinessModel.Token), "");
 213
 2214    var result = await _userManager.ResetPasswordAsync(user, resetPasswordRequestBusinessModel.Token, resetPasswordReque
 2215    if (!result.Succeeded)
 1216    {
 1217      throw new LgdxIdentity400Expection(result.Errors);
 218    }
 1219    await _emailService.SendPasswordUpdateEmailAsync(user.Email!, user.Name!, user.UserName!);
 1220    await _activityLogService.CreateActivityLogAsync(new ActivityLogCreateBusinessModel
 1221    {
 1222      EntityName = nameof(LgdxUser),
 1223      EntityId = user.Id.ToString(),
 1224      Action = ActivityAction.UserPasswordUpdated,
 1225    });
 1226  }
 227
 228  public async Task<RefreshTokenResponseBusinessModel> RefreshTokenAsync(RefreshTokenRequestBusinessModel refreshTokenRe
 4229  {
 230    // Validate the refresh token
 4231    var tokenHandler = new JwtSecurityTokenHandler();
 4232    TokenValidationParameters validationParameters = new()
 4233    {
 4234      ValidateIssuer = true,
 4235      ValidateAudience = true,
 4236      ValidateLifetime = true,
 4237      ValidateIssuerSigningKey = true,
 4238      ValidIssuer = _lgdxRobotCloudSecretConfiguration.LgdxUserJwtIssuer,
 4239      ValidAudience = _lgdxRobotCloudSecretConfiguration.LgdxUserJwtIssuer,
 4240      IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_lgdxRobotCloudSecretConfiguration.LgdxUserJwtS
 4241      ClockSkew = TimeSpan.Zero
 4242    };
 4243    ClaimsPrincipal principal = tokenHandler.ValidateToken(refreshTokenRequestBusinessModel.RefreshToken, validationPara
 244
 245    // The token is valid, check the database
 12246    var userId = principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value
 4247      ?? throw new LgdxValidation400Expection(nameof(refreshTokenRequestBusinessModel.RefreshToken), "The user ID is not
 4248    var user = await _userManager.FindByIdAsync(userId)
 4249      ?? throw new LgdxValidation400Expection(nameof(refreshTokenRequestBusinessModel.RefreshToken), "User not found.");
 3250    if (user.RefreshTokenHash != LgdxHelper.GenerateSha256Hash(refreshTokenRequestBusinessModel.RefreshToken))
 1251    {
 1252      throw new LgdxValidation400Expection(nameof(refreshTokenRequestBusinessModel.RefreshToken), "The refresh token is 
 253    }
 254
 255    // Generate new token pair and update the database
 2256    var notBefore = DateTime.UtcNow;
 2257    var accessExpires = notBefore.AddMinutes(_lgdxRobotCloudSecretConfiguration.LgdxUserAccessTokenExpiresMins);
 2258    var refreshExpires = validatedToken.ValidTo; // Reauthenticate to extend the expiration time
 2259    var accessToken = await GenerateAccessTokenAsync(user, notBefore, accessExpires);
 2260    var refreshToken = GenerateRefreshToken(user, notBefore, refreshExpires);
 2261    user.RefreshTokenHash = LgdxHelper.GenerateSha256Hash(refreshToken);
 2262    var result = await _userManager.UpdateAsync(user);
 2263    if (!result.Succeeded)
 1264    {
 1265      throw new LgdxIdentity400Expection(result.Errors);
 266    }
 267
 268    // Generate new token pair
 1269    return new RefreshTokenResponseBusinessModel()
 1270    {
 1271      AccessToken = accessToken,
 1272      RefreshToken = refreshToken,
 1273      ExpiresMins = _lgdxRobotCloudSecretConfiguration.LgdxUserAccessTokenExpiresMins
 1274    };
 1275  }
 276
 277  public async Task<bool> UpdatePasswordAsync(string userId, UpdatePasswordRequestBusinessModel updatePasswordRequestBus
 3278  {
 3279    var user = await userManager.FindByIdAsync(userId)
 3280      ?? throw new LgdxNotFound404Exception();
 281
 2282    var result = await _userManager.ChangePasswordAsync(user, updatePasswordRequestBusinessModel.CurrentPassword, update
 2283    if (!result.Succeeded)
 1284    {
 1285      throw new LgdxIdentity400Expection(result.Errors);
 286    }
 1287    await _emailService.SendPasswordUpdateEmailAsync(user.Email!, user.Name!, user.UserName!);
 1288    await _activityLogService.CreateActivityLogAsync(new ActivityLogCreateBusinessModel
 1289    {
 1290      EntityName = nameof(LgdxUser),
 1291      EntityId = user.Id.ToString(),
 1292      Action = ActivityAction.UserPasswordUpdated,
 1293    });
 1294    return true;
 1295  }
 296}