Open Closed

Replacing Profile Service/Controller for Password History Implementation #9127


User avatar
0
dominik.samsel created

Hello ABP Support Team,

I’m working on implementing a password history feature in our application (using ABP) so that users cannot reuse any of their last three passwords. To accomplish this, I need to override all places where password changes occur. I’ve successfully implemented the functionality in our Identity area by replacing the default IdentityUserAppService with our custom implementation. However, I also need to override the change‑password functionality in the Profile area.

My goal is to ensure that whenever a user changes their password—whether via the Identity services or via the profile API—the new password is validated against the stored password history and the history is updated accordingly.

Here’s what I have so far:
Working Identity Service (CustomIdentityUserAppService):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Random.Portal.Emailing;
using Random.Portal.Emailing.TemplateModels;
using Random.Portal.Localization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Volo.Abp;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Caching;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Emailing;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.Identity;
using Volo.Abp.ObjectExtending;
using Volo.Abp.TextTemplating;
using Volo.Abp.Users;

`namespace Random.Portal.Identity
{
    [Dependency(ReplaceServices = true)]
    [ExposeServices(typeof(IIdentityUserAppService), typeof(IdentityUserAppService), typeof(CustomIdentityUserAppService))]
    public class CustomIdentityUserAppService : IdentityUserAppService
    {
        private readonly IEmailSender _emailSender;
        private readonly ITemplateRenderer _templateRenderer;
        private readonly IStringLocalizer<PortalResource> _localizer;
        private readonly IIdentityUserRepository _userRepository;
        private readonly IdentityUserManager _userManager;
        private readonly IPasswordHistoryService _passwordHistoryService;

        public CustomIdentityUserAppService(
            IdentityUserManager userManager,
            IIdentityUserRepository userRepository,
            IIdentityRoleRepository roleRepository,
            IOrganizationUnitRepository organizationUnitRepository,
            IIdentityClaimTypeRepository identityClaimTypeRepository,
            IdentityProTwoFactorManager identityProTwoFactorManager,
            IOptions<IdentityOptions> identityOptions,
            IDistributedEventBus distributedEventBus,
            IOptions<AbpIdentityOptions> abpIdentityOptions,
            IPermissionChecker permissionChecker,
            IDistributedCache<IdentityUserDownloadTokenCacheItem, string> downloadTokenCache,
            IDistributedCache<ImportInvalidUsersCacheItem, string> importInvalidUsersCache,
            IdentitySessionManager identitySessionManager,
            IdentityUserTwoFactorChecker identityUserTwoFactorChecker,
            IEmailSender emailSender,
            ITemplateRenderer templateRenderer,
            IStringLocalizer<PortalResource> localizer,
            IPasswordHistoryService passwordHistoryService)
            : base(userManager, userRepository,
                  roleRepository, organizationUnitRepository, identityClaimTypeRepository, identityProTwoFactorManager,
                  identityOptions, distributedEventBus, abpIdentityOptions, permissionChecker, downloadTokenCache,
                  importInvalidUsersCache, identitySessionManager, identityUserTwoFactorChecker)
        {
            _userRepository = userRepository;
            _userManager = userManager;
            _emailSender = emailSender;
            _templateRenderer = templateRenderer;
            _localizer = localizer;
            _passwordHistoryService = passwordHistoryService;
        }

        #region Overridden Methods

        [Authorize(IdentityPermissions.Users.Create)]
        public override async Task<IdentityUserDto> CreateAsync(IdentityUserCreateDto input)
        {
            var history = _passwordHistoryService.GetHistory(input.ExtraProperties);

            // Create the user using base flow.
            var identityUserDto = await base.CreateAsync(input);

            // Load the full user entity to retrieve the stored (salted) password hash.
            var identityUser = await _userRepository.GetAsync(identityUserDto.Id);
            var actualHashedPassword = identityUser.PasswordHash;

            // Update history with the actual hashed password.
            _passwordHistoryService.AddToHistory(history, actualHashedPassword);

            // Update the user with the new password history.
            identityUser.SetProperty("PasswordHistory", history);
            await _userRepository.UpdateAsync(identityUser);

            // Additional logic (e.g., sending a welcome email) omitted for brevity.
            return identityUserDto;
        }

        [Authorize(IdentityPermissions.Users.Update)]
        public override async Task UpdatePasswordAsync(Guid id, IdentityUserUpdatePasswordInput input)
        {
            var user = await UserManager.GetByIdAsync(id);
            var passwordHistory = _passwordHistoryService.GetHistory(user.ExtraProperties);

            foreach (var storedHash in passwordHistory)
            {
                var verificationResult = _userManager.PasswordHasher.VerifyHashedPassword(user, storedHash, input.NewPassword);
                if (verificationResult == PasswordVerificationResult.Success)
                {
                    throw new UserFriendlyException("You cannot reuse one of your last 3 passwords.");
                }
            }

            var previousPasswordHash = user.PasswordHash;
            (await UserManager.RemovePasswordAsync(user)).CheckErrors();
            (await UserManager.AddPasswordAsync(user, input.NewPassword)).CheckErrors();
            _passwordHistoryService.AddToHistory(passwordHistory, previousPasswordHash);
            user.SetProperty("PasswordHistory", passwordHistory);
            await _userRepository.UpdateAsync(user);
        }

        #endregion
    }
}

This part and password service works as expected, e.g. when Host admin changes password for user in the user management. The Identity password change functionality correctly uses our custom logic.

namespace Random.Portal.Controllers.Account
{
    [RemoteService(Name = AccountProPublicRemoteServiceConsts.RemoteServiceName)]
    [Area(AccountProPublicRemoteServiceConsts.ModuleName)]
    [ControllerName("Profile")]
    [ReplaceControllers(typeof(ProfileController))]
    [Route("/api/account/my-profile", Order = 0)]
    [Dependency(ReplaceServices = true)]
    [ExposeServices(typeof(ProfileController), IncludeSelf = true)]
 
    public class CustomProfileController : ProfileController, ICustomProfileAppService
    {
        private readonly ICustomProfileAppService _customProfileAppService;
        public CustomProfileController(ICustomProfileAppService customProfileAppService)
            : base(customProfileAppService)
        {
            _customProfileAppService = customProfileAppService;
        }
 
        [HttpPost]
        [Route("change-password", Order = 0)]
        public override Task ChangePasswordAsync(ChangePasswordInput input)
        {
            return _customProfileAppService.ChangePasswordAsync(input);
        }
    }
}

using Random.Portal.Identity;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Account;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Identity;
using Volo.Abp.SettingManagement;
using Volo.Abp.Timing;
using Volo.Abp.Users;
 
namespace Random.Portal.Account
{
    [Dependency(ReplaceServices = true)]
    [ExposeServices(typeof(IProfileAppService), typeof(ProfileAppService), typeof(CustomProfileAppService))]
    public class CustomProfileAppService : ProfileAppService, ICustomProfileAppService
    {
 
        private readonly IPasswordHistoryService _passwordHistoryService;
        private readonly IIdentityUserRepository _userRepository;
        public CustomProfileAppService(
            IdentityUserManager userManager,
            IdentitySecurityLogManager identitySecurityLogManager,
            IdentityProTwoFactorManager identityProTwoFactorManager,
            IOptions<IdentityOptions> identityOptions,
            IdentityUserTwoFactorChecker identityUserTwoFactorChecker,
            ITimezoneProvider timezoneProvider,
            ISettingManager settingManager,
            IPasswordHistoryService passwordHistoryService,
            IIdentityUserRepository userRepository)
            : base(userManager, identitySecurityLogManager, identityProTwoFactorManager, identityOptions,
                   identityUserTwoFactorChecker, timezoneProvider, settingManager)
        {
            _passwordHistoryService = passwordHistoryService;
            _userRepository = userRepository;
        }
 
        public override async Task ChangePasswordAsync(ChangePasswordInput input)
        {
            // Retrieve the current user.
            var currentUser = await UserManager.GetByIdAsync(CurrentUser.GetId());
 
            // For external users, changing a password is not supported.
            if (currentUser.IsExternal)
            {
                throw new BusinessException(code: IdentityErrorCodes.ExternalUserPasswordChange);
            }
 
            // If there's no password (e.g. first-time setup), set it.
            if (currentUser.PasswordHash == null)
            {
                (await UserManager.AddPasswordAsync(currentUser, input.NewPassword)).CheckErrors();
                currentUser = await UserManager.GetByIdAsync(CurrentUser.GetId());
                var newHistory = _passwordHistoryService.GetHistory(currentUser.ExtraProperties);
                // Insert the (new) hash as the starting history.
                _passwordHistoryService.AddToHistory(newHistory, currentUser.PasswordHash);
                currentUser.SetProperty("PasswordHistory", newHistory);
                await _userRepository.UpdateAsync(currentUser);
            }
            else
            {
                // If the user already has a password, then check that the new password is not reused.
                var history = _passwordHistoryService.GetHistory(currentUser.ExtraProperties);
                foreach (var storedHash in history)
                {
                    var verificationResult =
                        UserManager.PasswordHasher.VerifyHashedPassword(currentUser, storedHash, input.NewPassword);
                    if (verificationResult == PasswordVerificationResult.Success)
                    {
                        throw new UserFriendlyException("You cannot reuse one of your last 3 passwords.");
                    }
                }
 
                // Capture the current (old) password hash before updating.
                var previousPasswordHash = currentUser.PasswordHash;
 
                // Proceed with the password change using the standard flow.
                (await UserManager.ChangePasswordAsync(currentUser, input.CurrentPassword, input.NewPassword)).CheckErrors();
 
                // Update the password history: add the previous hash.
                _passwordHistoryService.AddToHistory(history, previousPasswordHash);
                currentUser.SetProperty("PasswordHistory", history);
                await _userRepository.UpdateAsync(currentUser);
            }
 
            // Log the security event.
            await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext
            {
                Identity = IdentitySecurityLogIdentityConsts.Identity,
                Action = IdentitySecurityLogActionConsts.ChangePassword
            });
        }
    }
}

These are the implementations of custom ProfileAppService and custom ProfileController, not working.

My Issue:

I also need to override the change password functionality in the Profile area (for example, when a user changes for themself, their password using the profile endpoint, basically when he enters "My account" and "Change password" there). I created a CustomProfileAppService that extends ProfileAppService and a CustomProfileController that inherits from ProfileController. However, my attempts to replace or override the default ProfileController and ProfileAppService have not been successful. I have tried various attribute combinations (including [ReplaceControllers] and [Dependency(ReplaceServices = true)] with [ExposeServices]) without success.

Our API currently resolves to the default endpoint (for example, https://auth.mytestdomain.com/api/account/my-profile/change-password), but I want it to use our custom implementation. I’m unsure whether I should be replacing the service or the controller—or if both need to be replaced. I’ve reviewed ABP’s documentation (e.g. on object extensions and remote services) but I’m still confused about the best approach.

Projects and classes placement:

  • CustomIdentityUserAppService(working) - Random.Portal.Application/Identity/...

  • CustomProfileAppService - Random.Portal.Application/Account/...

  • ICustomProfileAppService - Random.Portal.Application.Contracts/Account/...

  • CustomProfileController - Random.Portal.HttpApi/Account/...

Questions:

What is the recommended approach to replace the default Profile functionality for our password history feature? Should we override the ProfileAppService or the ProfileController?

Are there any specific attributes or configuration steps in ABP I need to follow to properly replace the default service/controller so that my custom implementation is used (especially regarding routing and Swagger/OpenAPI generation)?

Could there be issues with the lifetime or DI registration that conflict between our custom implementation and the built‑in services, and if so, what is the best way to resolve them?

Any advice and guidance from your team would be greatly appreciated.

Thank you in advance for your assistance.
Best regards,
Dominik Samsel


5 Answer(s)
  • User Avatar
    0
    EngincanV created
    Support Team .NET Developer

    Hi, here are the answers to some of your questions:

    Should we override the ProfileAppService or the ProfileController?

    To apply business logic like password history checks, overriding the ProfileAppService is the right and recommended approach.

    The ProfileController in Volo.Abp.Account.Pro is just a thin wrapper that calls into the IProfileAppService. So you don’t need to replace the controller if your goal is only to apply logic like password history checks.

    Could there be issues with the lifetime or DI registration that conflict between our custom implementation and the built‑in services, and if so, what is the best way to resolve them?

    To answer this question properly, can you set a breakpoint to your own implementation and let me know if they are called or not?


    After the confirmation, I can assist you better.

    Regards.

  • User Avatar
    0
    dominik.samsel created

    Hi,

    I don't mind using only customised ProfileAppService, but unfortunately breakpoint in the method of CustomProfileAppService has not been hit

  • User Avatar
    0
    EngincanV created
    Support Team .NET Developer

    Hi,

    I don't mind using only customised ProfileAppService, but unfortunately breakpoint in the method of CustomProfileAppService has not been hit

    Okay, then this means the dependency replacement is not correct. Can you update it as below and try it again?

        [Dependency(ReplaceServices = true)]
        [ExposeServices(typeof(IProfileAppService), typeof(ProfileAppService), typeof(CustomProfileAppService))]
        public class CustomProfileAppService : ProfileAppService, ICustomProfileAppService, IProfileAppService
        {
            //ommited for brevity.
        }
    

    If your ICustomProfileAppService implements the IProfileAppService, then this should be not needed, but can you confirm please?


    Alternatively, in your module class, you can add the following line to replace it manually:

    context.Services.Replace(
        ServiceDescriptor.Transient<IProfileAppService, CustomProfileAppService>()
    );
    
  • User Avatar
    0
    dominik.samsel created

    I have tried both suggested ways, implementing ICustomProfileAppService, as well as adding code in my module class, but unfortunately breakpoint in the ChangePasswordAsync hasn't been hit.

    Just to confirm, in order to test it, I'm logging as a user into the portal, picking "My account" from user settings and the "Change the password". I have noticed, this is different application from main portal(Account is from "Auth" project and it's hosted on the different page).
    image.png

    Should I use something else to override change password functionality on that site? I was following request "change-password" and in the source code of the project Account.Pro, ProfileController was responsible for that.
    image.png

    Address for the portal:
    https://web.mydomain.com:44356/
    However, I suspect when we talk about the service solution it doesn't have the impact there.

  • User Avatar
    0
    EngincanV created
    Support Team .NET Developer

    Just to confirm, in order to test it, I'm logging as a user into the portal, picking "My account" from user settings and the "Change the password". I have noticed, this is different application from main portal(Account is from "Auth" project and it's hosted on the different page).

    Yes, you should add your dependency replacement in the relevant application. You might be doing the replacement in the wrong application. Can you also try that and let me know?

    Because with the suggested ways, it should have hit the breakpoints and you should have seen them in the debug mode.

Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
Do you need assistance from an ABP expert?
Schedule a Meeting
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v9.3.0-preview. Updated on April 16, 2025, 12:13