Base solution for your next web application
Open Closed

Unable to resolve service for type LogInManager #10105


User avatar
0
SBJ created

Prerequisites

Please answer the following questions before submitting an issue.

  • What is your product version? 8.7.0
  • What is your product type (Angular or MVC)? MVC
  • What is product framework type (.net framework or .net core)? .NET Core

<br> Hello,

We are currently working on ahybrid with asp.net core and Vue.js. Vue as our running frontend. Because we call Services and Controllers in the frontend we want to authenticate them. So we have been working on getting the authentication to work and added the TokenAuthController to the frontend as a seperate Service. This service has no errors and we did services.AddScoped in the startup.cs but when we try to run it gives back the following error:

System.AggregateException: 'Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: RMS.Web.Website.Philips.Services.IAuthenticationService Lifetime: Transient ImplementationType: RMS.Web.Website.Philips.AuthenticationService': Unable to resolve service for type 'RMS.Authorization.LogInManager' while attempting to activate 'RMS.Web.Website.Philips.AuthenticationService'.)'

How can we authenticate the frontend so we can safely call our Services/Controller?

AuthenticationService.cs:

    public async Task<AuthenticateResultModel> Authenticate(AuthenticateModel model)
    {
        /*if (UseCaptchaOnLogin())
        {
            await ValidateReCaptcha(model.CaptchaResponse);
        }*/

        var loginResult = await GetLoginResultAsync(
            model.UserNameOrEmailAddress,
            model.Password,
            GetTenancyNameOrNull()
        );

        var returnUrl = model.ReturnUrl;

        if (model.SingleSignIn.HasValue && model.SingleSignIn.Value && loginResult.Result == AbpLoginResultType.Success)
        {
            loginResult.User.SetSignInToken();
            returnUrl = AddSingleSignInParametersToReturnUrl(model.ReturnUrl, loginResult.User.SignInToken, loginResult.User.Id, loginResult.User.TenantId);
        }

        //Password reset
        if (loginResult.User.ShouldChangePasswordOnNextLogin)
        {
            loginResult.User.SetNewPasswordResetCode();
            return new AuthenticateResultModel
            {
                ShouldResetPassword = true,
                PasswordResetCode = loginResult.User.PasswordResetCode,
                UserId = loginResult.User.Id,
                ReturnUrl = returnUrl
            };
        }

        //Two factor auth
        await _userManager.InitializeOptionsAsync(loginResult.Tenant?.Id);

        string twoFactorRememberClientToken = null;
        if (await IsTwoFactorAuthRequiredAsync(loginResult, model))
        {
            if (model.TwoFactorVerificationCode.IsNullOrEmpty())
            {
                //Add a cache item which will be checked in SendTwoFactorAuthCode to prevent sending unwanted two factor code to users.
                _cacheManager
                    .GetTwoFactorCodeCache()
                    .Set(
                        loginResult.User.ToUserIdentifier().ToString(),
                        new TwoFactorCodeCacheItem()
                    );

                return new AuthenticateResultModel
                {
                    RequiresTwoFactorVerification = true,
                    UserId = loginResult.User.Id,
                    TwoFactorAuthProviders = await _userManager.GetValidTwoFactorProvidersAsync(loginResult.User),
                    ReturnUrl = returnUrl
                };
            }

            twoFactorRememberClientToken = await TwoFactorAuthenticateAsync(loginResult.User, model);
        }

        // One Concurrent Login 
        if (AllowOneConcurrentLoginPerUser())
        {
            await _userManager.UpdateSecurityStampAsync(loginResult.User);
            await _securityStampHandler.SetSecurityStampCacheItem(loginResult.User.TenantId, loginResult.User.Id, loginResult.User.SecurityStamp);
            loginResult.Identity.ReplaceClaim(new Claim(AppConsts.SecurityStampKey, loginResult.User.SecurityStamp));
        }

        var accessToken = CreateAccessToken(await CreateJwtClaims(loginResult.Identity, loginResult.User));
        var refreshToken = CreateRefreshToken(await CreateJwtClaims(loginResult.Identity, loginResult.User, tokenType: TokenType.RefreshToken));

        return new AuthenticateResultModel
        {
            AccessToken = accessToken,
            ExpireInSeconds = (int)_configuration.AccessTokenExpiration.TotalSeconds,
            RefreshToken = refreshToken,
            RefreshTokenExpireInSeconds = (int)_configuration.RefreshTokenExpiration.TotalSeconds,
            EncryptedAccessToken = GetEncryptedAccessToken(accessToken),
            TwoFactorRememberClientToken = twoFactorRememberClientToken,
            UserId = loginResult.User.Id,
            ReturnUrl = returnUrl
        };
    }

    private bool UseCaptchaOnLogin()
    {
        return _settingManager.GetSettingValue<bool>(AppSettings.UserManagement.UseCaptchaOnLogin);
    }

    private async Task<AbpLoginResult<Tenant, User>> GetLoginResultAsync(string usernameOrEmailAddress, string password, string tenancyName)
    {
        var loginResult = await _logInManager.LoginAsync(usernameOrEmailAddress, password, tenancyName);

        switch (loginResult.Result)
        {
            case AbpLoginResultType.Success:
                return loginResult;
            default:
                throw _abpLoginResultTypeHelper.CreateExceptionForFailedLoginAttempt(loginResult.Result, usernameOrEmailAddress, tenancyName);
        }
    }

    private string GetTenancyNameOrNull()
    {
        /*            if (!AbpSession.TenantId.HasValue)
                    {
                        return null;
                    }*/

        /*return _tenantCache.GetOrNull(AbpSession.TenantId.Value)?.TenancyName;*/

        return _appConfiguration["Tenant:Name"];
    }

    private static string AddSingleSignInParametersToReturnUrl(string returnUrl, string signInToken, long userId, int? tenantId)
    {
        returnUrl += (returnUrl.Contains("?") ? "&" : "?") +
                     "accessToken=" + signInToken +
                     "&userId=" + Convert.ToBase64String(Encoding.UTF8.GetBytes(userId.ToString()));
        if (tenantId.HasValue)
        {
            returnUrl += "&tenantId=" + Convert.ToBase64String(Encoding.UTF8.GetBytes(tenantId.Value.ToString()));
        }

        return returnUrl;
    }

    private async Task<bool> IsTwoFactorAuthRequiredAsync(AbpLoginResult<Tenant, User> loginResult, AuthenticateModel authenticateModel)
    {
        if (!await _settingManager.GetSettingValueAsync<bool>(AbpZeroSettingNames.UserManagement.TwoFactorLogin.IsEnabled))
        {
            return false;
        }

        if (!loginResult.User.IsTwoFactorEnabled)
        {
            return false;
        }

        if ((await _userManager.GetValidTwoFactorProvidersAsync(loginResult.User)).Count <= 0)
        {
            return false;
        }

        if (await TwoFactorClientRememberedAsync(loginResult.User.ToUserIdentifier(), authenticateModel))
        {
            return false;
        }

        return true;
    }

    private async Task<string> TwoFactorAuthenticateAsync(User user, AuthenticateModel authenticateModel)
    {
        var twoFactorCodeCache = _cacheManager.GetTwoFactorCodeCache();
        var userIdentifier = user.ToUserIdentifier().ToString();
        var cachedCode = await twoFactorCodeCache.GetOrDefaultAsync(userIdentifier);
        var provider = _cacheManager.GetCache("ProviderCache").Get("Provider", cache => cache).ToString();

        if (provider == GoogleAuthenticatorProvider.Name)
        {
            if (!await _googleAuthenticatorProvider.ValidateAsync("TwoFactor", authenticateModel.TwoFactorVerificationCode, _userManager, user))
            {
                /*throw; new UserFriendlyException(L("InvalidSecurityCode"));*/
                return null;
            }
        }
        else if (cachedCode?.Code == null || cachedCode.Code != authenticateModel.TwoFactorVerificationCode)
        {
            //throw new UserFriendlyException(L("InvalidSecurityCode"));
            return null;
        }

        //Delete from the cache since it was a single usage code
        await twoFactorCodeCache.RemoveAsync(userIdentifier);

        if (authenticateModel.RememberClient)
        {
            if (await _settingManager.GetSettingValueAsync<bool>(AbpZeroSettingNames.UserManagement.TwoFactorLogin.IsRememberBrowserEnabled))
            {
                return CreateAccessToken(new[]
                    {
                        new Claim(UserIdentifierClaimType, user.ToUserIdentifier().ToString())
                    },
                    TimeSpan.FromDays(365)
                );
            }
        }

        return null;
    }

    private bool AllowOneConcurrentLoginPerUser()
    {
        return _settingManager.GetSettingValue<bool>(AppSettings.UserManagement.AllowOneConcurrentLoginPerUser);
    }

    private string CreateAccessToken(IEnumerable<Claim> claims, TimeSpan? expiration = null)
    {
        return CreateToken(claims, expiration ?? _configuration.AccessTokenExpiration);
    }

    private string CreateRefreshToken(IEnumerable<Claim> claims)
    {
        return CreateToken(claims, AppConsts.RefreshTokenExpiration);
    }

    private async Task<IEnumerable<Claim>> CreateJwtClaims(ClaimsIdentity identity, User user, TimeSpan? expiration = null, TokenType tokenType = TokenType.AccessToken)
    {
        var tokenValidityKey = Guid.NewGuid().ToString();
        var claims = identity.Claims.ToList();
        var nameIdClaim = claims.First(c => c.Type == _identityOptions.ClaimsIdentity.UserIdClaimType);

        if (_identityOptions.ClaimsIdentity.UserIdClaimType != JwtRegisteredClaimNames.Sub)
        {
            claims.Add(new Claim(JwtRegisteredClaimNames.Sub, nameIdClaim.Value));
        }

        claims.AddRange(new[]
        {
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.Now.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64),
            new Claim(AppConsts.TokenValidityKey, tokenValidityKey),
            new Claim(AppConsts.UserIdentifier, user.ToUserIdentifier().ToUserIdentifierString()),
            new Claim(AppConsts.TokenType, tokenType.To<int>().ToString())
         });

        if (!expiration.HasValue)
        {
            expiration = tokenType == TokenType.AccessToken
                ? _configuration.AccessTokenExpiration
                : _configuration.RefreshTokenExpiration;
        }

        _cacheManager
            .GetCache(AppConsts.TokenValidityKey)
            .Set(tokenValidityKey, "", absoluteExpireTime: expiration);

        await _userManager.AddTokenValidityKeyAsync(
            user,
            tokenValidityKey,
            DateTime.UtcNow.Add(expiration.Value)
        );

        return claims;
    }

    private static string GetEncryptedAccessToken(string accessToken)
    {
        return SimpleStringCipher.Instance.Encrypt(accessToken, AppConsts.DefaultPassPhrase);
    }

    private async Task<bool> TwoFactorClientRememberedAsync(UserIdentifier userIdentifier, AuthenticateModel authenticateModel)
    {
        if (!await _settingManager.GetSettingValueAsync<bool>(AbpZeroSettingNames.UserManagement.TwoFactorLogin.IsRememberBrowserEnabled))
        {
            return false;
        }

        if (string.IsNullOrWhiteSpace(authenticateModel.TwoFactorRememberClientToken))
        {
            return false;
        }

        try
        {
            var validationParameters = new TokenValidationParameters
            {
                ValidAudience = _configuration.Audience,
                ValidIssuer = _configuration.Issuer,
                IssuerSigningKey = _configuration.SecurityKey
            };

            foreach (var validator in _jwtOptions.Value.SecurityTokenValidators)
            {
                if (validator.CanReadToken(authenticateModel.TwoFactorRememberClientToken))
                {
                    try
                    {
                        var principal = validator.ValidateToken(authenticateModel.TwoFactorRememberClientToken, validationParameters, out _);
                        var useridentifierClaim = principal.FindFirst(c => c.Type == UserIdentifierClaimType);
                        if (useridentifierClaim == null)
                        {
                            return false;
                        }

                        return useridentifierClaim.Value == userIdentifier.ToString();
                    }
                    catch (Exception ex)
                    {
                        _logger.Debug(ex.ToString(), ex);
                    }
                }
            }
        }
        catch (Exception ex)
        {
            _logger.Debug(ex.ToString(), ex);
        }

        return false;
    }

    private string CreateToken(IEnumerable<Claim> claims, TimeSpan? expiration = null)
    {
        var now = DateTime.UtcNow;

        var jwtSecurityToken = new JwtSecurityToken(
            issuer: _configuration.Issuer,
            audience: _configuration.Audience,
            claims: claims,
            notBefore: now,
            signingCredentials: _configuration.SigningCredentials,
            expires: expiration == null ?
                (DateTime?)null :
                now.Add(expiration.Value)
        );

        return new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
    }

