< 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
0%
Covered lines: 0
Uncovered lines: 162
Coverable lines: 162
Total lines: 233
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 48
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)0%7280%
GenerateJwtToken(...)100%210%
GenerateAccessTokenAsync()0%110100%
GenerateRefreshToken(...)100%210%
LoginAsync()0%156120%
ForgotPasswordAsync()0%620%
ResetPasswordAsync()0%2040%
RefreshTokenAsync()0%156120%
UpdatePasswordAsync()0%2040%

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
 027public class AuthService(
 028    LgdxContext context,
 029    IEmailService emailService,
 030    IOptionsSnapshot<LgdxRobotCloudSecretConfiguration> lgdxRobotCloudSecretConfiguration,
 031    UserManager<LgdxUser> userManager
 032  ) : IAuthService
 33{
 034  private readonly LgdxContext _context = context ?? throw new ArgumentNullException(nameof(context));
 035  private readonly IEmailService _emailService = emailService ?? throw new ArgumentNullException(nameof(emailService));
 036  private readonly LgdxRobotCloudSecretConfiguration _lgdxRobotCloudSecretConfiguration = lgdxRobotCloudSecretConfigurat
 037  private readonly UserManager<LgdxUser> _userManager = userManager ?? throw new ArgumentNullException(nameof(userManage
 38
 39  private JwtSecurityToken GenerateJwtToken(List<Claim> claims, DateTime notBefore, DateTime expires)
 040  {
 041    var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_lgdxRobotCloudSecretConfiguration.LgdxUserJwtSecr
 042    var credentials = new SigningCredentials(securityKey, _lgdxRobotCloudSecretConfiguration.LgdxUserJwtAlgorithm);
 043    return new JwtSecurityToken(
 044      _lgdxRobotCloudSecretConfiguration.LgdxUserJwtIssuer,
 045      _lgdxRobotCloudSecretConfiguration.LgdxUserJwtIssuer,
 046      claims,
 047      notBefore,
 048      expires,
 049      credentials);
 050  }
 51
 52  private async Task<string> GenerateAccessTokenAsync(LgdxUser user, DateTime notBefore, DateTime expires)
 053  {
 054    var userRoles = await _userManager.GetRolesAsync(user);
 055    var Claims = new List<Claim>
 056    {
 057      new (JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
 058      new (JwtRegisteredClaimNames.Sub, user.Id.ToString()),
 059      new (ClaimTypes.Name, user.UserName ?? string.Empty),
 060      new (ClaimTypes.Email, user.Email ?? string.Empty),
 061      new ("fullname", user.Name ?? string.Empty),
 062    };
 63    // Add Roles
 064    foreach (var userRole in userRoles)
 065    {
 066      Claims.Add(new Claim(ClaimTypes.Role, userRole));
 067    }
 68    // Add Role Claims
 069    {
 070      List<string> roleIds = await _context.Roles.AsNoTracking()
 071        .Where(r => userRoles.Select(ur => ur.ToUpper()).Contains(r.NormalizedName!))
 072        .Select(r => r.Id )
 073        .ToListAsync();
 074      var roleClaims = await _context.RoleClaims.AsNoTracking()
 075        .Where(r => roleIds.Contains(r.RoleId))
 076        .ToListAsync();
 077      foreach (var roleClaim in roleClaims)
 078      {
 079        Claims.Add(new Claim(roleClaim.ClaimType!, roleClaim.ClaimValue!));
 080      }
 081    }
 082    var token = GenerateJwtToken(Claims, notBefore, expires);
 083    return new JwtSecurityTokenHandler().WriteToken(token);
 084  }
 85
 86  private string GenerateRefreshToken(LgdxUser user, DateTime notBefore, DateTime expires)
 087  {
 088    var Claims = new List<Claim>
 089    {
 090      new (JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
 091      new (JwtRegisteredClaimNames.Sub, user.Id.ToString()),
 092    };
 093    var token = GenerateJwtToken(Claims, notBefore, expires);
 094    var tokenStr = new JwtSecurityTokenHandler().WriteToken(token);
 095    return tokenStr;
 096  }
 97
 98  public async Task<LoginResponseBusinessModel> LoginAsync(LoginRequestBusinessModel loginRequestBusinessModel)
 099  {
 0100    var user = await _userManager.FindByNameAsync(loginRequestBusinessModel.Username)
 0101      ?? throw new LgdxValidation400Expection(nameof(loginRequestBusinessModel.Username), "The user does not exist.");
 102
 0103    if (await _userManager.IsLockedOutAsync(user))
 0104    {
 0105      throw new LgdxValidation400Expection(nameof(loginRequestBusinessModel.Username), "The user is locked out.");
 106    }
 107
 108    // Check password and generate token
 0109    if (await _userManager.CheckPasswordAsync(user, loginRequestBusinessModel.Password))
 0110    {
 0111      var notBefore = DateTime.UtcNow;
 0112      var accessExpires = notBefore.AddMinutes(_lgdxRobotCloudSecretConfiguration.LgdxUserAccessTokenExpiresMins);
 0113      var refreshExpires = notBefore.AddMinutes(_lgdxRobotCloudSecretConfiguration.LgdxUserRefreshTokenExpiresMins);
 0114      var accessToken = await GenerateAccessTokenAsync(user, notBefore, accessExpires);
 0115      var refreshToken = GenerateRefreshToken(user, notBefore, refreshExpires);
 0116      user.RefreshTokenHash = LgdxHelper.GenerateSha256Hash(refreshToken);
 0117      var result = await _userManager.UpdateAsync(user);
 0118      if (!result.Succeeded)
 0119      {
 0120        throw new LgdxIdentity400Expection(result.Errors);
 121      }
 0122      return new LoginResponseBusinessModel
 0123      {
 0124        AccessToken = accessToken,
 0125        RefreshToken = refreshToken,
 0126        ExpiresMins = _lgdxRobotCloudSecretConfiguration.LgdxUserAccessTokenExpiresMins
 0127      };
 128    }
 129    else
 0130    {
 131      // Password is incorrect
 0132      var incrementLockoutResult = await _userManager.AccessFailedAsync(user);
 0133      if (!incrementLockoutResult.Succeeded)
 0134      {
 135        // Return the same failure we do when resetting the lockout fails after a correct password.
 0136        throw new LgdxIdentity400Expection(incrementLockoutResult.Errors);
 137      }
 0138      if (await _userManager.IsLockedOutAsync(user))
 0139      {
 0140        throw new LgdxValidation400Expection(nameof(loginRequestBusinessModel.Username), "The user is locked out.");
 141      }
 0142    }
 0143    throw new LgdxValidation400Expection(nameof(loginRequestBusinessModel.Username), "Login failed.");
 0144  }
 145
 146  public async Task ForgotPasswordAsync(ForgotPasswordRequestBusinessModel forgotPasswordRequestBusinessModel)
 0147  {
 0148    var user = await _userManager.FindByEmailAsync(forgotPasswordRequestBusinessModel.Email);
 0149    if (user != null)
 0150    {
 0151      var token = await _userManager.GeneratePasswordResetTokenAsync(user);
 0152      await _emailService.SendPasswordResetEmailAsync(user.Email!, user.Name!, user.UserName!, token);
 0153    }
 154    // For security reasons, we do not return a 404 status code.
 0155  }
 156
 157  public async Task ResetPasswordAsync(ResetPasswordRequestBusinessModel resetPasswordRequestBusinessModel)
 0158  {
 0159    var user = await _userManager.FindByEmailAsync(resetPasswordRequestBusinessModel.Email)
 0160      ?? throw new LgdxValidation400Expection(nameof(resetPasswordRequestBusinessModel.Token), "");
 161
 0162    var result = await _userManager.ResetPasswordAsync(user, resetPasswordRequestBusinessModel.Token, resetPasswordReque
 0163    if (!result.Succeeded)
 0164    {
 0165      throw new LgdxIdentity400Expection(result.Errors);
 166    }
 0167    await _emailService.SendPasswordUpdateEmailAsync(user.Email!, user.Name!, user.UserName!);
 0168  }
 169
 170  public async Task<RefreshTokenResponseBusinessModel> RefreshTokenAsync(RefreshTokenRequestBusinessModel refreshTokenRe
 0171  {
 172    // Validate the refresh token
 0173    var tokenHandler = new JwtSecurityTokenHandler();
 0174    TokenValidationParameters validationParameters = new()
 0175    {
 0176      ValidateIssuer = true,
 0177      ValidateAudience = true,
 0178      ValidateLifetime = true,
 0179      ValidateIssuerSigningKey = true,
 0180      ValidIssuer = _lgdxRobotCloudSecretConfiguration.LgdxUserJwtIssuer,
 0181      ValidAudience = _lgdxRobotCloudSecretConfiguration.LgdxUserJwtIssuer,
 0182      IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_lgdxRobotCloudSecretConfiguration.LgdxUserJwtS
 0183      ClockSkew = TimeSpan.Zero
 0184    };
 0185    ClaimsPrincipal principal = tokenHandler.ValidateToken(refreshTokenRequestBusinessModel.RefreshToken, validationPara
 0186      ?? throw new LgdxValidation400Expection(nameof(refreshTokenRequestBusinessModel.RefreshToken), "Invalid refresh to
 187
 188    // The token is valid, check the database
 0189    var userId = principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value
 0190      ?? throw new LgdxValidation400Expection(nameof(refreshTokenRequestBusinessModel.RefreshToken), "The user ID is not
 0191    var user = await _userManager.FindByIdAsync(userId)
 0192      ?? throw new LgdxValidation400Expection(nameof(refreshTokenRequestBusinessModel.RefreshToken), "User not found.");
 0193    if (user.RefreshTokenHash != LgdxHelper.GenerateSha256Hash(refreshTokenRequestBusinessModel.RefreshToken))
 0194    {
 0195      throw new LgdxValidation400Expection(nameof(refreshTokenRequestBusinessModel.RefreshToken), "The refresh token is 
 196    }
 197
 198    // Generate new token pair and update the database
 0199    var notBefore = DateTime.UtcNow;
 0200    var accessExpires = notBefore.AddMinutes(_lgdxRobotCloudSecretConfiguration.LgdxUserAccessTokenExpiresMins);
 0201    var refreshExpires = validatedToken.ValidTo; // Reauthenticate to extend the expiration time
 0202    var accessToken = await GenerateAccessTokenAsync(user, notBefore, accessExpires);
 0203    var refreshToken = GenerateRefreshToken(user, notBefore, refreshExpires);
 0204    user.RefreshTokenHash = LgdxHelper.GenerateSha256Hash(refreshToken);
 0205    var result = await _userManager.UpdateAsync(user);
 0206    if (!result.Succeeded)
 0207    {
 0208      throw new LgdxIdentity400Expection(result.Errors);
 209    }
 210
 211    // Generate new token pair
 0212    return new RefreshTokenResponseBusinessModel()
 0213    {
 0214      AccessToken = accessToken,
 0215      RefreshToken = refreshToken,
 0216      ExpiresMins = _lgdxRobotCloudSecretConfiguration.LgdxUserAccessTokenExpiresMins
 0217    };
 0218  }
 219
 220  public async Task<bool> UpdatePasswordAsync(string userId, UpdatePasswordRequestBusinessModel updatePasswordRequestBus
 0221  {
 0222    var user = await userManager.FindByIdAsync(userId)
 0223      ?? throw new LgdxNotFound404Exception();
 224
 0225    var result = await _userManager.ChangePasswordAsync(user, updatePasswordRequestBusinessModel.CurrentPassword, update
 0226    if (!result.Succeeded)
 0227    {
 0228      throw new LgdxIdentity400Expection(result.Errors);
 229    }
 0230    await _emailService.SendPasswordUpdateEmailAsync(user.Email!, user.Name!, user.UserName!);
 0231    return true;
 0232  }
 233}