Changing an Insite User's Username


So, you want to allow your users to change their username? I recently worked with a client who wanted the functionality, in their website, because their users had grown to expect the functionality. Personally, I recommend against the following customization, if possible, because Insite wisely chose to make a user’s username immutable. I suspect their reasons were similar to the following:

  1. The user’s username the natural key for the entity. In other words, this field is used to match users when refreshing user data from the ERP.
  2. A user’s username is used in the audit trail for all of Insite’s data entities. Specifically, the CreatedBy and ModifiedBy fields are populated with the user’s username when they create or change a business object.

The following article investigates a work-around to an immutable username. It gives the impression that the user can change their username. In reality the username is not changed. Instead, an alias is stored in a custom property called EffectiveUserName. One drawback to this approach is that the an alias cannot duplicate any other alias, nor the user's original username. Thus, the number of unavailable usernames can be as high as 2u, where u is the number of users in the system.

At the high level, this task requires updating the following systems, in Insite:

  • UpdateAccountHandler – The chain of responsibility responsible for modifying a user’s account.
  • IdentityServer – Service responsible for authenticating users.
  • AddSessionHandler – The chain of responsibility responsible for adding a session for the user, when they log in.

Adding a Custom Property Repository

For simplicity, we add a custom property repository that can search for an effective username, in order to determine the user. Or, determine whether an effective username is used.

ICustomPropertyRepository.cs
public interface ICustomPropertyRepository : IRepository
{
        CustomProperty GetEffectiveUserName(string effectiveUserName);
        bool IsEffectiveUserNameUsed(string effectiveUserName);
}
CustomPropertyRepository.cs
public class CustomPropertyRepository: Repository, ICustomPropertyRepository
    {
        /// 
        /// The table name for the custom property that contains the effective user name.
        /// 
        public const string UserProfileTable = "UserProfile";

        /// 
        /// The name of the custom property that contains the effective user name.
        /// 
        public const string EffectiveUserName = "EffectiveUserName";

        public CustomProperty GetEffectiveUserName(string effectiveUserName)
        {
            return GetTable()
                .Where(cp => cp.ParentTable == UserProfileTable && cp.Name == EffectiveUserName)
                .FirstOrDefault(cp => cp.Value.Equals(effectiveUserName, StringComparison.InvariantCultureIgnoreCase));
        }

        public bool IsEffectiveUserNameUsed(string effectiveUserName)
        {
            return GetTable()
                .Where(cp => cp.ParentTable == UserProfileTable && cp.Name == EffectiveUserName)
                .Any(cp => cp.Value.Equals(effectiveUserName, StringComparison.InvariantCultureIgnoreCase));
        }
    }

Update the Database

Out of the box, the CustomProperty table isn’t optimized for searching by Name. Also, we want to ensure that the AppDict connection from entity to CustomProperty is set up correctly. So, we have the following migration script.

CreateEffectiveUserNameAttribute.sql
-- Add an index for ParentTable/Name to speed up checking for existing user names.
if not exists (select top 1 1 from sys.indexes where name='IX_CustomProperty_ParentTable_Name_Value' and object_id = object_id('dbo.CustomProperty'))
	create nonclustered index IX_CustomProperty_ParentTable_Name_Value 
		on CustomProperty (
			Name ASC,
			ParentTable ASC
		)
		include ( Value )

-- Add the custom property "EffectiveUserName"
declare @userProfileEntityId uniqueidentifier;

select top 1 @userProfileEntityId = Id 
	from AppDict.EntityConfiguration 
	where name = 'userProfile';

if not exists (select 1 from [AppDict].[PropertyConfiguration] where Name = 'effectiveUserName')
	insert into [AppDict].[PropertyConfiguration] (EntityConfigurationId, Name, Label, ControlType, IsRequired, PropertyType, IsCustomProperty, CanView, CanEdit) 
		values ( @userProfileEntityId, 'effectiveUserName', 'Effective User Name', 'Insite.Admin.ControlTypes.TextFieldControl', 0, 'System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089', 1, 1, 0)

Changes to Update Account

We modify the Update Account handler chain to allow changing the user's username. Fortunately, the AccountModel passed into the web API already has a UserName property. However, Insite doesn't propagate that username into the chain of responsibility (in the UpdateAccountParameter). So, we will need to extend the UpdateAccountParameter with the UserName, and add the effective username to the user's custom properties.

