I’m in the process of migrating my v8 site all the way up to v16. I’m currently migrating to v10 as an intermediate step which is pretty juicy as it’s the move from .net framework to .net core. I have most of it completed but I’m struggling with the custom membership provider I had created. Things have moved to using MemberIdentityUsers so I’m trying to create a custom UserStore to replace my old provider. I’m struggling to find any documentation on how to go about doing this. If anyone could point me towards anything useful I’d be really grateful. AI just sends me around in circles with assumptions and guesses so I’m really looking for something official to get me going.
I don’t have an answer for you, but what really helps me in cases like this, is to look at the Umbraco source code and see how they have done it. Or better, clone the repository, open it in your IDE and ask AI in your IDE questions within the context of the Umbraco source code.
Hi Luuk, Appreciate your response. I think that is going to be my only sensible option once I arrive at v16. In the meantime I can fudge it to test the functionality that requires login. It doesn’t look to me like anyone has done this yet. From what I’ve read it doesn’t look like that Umbraco team have yet looked at officially supporting extending it in that way yet. There seem to be a couple of implementations (virtual users) that have been implemented in the past but they won’t work on 13+
Although we should be careful when copying AI solutions here, I still think that if you ask AI (especially using more complex models) in the context of the Umbraco source code it usually comes up with decent answers.
So spoiler, this is AI generated, but to me it makes enough sense for me to post it here so you can try it!
If it does not work, let me know and I’ll remove it. It’s based on the Umbraco 16 source code. I doubt things have changed much, but still it’s possible.
The user is migrating from v8 to v10 and needs to create a custom UserStore to replace their old membership provider. Here’s how to create a custom member user store in Umbraco v10+:
Understanding the Architecture
In Umbraco v10+, the member identity system is built around:
- MemberIdentityUser - The identity user model for members
- IMemberUserStore - The interface for member user stores
- MemberUserStore - The default implementation
- UmbracoUserStore<TUser, TRole> - The base class to inherit from
Creating a Custom Member User Store
Here’s how to create your custom member user store:
- Create your custom user store class:
The key insight from the source code is how Umbraco registers the member identity services. You need to replace the default store registration:
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
namespace YourProject.Security
{
public class CustomMemberUserStore : UmbracoUserStore<MemberIdentityUser, UmbracoIdentityRole>, IMemberUserStore
{
private readonly IExternalLoginWithKeyService _externalLoginService;
private readonly IUmbracoMapper _mapper;
private readonly IMemberService _memberService;
private readonly ICoreScopeProvider _scopeProvider;
private readonly ITwoFactorLoginService _twoFactorLoginService;
private readonly IPublishedMemberCache _memberCache;
// Your custom services
private readonly IYourCustomService _yourCustomService;
[ActivatorUtilitiesConstructor]
public CustomMemberUserStore(
IMemberService memberService,
IUmbracoMapper mapper,
ICoreScopeProvider scopeProvider,
IdentityErrorDescriber describer,
IExternalLoginWithKeyService externalLoginService,
ITwoFactorLoginService twoFactorLoginService,
IPublishedMemberCache memberCache,
IYourCustomService yourCustomService) // Your custom dependencies
: base(describer)
{
_memberService = memberService ?? throw new ArgumentNullException(nameof(memberService));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
_scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider));
_externalLoginService = externalLoginService;
_twoFactorLoginService = twoFactorLoginService;
_memberCache = memberCache;
_yourCustomService = yourCustomService;
}
// Override methods as needed
public override async Task<IdentityResult> CreateAsync(
MemberIdentityUser user,
CancellationToken cancellationToken = default)
{
// Your custom creation logic
// You can call the base implementation and add custom logic:
// var result = await base.CreateAsync(user, cancellationToken);
// Or completely custom implementation:
try
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user is null)
{
throw new ArgumentNullException(nameof(user));
}
using ICoreScope scope = _scopeProvider.CreateCoreScope();
// Your custom member creation logic here
// Use _yourCustomService to integrate with external systems
scope.Complete();
return IdentityResult.Success;
}
catch (Exception ex)
{
return IdentityResult.Failed(new IdentityError
{
Code = "CustomError",
Description = ex.Message
});
}
}
public override async Task<MemberIdentityUser?> FindByNameAsync(
string userName,
CancellationToken cancellationToken = default)
{
// Your custom find logic - perhaps looking in an external system
// You can still use Umbraco's member service alongside your custom logic
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
// Check your external system first
var externalMember = await _yourCustomService.FindMemberAsync(userName);
if (externalMember != null)
{
// Convert to MemberIdentityUser or sync with Umbraco
}
// Fall back to default behavior
IMember? user = _memberService.GetByUsername(userName);
if (user == null)
{
return null;
}
MemberIdentityUser? result = _mapper.Map<MemberIdentityUser>(user);
return AssignLoginsCallback(result);
}
public IPublishedContent? GetPublishedMember(MemberIdentityUser? user)
{
if (user == null)
{
return null;
}
IMember? member = _memberService.GetById(user.Key);
if (member == null)
{
return null;
}
return _memberCache.Get(member);
}
// Implement other methods as needed...
private MemberIdentityUser? AssignLoginsCallback(MemberIdentityUser? user)
{
if (user is not null)
{
user.SetLoginsCallback(new Lazy<IEnumerable<IIdentityUserLogin>?>(() =>
_externalLoginService.GetExternalLogins(user.Key)));
user.SetTokensCallback(new Lazy<IEnumerable<IIdentityUserToken>?>(() =>
_externalLoginService.GetExternalLoginTokens(user.Key)));
}
return user;
}
}
}
- Register your custom user store:
// In your Startup.cs or Program.cs (depending on .NET version)
public void ConfigureServices(IServiceCollection services)
{
// First add Umbraco
services.AddUmbraco(_env, _config)
.AddBackOffice()
.AddWebsite()
.AddComposers()
.AddMembersIdentity(); // This adds the default member identity
// Then replace the member user store
services.AddScoped<IUserStore<MemberIdentityUser>>(factory =>
new CustomMemberUserStore(
factory.GetRequiredService<IMemberService>(),
factory.GetRequiredService<IUmbracoMapper>(),
factory.GetRequiredService<ICoreScopeProvider>(),
factory.GetRequiredService<IdentityErrorDescriber>(),
factory.GetRequiredService<IExternalLoginWithKeyService>(),
factory.GetRequiredService<ITwoFactorLoginService>(),
factory.GetRequiredService<IPublishedMemberCache>(),
factory.GetRequiredService<IYourCustomService>()));
// Also register as IMemberUserStore
services.AddScoped<IMemberUserStore>(x =>
(CustomMemberUserStore)x.GetRequiredService<IUserStore<MemberIdentityUser>>());
}
- Key Points from the Source Analysis:
- Base Class: Always inherit from UmbracoUserStore<MemberIdentityUser, UmbracoIdentityRole>
- Required Interface: Implement IMemberUserStore
- Scope Management: Use ICoreScopeProvider for database transactions
- Member Service: Use IMemberService to interact with Umbraco’s member system
- Identity Integration: The store works with ASP.NET Core Identity, so follow Identity patterns
- Error Handling: Use IdentityResult for success/failure responses
- Change Tracking: MemberIdentityUser has built-in dirty property tracking
- Common Scenarios:
- External Authentication: Override FindByNameAsync and FindByEmailAsync to check external systems
- Custom Properties: Use the UpdateMemberProperties pattern from the source to map custom fields
- External Storage: Override create/update methods to sync with external repositories
- Custom Validation: Add validation logic in create/update methods
- Testing:
Look at MemberUserStoreTests.cs in the source for examples of how to unit test your custom store.
This approach gives you full control over member authentication while still leveraging Umbraco’s member management system and ASP.NET Core Identity integration.
Thanks very much Lukk, I appreciate the responses, when I arrive at v16 I will be sure to give that a try. I’m curious which AI you used to get that. It’s probably worth noting I have been down this road and used many “solutions” AI has offered me, none of which worked. To be fair it ended up just sending me round in circles, guessing and making assumptions. Claude seemed to get the closest, Gemini was a very painful experience! I thought it best to park it until I arrived at v16 as I didn’t want to implement something just to have to re-implement again due to any potential changes between 10 and 16.
I used GitHub Copilot in Visual Studio with Claude Sonnet 4 in the context of the Umbraco 16 source code, which I have pulled to my local machine.
I’m guessing you don’t want to leverage the External Login Providers (OIDC)?
Hi Mike, isn’t that just for authentication via external login providers such as Google etc?
Couldn’t an external login provider be your memberstore too? passing your required data via claims and syching into the umbraco member store.. I guess it depends on the requirement?
Maybe the external login provider is more than I assumed it was. I have my own repository of users (in an sql database) and my users have additional properties. So I need to access my own authentication mechanism and also extend the user profiles and query the repository etc. My assumption was that the external login provider (such as Google etc.) is just an external provider to authenticate users with their own mechanisms (such as gmail accounts etc.). So it’ll authenticate users for me but that’s pretty much it? Maybe I’m missing something.
No your assumption is correct, it’s for authentication primarily…
However, when you authenticate against the external provider you can have it pass additional claims in the response, that you can then use to sync into the umbraco member store.
Something like
where it’s synching roles and displaynames.. But I guess it comes down to how much extra you’d be passing and the type of data…
you can also do this at anytime the JWT token is validated to..
options.Events.OnTokenValidated = async context =>
{
var claims = context?.Principal?.Claims.ToList();
......
}
But it could be an alternative, setting up an OIDC provider against your SQL DB?
I’m guessing that your sql database is used as your SSO/MemberStore elsewhere too, so you wouldn’t want to just import into the Umbraco Member store? ![]()
Thank you for that Mike, it certainly sounds like it warrants further investigation, I really appreciate that information