Startup.cs:

public class Startup
{
    private readonly string SPA_ROOT_PATH = "ClientApp";
    private readonly string SPA_SOURCE_PATH = "dist";

    public Startup(
        IConfiguration configuration
        )
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {

        services.AddControllers();
        services.AddSpaStaticFiles(configuration =>
        {
            configuration.RootPath = SPA_ROOT_PATH;
        });

        services.AddScoped<IAuthenticationService, AuthenticationService>();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IAuthenticationService authenticationService)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();
        app.UseSpaStaticFiles();
        app.UseAuthentication();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });

        authenticationService.Authenticate(new Models.TokenAuth.AuthenticateModel
        {
            UserNameOrEmailAddress = Configuration["Authentication:UserNameOrEmailAddress"],
            Password = Configuration["Authentication:Password"]
        });

        app.UseSpa(spa =>
        {
            if (env.IsDevelopment())
            { 
                spa.Options.SourcePath = $"{SPA_ROOT_PATH}/";
                spa.UseVueCli(npmScript: "serve");
            }
            else
            { 
                spa.Options.SourcePath = SPA_SOURCE_PATH;
            }
        });
    }
}

6 Answer(s)
  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi @SBJ

    The default TokenAuthController's Authenticate method should work out of the box. Why did you need to create your own AuthenticationService ? You can take a look at https://docs.aspnetzero.com/en/aspnet-core-angular/latest/Features-Angular-Token-Based-Authentication for authenticating your Vue app.

  • User Avatar
    0
    SBJ created

    Hi @ismcagdas

    The reason is because we've integrated Vue as a C# project in the solution (basically similar to the Public project, but then with a Vue bootstrapping). We need to be able to authenticate and authorize in Startup.cs instead of using a controller, that's why we went for a service approach instead of a controller approach. We've stored our credentials in the appsettings.json, because the front-end is JS based and we would prefer not having "traceable" credentials present there with a controller call.

    And if the above isn't best practice for this scenario, what would you suggest be the best approach for this, so that front-end users with the know-how wouldn't be able to abuse traceability of credentials and check the transpiled/compiled JS?

  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi @SBJ

    I don't have any experience with such a usage (Vue as a C# project) but if it is working as a standard SPA application, you can use a similar approach with AspNet Zero's Angular app.

  • User Avatar
    0
    SBJ created

    Hi @ismcagdas

    I see. We'll download the Angular version and see if we can use that as an example to make it work for us.

    Thanks for the swift reply.

  • User Avatar
    0
    ismcagdas created
    Support Team

    Thanks :), please let us know if you face any problems while doing that.

  • User Avatar
    0
    SBJ created

    Hi @ismcagdas

    Considering we're running across a problem that is more of a "superset" to this problem, I'll close this and open a new issue. Thanks. :-)