UpdateAccountUsernameHandler.cs
/// 
/// Handles processing a request to change a user's username.
/// 
[DependencyName("UpdateAccountUsernameHandler")]
public class UpdateAccountUsernameHandler : HandlerBase
{
	/// 
	/// Chain of responsibility order - executes after default handler (500).
	/// 
	public override int Order => 600;

	/// 
	/// Helper to determine who is authorized to change a user's user name.
	/// 
	protected readonly IAccountHelper AccountHelper;

	/// 
	/// Constructor.
	/// 
	/// Helper to determine who is authorized to change a user's user name.
	public UpdateAccountUsernameHandler(IAccountHelper accountHelper)
	{
		AccountHelper = accountHelper;
	}

	/// 
	/// Handles the processing for this handler.
	/// 
	/// 
	/// 
	/// 
	/// 
	public override UpdateAccountResult Execute(IUnitOfWork unitOfWork, UpdateAccountParameter parameter, UpdateAccountResult result)
	{
		if (parameter.Id == null)
		{
			return NextHandler.Execute(unitOfWork, parameter, result);
		}

		var rioParameter = parameter as UpdateAccountParameterRio;
		if (!string.IsNullOrWhiteSpace(rioParameter?.UserName))
		{
			try
			{
				var userProfile = result.GetAccountResult?.UserProfile ?? unitOfWork.GetRepository().Get(parameter.Id.Value);

				// Ensure that the user is authorized to change the user's username.
				// TODO: This duplicates code in the default UpdateAccountHandler. You may decide to eliminate this.
				if (SiteContext.Current.UserProfile.Id != userProfile.Id)
				{
					var canUpdateUsers = AccountHelper.RolesThatCanUpdateUsers;

					if (!canUpdateUsers.Any(role => SiteContext.Current.IsUserInRole(role)))
						return CreateErrorServiceResult(result, SubCode.Forbidden, MessageProvider.Current.Forbidden);
				}

				var effectiveUserName = userProfile.GetProperty(CustomPropertyRepository.EffectiveUserName, null);

				// No change needed.
				if (effectiveUserName == rioParameter.UserName || (effectiveUserName == null && userProfile.UserName == rioParameter.UserName))
				{
					return NextHandler.Execute(unitOfWork, parameter, result);
				}
				var userNameExists = unitOfWork.GetRepository().GetTable()
										 .Any(up => up.UserName.Equals(rioParameter.UserName))
									 || unitOfWork.GetTypedRepository().IsEffectiveUserNameUsed(rioParameter.UserName);
				// If the username already exists, return an error.
				if (userNameExists)
				{
					return CreateErrorServiceResult(result, SubCode.AccountServiceUserNameAlreadyExists, MessageProvider.Current.CreateNewAccountInfo_UserName_AlreadyExists);
				}

				// Otherwise, set the effective username.
				userProfile.SetProperty(CustomPropertyRepository.EffectiveUserName, rioParameter.UserName);
			}
			catch (Exception ex)
			{
				LogHelper.For(this).Error("An unexpected error occurred.", ex);
			}
		}

		return NextHandler.Execute(unitOfWork, parameter, result);
	}
}
PatchAccountMapperXc .cs
/// 
/// Update/patch mapper for Accounts. The parameter is mapped to a .
/// 
public class PatchAccountMapperXc : PatchAccountMapper
{
	/// 
	/// Constructor.
	/// 
	/// 
	/// 
	/// 
	public PatchAccountMapperXc(IGetAccountMapper getAccountMapper, IObjectToObjectMapper objectToObjectMapper, IRouteDataProvider routeDataProvider) 
		: base(getAccountMapper, objectToObjectMapper, routeDataProvider)
	{
		// no op
	}

	/// 
	/// Maps the paramter from the web api to the service parameter.
	/// 
	/// The web api parameter.
	/// The http request message.
	/// The service parameter.
	public override UpdateAccountParameter MapParameter(AccountModel apiParameter, HttpRequestMessage request)
	{
		var accountId = RouteDataProvider.GetRouteValue(request, "accountid");

		var serviceParameter = new UpdateAccountParameterXc();
		// Will include copying the UserName parameter from the api parameter to the service paramter..
		ObjectToObjectMapper.Map(apiParameter, serviceParameter);
		serviceParameter.Id = accountId.ToCurrentOrGuid();
		return serviceParameter;
	}
}
AccountMapperStartupTaskXc.cs
/// 
/// Mapping for UserProfile parameters and results with username changing.
/// 
[BootStrapperOrder(100), ExcludeFromCodeCoverage]
public class AccountMapperStartupTaskXc : IStartupTask
{
	public void Run()
	{
		Mapper.CreateMap()
			.ForMember(dest => dest.Id, config => config.MapFrom(src => src.Id.ToCurrentOrGuid()));
	}
}
UpdateAccountParameterXc.cs
/// 
/// The  extension for a changeable username. 
/// This model includes the username to allow a "mutable" username.
/// Since Insite has an immutable username, this functionality is managed through a custom property 
/// for their effective username.
/// 
public class UpdateAccountParameterXc : UpdateAccountParameter
{
	/// 
	/// Added to allow changing the username.
	/// 
	public string UserName { get; set; }
}

Changes to Identity Server

We’ll springboard our changes from Justin Pettinger’s article about extending Identity Server. Our changes are simply about detecting the case when an extended username has been received, and then replacing the parameter username with the actual username.

XcIdentityFactory.cs

/// 
/// Implmentation of , handles the registration of the Xcentium User Manager and the User Service.
/// 
public static class XcIdentityFactory
{
	public static IdentityServerServiceFactory Configure(string connectionString)
	{
		var factory = Factory.Configure(connectionString);

		factory.Register(new Registration());
		factory.UserService = new Registration();

		return factory;
	}
}
XcUserManager.cs
/// 
/// An implementation of the . 
/// This user manager is responsible for replacing a user's effective username with their actual username.
/// 
public class XcUserManager : IdentityUserManager
{
	/// 
	/// Unit of work factory.
	/// 
	protected readonly IUnitOfWorkFactory UnitOfWorkFactory;

	public XcUserManager(IdentityUserStore store, IUnitOfWorkFactory unitOfWorkFactory) 
		: base(store)
	{
		UnitOfWorkFactory = unitOfWorkFactory;
	}

	/// 
	/// Verifies a user's password. Overridden to ensure that effective usernames are replaced with actual usernames.
	/// 
	/// The user store.
	/// The user whose password is being validated.
	/// The password to validate.
	/// true if the user/password combination are valid. Otherwise, false.
	protected override Task VerifyPasswordAsync(IUserPasswordStore store, IdentityUser user, string password)
	{
		var unitOfWork = UnitOfWorkFactory.GetUnitOfWork();
		var matchedEffectiveUserName = unitOfWork.GetTypedRepository()
			.GetEffectiveUserName(user.UserName);
		if (matchedEffectiveUserName != null)
		{
			var userProfile = unitOfWork.GetRepository().Get(matchedEffectiveUserName.ParentId);
			if (userProfile == null)
			{
				return Task.Run(() => false);
			}
			user.UserName = userProfile.UserName;
		}
		return base.VerifyPasswordAsync(store, user, password);
	}

	/// 
	/// Constructs the .
	/// 
	/// Construction options.
	/// 
	/// A newly constructed .
	public static IdentityUserManager CreateXc(IdentityFactoryOptions options, IOwinContext context)
	{
		var unitOfWorkFactory = ServiceLocator.Current.GetInstance();
		var manager = new XcUserManager(new XcUserStore(context.Get()), unitOfWorkFactory);
		var dataProtectionProvider = options.DataProtectionProvider;
		if (dataProtectionProvider != null)
		{
			manager.UserTokenProvider = new DataProtectorTokenProvider(dataProtectionProvider.Create("ASP.NET Identity"));
		}
		return manager;
	}
}
XcUserService.cs
/// 
/// An implementation of  for IdentityServer3.
/// Ensures that a user logging in with their effective username get their username replaced with the actual one.
/// 
public class XcUserService : UserService
{
	/// 
	/// Constuctor.
	/// 
	/// 
	/// 
	public XcUserService(XcUserManager userManager, IUnitOfWorkFactory unitOfWorkFactory) 
		: base(userManager, unitOfWorkFactory)
	{
	}

	/// 
	/// Ensures that the effective username is replaced with the actual one, before regular processing.
	/// 
	/// The context.
	/// 
	public override Task AuthenticateLocalAsync(LocalAuthenticationContext ctx)
	{
		var matchedEffectiveUserName = UnitOfWork.GetTypedRepository()
			.GetEffectiveUserName(ctx.UserName);

		if (matchedEffectiveUserName != null)
		{
			var effectiveUser = UnitOfWork.GetRepository().Get(matchedEffectiveUserName.ParentId);
			if (effectiveUser != null)
			{
				ctx.UserName = effectiveUser.UserName;
			}
		}

		return base.AuthenticateLocalAsync(ctx);
	}
}
XcUserStore.cs
/// 
/// An implmentation of the .
/// Ensures that logging in with an effective username will log in with the actual immutable username.
/// 
public class XcUserStore : IdentityUserStore
{
	/// 
	/// The unit of work factory.
	/// 
	protected readonly IUnitOfWorkFactory UnitOfWorkFactory;

	/// 
	/// Constructor.
	/// 
	/// 
	public XcUserStore(IdentityDbContext context) 
		: base(context)
	{
		UnitOfWorkFactory = ServiceLocator.Current.GetInstance();
	}

	/// 
	/// Ensures that a user logging in with their effective username, will be logged in with their actual username.
	/// 
	/// The username from the user. (Could be effective username, or actual one)
	/// 
	public override async Task FindByNameAsync(string userName)
	{
		// Check that the username is an actual username.
		var user = await Users.FirstOrDefaultAsync(u => u.UserName == userName);
		if (user != null)
		{
			return user;
		}

		// Check for an effective username match.
		var unitOfWork = UnitOfWorkFactory.GetUnitOfWork();
		var effectiveUser = unitOfWork.GetTypedRepository()
			.GetEffectiveUserName(userName);
		if (effectiveUser == null)
		{
			return null;
		}

		// Get the userprofile associated with the effective username.
		var userProfile = unitOfWork.GetRepository().Get(effectiveUser.ParentId);
		if (userProfile == null)
		{
			return null;
		}
		return await Users.FirstOrDefaultAsync(u => u.UserName == userProfile.UserName);
	}
}
Startup.Auth.cs
/// 
/// Authorization Configuration part of OWIN Startup.
/// 
public partial class Startup
{
	/// 
	/// Initializes static members of the  class.
	/// Initializes the  class.
	/// Enable the application to use OAuthAuthorization. You can then secure your Web APIs
	/// 
	static Startup()
	{
		// ... 
		
		// Configure Identity Server
		SecurityOptions.IdentityServerOptions = new IdentityServerOptions
		{
			// ...
			Factory = XcIdentityFactory.Configure(ConnectionStringProvider.Current.ConnectionStringName),
			// ...
		};
		
		// ...
	}
}

Changes to Add Session

Finally, we need to add a hook into the Add Session chain of responsibility that will replace the user’s effective username with the original one, for logging in.

AddSessionUsernameHandler
/// 
/// Ensures that a user logging in with an altered username will log in correctly.
/// 
[DependencyName("AddSessionUsernameHandler")]
public class AddSessionUsernameHandler : HandlerBase
{
	/// 
	/// Updates the effective username with the actual one.
	/// 
	/// 
	/// 
	/// 
	/// 
	public override AddSessionResult Execute(IUnitOfWork unitOfWork, AddSessionParameter parameter, AddSessionResult result)
	{
		var effectiveUserName = unitOfWork.GetTypedRepository()
			.GetEffectiveUserName(parameter.UserName);
		if (effectiveUserName != null)
		{
			var userProfile = unitOfWork.GetRepository().Get(effectiveUserName.ParentId);
			if (userProfile != null)
			{
				parameter.UserName = userProfile.UserName;
			}
		}

		return NextHandler.Execute(unitOfWork, parameter, result);
	}

	/// 
	/// The AddSessionHandler chain of responsibility order, needs to run first.
	/// 
	public override int Order => 1;
}

Conclusion

The above code enables the ability to change a username, and log in with the changed username. However, it doesn't affect the actual username of the user. For example, the displayed username, when a user is logged in, will still be the original username.

Categories: Insite, Authentication
Tags: Insite;

SEARCH ARTICLES