Security stamp implementation for members (#10140)

* Getting new netcore PublicAccessChecker in place

* Adds full test coverage for PublicAccessChecker

* remove PublicAccessComposer

* adjust namespaces, ensure RoleManager works, separate public access controller, reduce content controller

* Implements the required methods on IMemberManager, removes old migrated code

* Updates routing to be able to re-route, Fixes middleware ordering ensuring endpoints are last, refactors pipeline options, adds public access middleware, ensures public access follows all hops

* adds note

* adds note

* Cleans up ext methods, ensures that members identity is added on both front-end and back ends. updates how UmbracoApplicationBuilder works in that it explicitly starts endpoints at the time of calling.

* Changes name to IUmbracoEndpointBuilder

* adds note

* Fixing tests, fixing error describers so there's 2x one for back office, one for members, fixes TryConvertTo, fixes login redirect

* fixing build

* Updates user manager to correctly validate password hashing and injects the IBackOfficeUserPasswordChecker

* Merges PR

* Fixes up build and notes

* Implements security stamp and email confirmed for members, cleans up a bunch of repo/service level member groups stuff, shares user store code between members and users and fixes the user identity object so we arent' tracking both groups and roles.

* Security stamp for members is now working

* Fixes keepalive, fixes PublicAccessMiddleware to not throw, updates startup code to be more clear and removes magic that registers middleware.

* adds note

* removes unused filter, fixes build

* fixes WebPath and tests

* Looks up entities in one query

* remove usings

* Fix test, remove stylesheet

* Set status code before we write to response to avoid error

* Ensures that users and members are validated when logging in. Shares more code between users and members.

* merge changes

* oops

* Fixes RepositoryCacheKeys to ensure the keys are normalized

* oops didn't mean to commit this

* Fix casing issues with caching, stop boxing value types for all cache operations, stop re-creating string keys in DefaultRepositoryCachePolicy

* oops didn't mean to comit this

* bah, far out this keeps getting recommitted. sorry

Co-authored-by: Bjarke Berg <mail@bergmania.dk>
This commit is contained in:
Shannon Deminick
2021-04-20 17:13:40 +10:00
committed by GitHub
parent de28fbb0a4
commit 6c660d5721
26 changed files with 670 additions and 709 deletions

View File

@@ -19,11 +19,6 @@ namespace Umbraco.Cms.Core.Security
private int[] _startMediaIds;
private int[] _startContentIds;
// Custom comparer for enumerables
private static readonly DelegateEqualityComparer<IReadOnlyCollection<IReadOnlyUserGroup>> s_groupsComparer = new DelegateEqualityComparer<IReadOnlyCollection<IReadOnlyUserGroup>>(
(groups, enumerable) => groups.Select(x => x.Alias).UnsortedSequenceEqual(enumerable.Select(x => x.Alias)),
groups => groups.GetHashCode());
private static readonly DelegateEqualityComparer<int[]> s_startIdsComparer = new DelegateEqualityComparer<int[]>(
(groups, enumerable) => groups.UnsortedSequenceEqual(enumerable),
groups => groups.GetHashCode());
@@ -64,8 +59,7 @@ namespace Umbraco.Cms.Core.Security
_allowedSections = Array.Empty<string>();
_culture = globalSettings.DefaultUILanguage;
// use the property setters - they do more than just setting a field
Groups = groups;
SetGroups(groups);
}
/// <summary>
@@ -118,7 +112,7 @@ namespace Umbraco.Cms.Core.Security
/// <summary>
/// Gets a readonly list of the user's allowed sections which are based on it's user groups
/// </summary>
public string[] AllowedSections => _allowedSections ?? (_allowedSections = _groups.SelectMany(x => x.AllowedSections).Distinct().ToArray());
public string[] AllowedSections => _allowedSections ??= _groups.SelectMany(x => x.AllowedSections).Distinct().ToArray();
/// <summary>
/// Gets or sets the culture
@@ -132,31 +126,25 @@ namespace Umbraco.Cms.Core.Security
/// <summary>
/// Gets or sets the user groups
/// </summary>
public IReadOnlyCollection<IReadOnlyUserGroup> Groups
public void SetGroups(IReadOnlyCollection<IReadOnlyUserGroup> value)
{
get => _groups;
set
// so they recalculate
_allowedSections = null;
_groups = value.Where(x => x.Alias != null).ToArray();
var roles = new List<IdentityUserRole<string>>();
foreach (IdentityUserRole<string> identityUserRole in _groups.Select(x => new IdentityUserRole<string>
{
// so they recalculate
_allowedSections = null;
_groups = value.Where(x => x.Alias != null).ToArray();
var roles = new List<IdentityUserRole<string>>();
foreach (IdentityUserRole<string> identityUserRole in _groups.Select(x => new IdentityUserRole<string>
{
RoleId = x.Alias,
UserId = Id?.ToString()
}))
{
roles.Add(identityUserRole);
}
// now reset the collection
Roles = roles;
BeingDirty.SetPropertyValueAndDetectChanges(value, ref _groups, nameof(Groups), s_groupsComparer);
RoleId = x.Alias,
UserId = Id?.ToString()
}))
{
roles.Add(identityUserRole);
}
// now reset the collection
Roles = roles;
}
private static string UserIdToString(int userId) => string.Intern(userId.ToString());

View File

@@ -19,12 +19,11 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Security
{
// TODO: Make this into a base class that can be re-used
/// <summary>
/// The user store for back office users
/// </summary>
public class BackOfficeUserStore : UserStoreBase<BackOfficeIdentityUser, IdentityRole<string>, string, IdentityUserClaim<string>, IdentityUserRole<string>, IdentityUserLogin<string>, IdentityUserToken<string>, IdentityRoleClaim<string>>
public class BackOfficeUserStore : UmbracoUserStore<BackOfficeIdentityUser, IdentityRole<string>>
{
private readonly IScopeProvider _scopeProvider;
private readonly IUserService _userService;
@@ -59,19 +58,6 @@ namespace Umbraco.Cms.Core.Security
_externalLoginService = externalLoginService;
}
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override IQueryable<BackOfficeIdentityUser> Users => throw new NotImplementedException();
/// <inheritdoc />
public override Task<string> GetNormalizedUserNameAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) => GetUserNameAsync(user, cancellationToken);
/// <inheritdoc />
public override Task SetNormalizedUserNameAsync(BackOfficeIdentityUser user, string normalizedName, CancellationToken cancellationToken) => SetUserNameAsync(user, normalizedName, cancellationToken);
/// <inheritdoc />
public override Task<IdentityResult> CreateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default)
{
@@ -215,9 +201,6 @@ namespace Umbraco.Cms.Core.Security
return Task.FromResult(IdentityResult.Success);
}
/// <inheritdoc />
public override Task<BackOfficeIdentityUser> FindByIdAsync(string userId, CancellationToken cancellationToken = default) => FindUserAsync(userId, cancellationToken);
/// <inheritdoc />
protected override Task<BackOfficeIdentityUser> FindUserAsync(string userId, CancellationToken cancellationToken)
{
@@ -249,29 +232,6 @@ namespace Umbraco.Cms.Core.Security
return Task.FromResult(result);
}
/// <inheritdoc />
public override async Task SetPasswordHashAsync(BackOfficeIdentityUser user, string passwordHash, CancellationToken cancellationToken = default)
{
await base.SetPasswordHashAsync(user, passwordHash, cancellationToken);
user.PasswordConfig = null; // Clear this so that it's reset at the repository level
user.LastPasswordChangeDateUtc = DateTime.UtcNow;
}
/// <inheritdoc />
public override async Task<bool> HasPasswordAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default)
{
// This checks if it's null
var result = await base.HasPasswordAsync(user, cancellationToken);
if (result)
{
// we also want to check empty
return string.IsNullOrEmpty(user.PasswordHash) == false;
}
return result;
}
/// <inheritdoc />
public override Task<BackOfficeIdentityUser> FindByEmailAsync(string email, CancellationToken cancellationToken = default)
{
@@ -286,12 +246,13 @@ namespace Umbraco.Cms.Core.Security
}
/// <inheritdoc />
public override Task<string> GetNormalizedEmailAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken)
=> GetEmailAsync(user, cancellationToken);
public override async Task SetPasswordHashAsync(BackOfficeIdentityUser user, string passwordHash, CancellationToken cancellationToken = default)
{
await base.SetPasswordHashAsync(user, passwordHash, cancellationToken);
/// <inheritdoc />
public override Task SetNormalizedEmailAsync(BackOfficeIdentityUser user, string normalizedEmail, CancellationToken cancellationToken)
=> SetEmailAsync(user, normalizedEmail, cancellationToken);
// Clear this so that it's reset at the repository level
user.PasswordConfig = null;
}
/// <inheritdoc />
public override Task AddLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login, CancellationToken cancellationToken = default)
@@ -398,100 +359,6 @@ namespace Umbraco.Cms.Core.Security
});
}
/// <summary>
/// Adds a user to a role (user group)
/// </summary>
public override Task AddToRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (normalizedRoleName == null)
{
throw new ArgumentNullException(nameof(normalizedRoleName));
}
if (string.IsNullOrWhiteSpace(normalizedRoleName))
{
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(normalizedRoleName));
}
IdentityUserRole<string> userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName);
if (userRole == null)
{
user.AddRole(normalizedRoleName);
}
return Task.CompletedTask;
}
/// <summary>
/// Removes the role (user group) for the user
/// </summary>
public override Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (normalizedRoleName == null)
{
throw new ArgumentNullException(nameof(normalizedRoleName));
}
if (string.IsNullOrWhiteSpace(normalizedRoleName))
{
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(normalizedRoleName));
}
IdentityUserRole<string> userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName);
if (userRole != null)
{
user.Roles.Remove(userRole);
}
return Task.CompletedTask;
}
/// <summary>
/// Gets a list of role names the specified user belongs to.
/// </summary>
public override Task<IList<string>> GetRolesAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
return Task.FromResult((IList<string>)user.Roles.Select(x => x.RoleId).ToList());
}
/// <summary>
/// Returns true if a user is in the role
/// </summary>
public override Task<bool> IsInRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
return Task.FromResult(user.Roles.Select(x => x.RoleId).InvariantContains(normalizedRoleName));
}
/// <summary>
/// Lists all users of a given role.
/// </summary>
@@ -543,22 +410,6 @@ namespace Umbraco.Cms.Core.Security
return found;
}
/// <inheritdoc />
public override Task<string> GetSecurityStampAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
// the stamp cannot be null, so if it is currently null then we'll just return a hash of the password
return Task.FromResult(user.SecurityStamp.IsNullOrWhiteSpace()
? user.PasswordHash.GenerateHash()
: user.SecurityStamp);
}
private BackOfficeIdentityUser AssignLoginsCallback(BackOfficeIdentityUser user)
{
if (user != null)
@@ -678,36 +529,26 @@ namespace Umbraco.Cms.Core.Security
user.SecurityStamp = identityUser.SecurityStamp;
}
// TODO: Fix this for Groups too
if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Roles)) || identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Groups)))
if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Roles)))
{
var userGroupAliases = user.Groups.Select(x => x.Alias).ToArray();
var identityUserRoles = identityUser.Roles.Select(x => x.RoleId).ToArray();
var identityUserGroups = identityUser.Groups.Select(x => x.Alias).ToArray();
var combinedAliases = identityUserRoles.Union(identityUserGroups).ToArray();
anythingChanged = true;
if (userGroupAliases.ContainsAll(combinedAliases) == false
|| combinedAliases.ContainsAll(userGroupAliases) == false)
// clear out the current groups (need to ToArray since we are modifying the iterator)
user.ClearGroups();
// go lookup all these groups
IReadOnlyUserGroup[] groups = _userService.GetUserGroupsByAlias(identityUserRoles).Select(x => x.ToReadOnlyGroup()).ToArray();
// use all of the ones assigned and add them
foreach (IReadOnlyUserGroup group in groups)
{
anythingChanged = true;
// clear out the current groups (need to ToArray since we are modifying the iterator)
user.ClearGroups();
// go lookup all these groups
var groups = _userService.GetUserGroupsByAlias(combinedAliases).Select(x => x.ToReadOnlyGroup()).ToArray();
// use all of the ones assigned and add them
foreach (var group in groups)
{
user.AddGroup(group);
}
// re-assign
identityUser.Groups = groups;
user.AddGroup(group);
}
// re-assign
identityUser.SetGroups(groups);
}
// we should re-set the calculated start nodes
@@ -731,54 +572,6 @@ namespace Umbraco.Cms.Core.Security
return Task.FromResult(false);
}
private static int UserIdToInt(string userId)
{
Attempt<int> attempt = userId.TryConvertTo<int>();
if (attempt.Success)
{
return attempt.Result;
}
throw new InvalidOperationException("Unable to convert user ID to int", attempt.Exception);
}
private static string UserIdToString(int userId) => string.Intern(userId.ToString());
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override Task<IList<Claim>> GetClaimsAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override Task AddClaimsAsync(BackOfficeIdentityUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken = default) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override Task ReplaceClaimAsync(BackOfficeIdentityUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override Task RemoveClaimsAsync(BackOfficeIdentityUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken = default) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override Task<IList<BackOfficeIdentityUser>> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default) => throw new NotImplementedException();
/// <summary>
/// Overridden to support Umbraco's own data storage requirements
@@ -859,25 +652,5 @@ namespace Umbraco.Cms.Core.Security
return Task.FromResult(token?.Value);
}
/// <summary>
/// Not supported in Umbraco, see comments above on GetTokenAsync, RemoveTokenAsync, SetTokenAsync
/// </summary>
/// <inheritdoc />
protected override Task<IdentityUserToken<string>> FindTokenAsync(BackOfficeIdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco, see comments above on GetTokenAsync, RemoveTokenAsync, SetTokenAsync
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
protected override Task AddUserTokenAsync(IdentityUserToken<string> token) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco, see comments above on GetTokenAsync, RemoveTokenAsync, SetTokenAsync
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
protected override Task RemoveUserTokenAsync(IdentityUserToken<string> token) => throw new NotImplementedException();
}
}

View File

@@ -22,7 +22,7 @@ namespace Umbraco.Extensions
}
}
public static void MergeClaimsFromBackOfficeIdentity(this ClaimsIdentity destination, ClaimsIdentity source)
public static void MergeClaimsFromCookieIdentity(this ClaimsIdentity destination, ClaimsIdentity source)
{
foreach (Claim claim in source.Claims
.Where(claim => !s_ignoredClaims.Contains(claim.Type))

View File

@@ -100,6 +100,7 @@ namespace Umbraco.Cms.Core.Security
//target.Roles =;
}
// TODO: We need to validate this mapping is OK, we need to get Umbraco.Code working
private void Map(IMember source, MemberIdentityUser target)
{
target.Email = source.Email;

View File

@@ -12,7 +12,7 @@ namespace Umbraco.Cms.Core.Security
/// </summary>
public class MemberIdentityUser : UmbracoIdentityUser
{
private string _comments;
private string _comments;
private IReadOnlyCollection<IReadOnlyUserGroup> _groups;
// Custom comparer for enumerables

View File

@@ -18,7 +18,7 @@ namespace Umbraco.Cms.Core.Security
/// <summary>
/// A custom user store that uses Umbraco member data
/// </summary>
public class MemberUserStore : UserStoreBase<MemberIdentityUser, UmbracoIdentityRole, string, IdentityUserClaim<string>, IdentityUserRole<string>, IdentityUserLogin<string>, IdentityUserToken<string>, IdentityRoleClaim<string>>
public class MemberUserStore : UmbracoUserStore<MemberIdentityUser, UmbracoIdentityRole>
{
private const string genericIdentityErrorCode = "IdentityErrorUserStore";
private readonly IMemberService _memberService;
@@ -32,7 +32,11 @@ namespace Umbraco.Cms.Core.Security
/// <param name="mapper">The mapper for properties</param>
/// <param name="scopeProvider">The scope provider</param>
/// <param name="describer">The error describer</param>
public MemberUserStore(IMemberService memberService, UmbracoMapper mapper, IScopeProvider scopeProvider, IdentityErrorDescriber describer)
public MemberUserStore(
IMemberService memberService,
UmbracoMapper mapper,
IScopeProvider scopeProvider,
IdentityErrorDescriber describer)
: base(describer)
{
_memberService = memberService ?? throw new ArgumentNullException(nameof(memberService));
@@ -40,22 +44,6 @@ namespace Umbraco.Cms.Core.Security
_scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider));
}
//TODO: why is this not supported?
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override IQueryable<MemberIdentityUser> Users => throw new NotImplementedException();
/// <inheritdoc />
public override Task<string> GetNormalizedUserNameAsync(MemberIdentityUser user, CancellationToken cancellationToken = default)
=> GetUserNameAsync(user, cancellationToken);
/// <inheritdoc />
public override Task SetNormalizedUserNameAsync(MemberIdentityUser user, string normalizedName, CancellationToken cancellationToken = default)
=> SetUserNameAsync(user, normalizedName, cancellationToken);
/// <inheritdoc />
public override Task<IdentityResult> CreateAsync(MemberIdentityUser user, CancellationToken cancellationToken = default)
{
@@ -68,6 +56,8 @@ namespace Umbraco.Cms.Core.Security
throw new ArgumentNullException(nameof(user));
}
using IScope scope = _scopeProvider.CreateScope(autoComplete: true);
// create member
IMember memberEntity = _memberService.CreateMember(
user.UserName,
@@ -130,36 +120,33 @@ namespace Umbraco.Cms.Core.Security
throw new InvalidOperationException("The user id must be an integer to work with Umbraco");
}
using (IScope scope = _scopeProvider.CreateScope())
using IScope scope = _scopeProvider.CreateScope(autoComplete: true);
IMember found = _memberService.GetById(asInt.Result);
if (found != null)
{
IMember found = _memberService.GetById(asInt.Result);
if (found != null)
// we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(MemberIdentityUser.Logins));
if (UpdateMemberProperties(found, user))
{
// we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(MemberIdentityUser.Logins));
if (UpdateMemberProperties(found, user))
{
_memberService.Save(found);
}
// TODO: when to implement external login service?
//if (isLoginsPropertyDirty)
//{
// _externalLoginService.Save(
// found.Id,
// user.Logins.Select(x => new ExternalLogin(
// x.LoginProvider,
// x.ProviderKey,
// x.UserData)));
//}
_memberService.Save(found);
}
scope.Complete();
// TODO: when to implement external login service?
return Task.FromResult(IdentityResult.Success);
//if (isLoginsPropertyDirty)
//{
// _externalLoginService.Save(
// found.Id,
// user.Logins.Select(x => new ExternalLogin(
// x.LoginProvider,
// x.ProviderKey,
// x.UserData)));
//}
}
return Task.FromResult(IdentityResult.Success);
}
catch (Exception ex)
{
@@ -196,9 +183,6 @@ namespace Umbraco.Cms.Core.Security
}
}
/// <inheritdoc />
public override Task<MemberIdentityUser> FindByIdAsync(string userId, CancellationToken cancellationToken = default) => FindUserAsync(userId, cancellationToken);
/// <inheritdoc />
protected override Task<MemberIdentityUser> FindUserAsync(string userId, CancellationToken cancellationToken)
{
@@ -235,30 +219,6 @@ namespace Umbraco.Cms.Core.Security
return Task.FromResult(result);
}
/// <inheritdoc />
public override async Task SetPasswordHashAsync(MemberIdentityUser user, string passwordHash, CancellationToken cancellationToken = default)
{
await base.SetPasswordHashAsync(user, passwordHash, cancellationToken);
// Clear this so that it's reset at the repository level
user.PasswordConfig = null;
user.LastPasswordChangeDateUtc = DateTime.UtcNow;
}
/// <inheritdoc />
public override async Task<bool> HasPasswordAsync(MemberIdentityUser user, CancellationToken cancellationToken = default)
{
// This checks if it's null
bool result = await base.HasPasswordAsync(user, cancellationToken);
if (result)
{
// we also want to check empty
return string.IsNullOrEmpty(user.PasswordHash) == false;
}
return false;
}
/// <inheritdoc />
public override Task<MemberIdentityUser> FindByEmailAsync(string email, CancellationToken cancellationToken = default)
{
@@ -272,14 +232,6 @@ namespace Umbraco.Cms.Core.Security
return Task.FromResult(AssignLoginsCallback(result));
}
/// <inheritdoc />
public override Task<string> GetNormalizedEmailAsync(MemberIdentityUser user, CancellationToken cancellationToken)
=> GetEmailAsync(user, cancellationToken);
/// <inheritdoc />
public override Task SetNormalizedEmailAsync(MemberIdentityUser user, string normalizedEmail, CancellationToken cancellationToken)
=> SetEmailAsync(user, normalizedEmail, cancellationToken);
/// <inheritdoc />
public override Task AddLoginAsync(MemberIdentityUser user, UserLoginInfo login, CancellationToken cancellationToken = default)
{
@@ -434,93 +386,33 @@ namespace Umbraco.Cms.Core.Security
});
}
/// <inheritdoc />
public override Task AddToRoleAsync(MemberIdentityUser user, string role, CancellationToken cancellationToken = default)
{
if (cancellationToken != null)
{
cancellationToken.ThrowIfCancellationRequested();
}
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (role == null)
{
throw new ArgumentNullException(nameof(role));
}
if (string.IsNullOrWhiteSpace(role))
{
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(role));
}
IdentityUserRole<string> userRole = user.Roles.SingleOrDefault(r => r.RoleId == role);
if (userRole == null)
{
_memberService.AssignRole(user.UserName, role);
user.AddRole(role);
}
return Task.CompletedTask;
}
/// <inheritdoc/>
public override Task RemoveFromRoleAsync(MemberIdentityUser user, string role, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (role == null)
{
throw new ArgumentNullException(nameof(role));
}
if (string.IsNullOrWhiteSpace(role))
{
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(role));
}
IdentityUserRole<string> userRole = user.Roles.SingleOrDefault(r => r.RoleId == role);
if (userRole != null)
{
_memberService.DissociateRole(user.UserName, userRole.RoleId);
user.Roles.Remove(userRole);
}
return Task.CompletedTask;
}
/// <summary>
/// Gets a list of role names the specified user belongs to.
/// </summary>
/// <remarks>
/// This lazy loads the roles for the member
/// </remarks>
public override Task<IList<string>> GetRolesAsync(MemberIdentityUser user, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
EnsureRoles(user);
return base.GetRolesAsync(user, cancellationToken);
}
private void EnsureRoles(MemberIdentityUser user)
{
if (user.Roles.Count == 0)
{
throw new ArgumentNullException(nameof(user));
// if there are no roles, they either haven't been loaded since we don't eagerly
// load for members, or they just have no roles.
IEnumerable<string> currentRoles = _memberService.GetAllRoles(user.UserName);
ICollection<IdentityUserRole<string>> roles = currentRoles.Select(role => new IdentityUserRole<string>
{
RoleId = role,
UserId = user.Id
}).ToList();
user.Roles = roles;
}
IEnumerable<string> currentRoles = _memberService.GetAllRoles(user.UserName);
ICollection<IdentityUserRole<string>> roles = currentRoles.Select(role => new IdentityUserRole<string>
{
RoleId = role,
UserId = user.Id
}).ToList();
user.Roles = roles;
return Task.FromResult((IList<string>)user.Roles.Select(x => x.RoleId).ToList());
}
/// <summary>
@@ -528,19 +420,9 @@ namespace Umbraco.Cms.Core.Security
/// </summary>
public override Task<bool> IsInRoleAsync(MemberIdentityUser user, string roleName, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
EnsureRoles(user);
if (string.IsNullOrWhiteSpace(roleName))
{
throw new ArgumentNullException(nameof(roleName));
}
return Task.FromResult(user.Roles.Select(x => x.RoleId).InvariantContains(roleName));
return base.IsInRoleAsync(user, roleName, cancellationToken);
}
/// <summary>
@@ -597,22 +479,6 @@ namespace Umbraco.Cms.Core.Security
return found;
}
/// <inheritdoc />
public override Task<string> GetSecurityStampAsync(MemberIdentityUser user, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
// the stamp cannot be null, so if it is currently null then we'll just return a hash of the password
return Task.FromResult(user.SecurityStamp.IsNullOrWhiteSpace()
? user.PasswordHash.GenerateHash()
: user.SecurityStamp);
}
private MemberIdentityUser AssignLoginsCallback(MemberIdentityUser user)
{
if (user != null)
@@ -624,70 +490,70 @@ namespace Umbraco.Cms.Core.Security
return user;
}
private bool UpdateMemberProperties(IMember member, MemberIdentityUser identityUserMember)
private bool UpdateMemberProperties(IMember member, MemberIdentityUser identityUser)
{
var anythingChanged = false;
// don't assign anything if nothing has changed as this will trigger the track changes of the model
if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.LastLoginDateUtc))
|| (member.LastLoginDate != default && identityUserMember.LastLoginDateUtc.HasValue == false)
|| (identityUserMember.LastLoginDateUtc.HasValue && member.LastLoginDate.ToUniversalTime() != identityUserMember.LastLoginDateUtc.Value))
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.LastLoginDateUtc))
|| (member.LastLoginDate != default && identityUser.LastLoginDateUtc.HasValue == false)
|| (identityUser.LastLoginDateUtc.HasValue && member.LastLoginDate.ToUniversalTime() != identityUser.LastLoginDateUtc.Value))
{
anythingChanged = true;
// if the LastLoginDate is being set to MinValue, don't convert it ToLocalTime
DateTime dt = identityUserMember.LastLoginDateUtc == DateTime.MinValue ? DateTime.MinValue : identityUserMember.LastLoginDateUtc.Value.ToLocalTime();
DateTime dt = identityUser.LastLoginDateUtc == DateTime.MinValue ? DateTime.MinValue : identityUser.LastLoginDateUtc.Value.ToLocalTime();
member.LastLoginDate = dt;
}
if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.LastPasswordChangeDateUtc))
|| (member.LastPasswordChangeDate != default && identityUserMember.LastPasswordChangeDateUtc.HasValue == false)
|| (identityUserMember.LastPasswordChangeDateUtc.HasValue && member.LastPasswordChangeDate.ToUniversalTime() != identityUserMember.LastPasswordChangeDateUtc.Value))
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.LastPasswordChangeDateUtc))
|| (member.LastPasswordChangeDate != default && identityUser.LastPasswordChangeDateUtc.HasValue == false)
|| (identityUser.LastPasswordChangeDateUtc.HasValue && member.LastPasswordChangeDate.ToUniversalTime() != identityUser.LastPasswordChangeDateUtc.Value))
{
anythingChanged = true;
member.LastPasswordChangeDate = identityUserMember.LastPasswordChangeDateUtc.Value.ToLocalTime();
member.LastPasswordChangeDate = identityUser.LastPasswordChangeDateUtc.Value.ToLocalTime();
}
if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.Comments))
&& member.Comments != identityUserMember.Comments && identityUserMember.Comments.IsNullOrWhiteSpace() == false)
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.Comments))
&& member.Comments != identityUser.Comments && identityUser.Comments.IsNullOrWhiteSpace() == false)
{
anythingChanged = true;
member.Comments = identityUserMember.Comments;
member.Comments = identityUser.Comments;
}
if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.EmailConfirmed))
|| (member.EmailConfirmedDate.HasValue && member.EmailConfirmedDate.Value != default && identityUserMember.EmailConfirmed == false)
|| ((member.EmailConfirmedDate.HasValue == false || member.EmailConfirmedDate.Value == default) && identityUserMember.EmailConfirmed))
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.EmailConfirmed))
|| (member.EmailConfirmedDate.HasValue && member.EmailConfirmedDate.Value != default && identityUser.EmailConfirmed == false)
|| ((member.EmailConfirmedDate.HasValue == false || member.EmailConfirmedDate.Value == default) && identityUser.EmailConfirmed))
{
anythingChanged = true;
member.EmailConfirmedDate = identityUserMember.EmailConfirmed ? (DateTime?)DateTime.Now : null;
member.EmailConfirmedDate = identityUser.EmailConfirmed ? (DateTime?)DateTime.Now : null;
}
if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.Name))
&& member.Name != identityUserMember.Name && identityUserMember.Name.IsNullOrWhiteSpace() == false)
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.Name))
&& member.Name != identityUser.Name && identityUser.Name.IsNullOrWhiteSpace() == false)
{
anythingChanged = true;
member.Name = identityUserMember.Name;
member.Name = identityUser.Name;
}
if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.Email))
&& member.Email != identityUserMember.Email && identityUserMember.Email.IsNullOrWhiteSpace() == false)
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.Email))
&& member.Email != identityUser.Email && identityUser.Email.IsNullOrWhiteSpace() == false)
{
anythingChanged = true;
member.Email = identityUserMember.Email;
member.Email = identityUser.Email;
}
if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.AccessFailedCount))
&& member.FailedPasswordAttempts != identityUserMember.AccessFailedCount)
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.AccessFailedCount))
&& member.FailedPasswordAttempts != identityUser.AccessFailedCount)
{
anythingChanged = true;
member.FailedPasswordAttempts = identityUserMember.AccessFailedCount;
member.FailedPasswordAttempts = identityUser.AccessFailedCount;
}
if (member.IsLockedOut != identityUserMember.IsLockedOut)
if (member.IsLockedOut != identityUser.IsLockedOut)
{
anythingChanged = true;
member.IsLockedOut = identityUserMember.IsLockedOut;
member.IsLockedOut = identityUser.IsLockedOut;
if (member.IsLockedOut)
{
@@ -696,112 +562,46 @@ namespace Umbraco.Cms.Core.Security
}
}
if (member.IsApproved != identityUserMember.IsApproved)
if (member.IsApproved != identityUser.IsApproved)
{
anythingChanged = true;
member.IsApproved = identityUserMember.IsApproved;
member.IsApproved = identityUser.IsApproved;
}
if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.UserName))
&& member.Username != identityUserMember.UserName && identityUserMember.UserName.IsNullOrWhiteSpace() == false)
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.UserName))
&& member.Username != identityUser.UserName && identityUser.UserName.IsNullOrWhiteSpace() == false)
{
anythingChanged = true;
member.Username = identityUserMember.UserName;
member.Username = identityUser.UserName;
}
if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.PasswordHash))
&& member.RawPasswordValue != identityUserMember.PasswordHash && identityUserMember.PasswordHash.IsNullOrWhiteSpace() == false)
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.PasswordHash))
&& member.RawPasswordValue != identityUser.PasswordHash && identityUser.PasswordHash.IsNullOrWhiteSpace() == false)
{
anythingChanged = true;
member.RawPasswordValue = identityUserMember.PasswordHash;
member.PasswordConfiguration = identityUserMember.PasswordConfig;
member.RawPasswordValue = identityUser.PasswordHash;
member.PasswordConfiguration = identityUser.PasswordConfig;
}
if (member.SecurityStamp != identityUserMember.SecurityStamp)
if (member.SecurityStamp != identityUser.SecurityStamp)
{
anythingChanged = true;
member.SecurityStamp = identityUserMember.SecurityStamp;
member.SecurityStamp = identityUser.SecurityStamp;
}
// TODO: Fix this for Groups too (as per backoffice comment)
if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.Roles)) || identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.Groups)))
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.Roles)))
{
anythingChanged = true;
var identityUserRoles = identityUser.Roles.Select(x => x.RoleId).ToArray();
_memberService.ReplaceRoles(new[] { member.Id }, identityUserRoles);
}
// reset all changes
identityUserMember.ResetDirtyProperties(false);
identityUser.ResetDirtyProperties(false);
return anythingChanged;
}
private static int UserIdToInt(string userId)
{
Attempt<int> attempt = userId.TryConvertTo<int>();
if (attempt.Success)
{
return attempt.Result;
}
throw new InvalidOperationException("Unable to convert user ID to int", attempt.Exception);
}
private static string UserIdToString(int userId) => string.Intern(userId.ToString());
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override Task<IList<Claim>> GetClaimsAsync(MemberIdentityUser user, CancellationToken cancellationToken = default) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override Task AddClaimsAsync(MemberIdentityUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken = default) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override Task ReplaceClaimAsync(MemberIdentityUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override Task RemoveClaimsAsync(MemberIdentityUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken = default) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override Task<IList<MemberIdentityUser>> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
protected override Task<IdentityUserToken<string>> FindTokenAsync(MemberIdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
protected override Task AddUserTokenAsync(IdentityUserToken<string> token) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
protected override Task RemoveUserTokenAsync(IdentityUserToken<string> token) => throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,247 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Security
{
public abstract class UmbracoUserStore<TUser, TRole> : UserStoreBase<TUser, TRole, string, IdentityUserClaim<string>, IdentityUserRole<string>, IdentityUserLogin<string>, IdentityUserToken<string>, IdentityRoleClaim<string>>
where TUser : UmbracoIdentityUser
where TRole : IdentityRole<string>
{
protected UmbracoUserStore(IdentityErrorDescriber describer) : base(describer)
{
}
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override IQueryable<TUser> Users => throw new NotImplementedException();
protected static int UserIdToInt(string userId)
{
Attempt<int> attempt = userId.TryConvertTo<int>();
if (attempt.Success)
{
return attempt.Result;
}
throw new InvalidOperationException("Unable to convert user ID to int", attempt.Exception);
}
protected static string UserIdToString(int userId) => string.Intern(userId.ToString());
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override Task AddClaimsAsync(TUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken = default) => throw new NotImplementedException();
/// <summary>
/// Adds a user to a role (user group)
/// </summary>
public override Task AddToRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (normalizedRoleName == null)
{
throw new ArgumentNullException(nameof(normalizedRoleName));
}
if (string.IsNullOrWhiteSpace(normalizedRoleName))
{
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(normalizedRoleName));
}
IdentityUserRole<string> userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName);
if (userRole == null)
{
user.AddRole(normalizedRoleName);
}
return Task.CompletedTask;
}
/// <inheritdoc />
public override Task<TUser> FindByIdAsync(string userId, CancellationToken cancellationToken = default) => FindUserAsync(userId, cancellationToken);
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override Task<IList<Claim>> GetClaimsAsync(TUser user, CancellationToken cancellationToken = default) => throw new NotImplementedException();
/// <inheritdoc />
public override Task<string> GetNormalizedEmailAsync(TUser user, CancellationToken cancellationToken)
=> GetEmailAsync(user, cancellationToken);
/// <inheritdoc />
public override Task<string> GetNormalizedUserNameAsync(TUser user, CancellationToken cancellationToken = default)
=> GetUserNameAsync(user, cancellationToken);
/// <summary>
/// Gets a list of role names the specified user belongs to.
/// </summary>
public override Task<IList<string>> GetRolesAsync(TUser user, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
return Task.FromResult((IList<string>)user.Roles.Select(x => x.RoleId).ToList());
}
/// <inheritdoc />
public override Task<string> GetSecurityStampAsync(TUser user, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
// the stamp cannot be null, so if it is currently null then we'll just return a hash of the password
return Task.FromResult(user.SecurityStamp.IsNullOrWhiteSpace()
? user.PasswordHash.GenerateHash()
: user.SecurityStamp);
}
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override Task<IList<TUser>> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default) => throw new NotImplementedException();
/// <inheritdoc />
public override async Task<bool> HasPasswordAsync(TUser user, CancellationToken cancellationToken = default)
{
// This checks if it's null
bool result = await base.HasPasswordAsync(user, cancellationToken);
if (result)
{
// we also want to check empty
return string.IsNullOrEmpty(user.PasswordHash) == false;
}
return false;
}
/// <summary>
/// Returns true if a user is in the role
/// </summary>
public override Task<bool> IsInRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
return Task.FromResult(user.Roles.Select(x => x.RoleId).InvariantContains(normalizedRoleName));
}
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override Task RemoveClaimsAsync(TUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken = default) => throw new NotImplementedException();
/// <summary>
/// Removes the role (user group) for the user
/// </summary>
public override Task RemoveFromRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (normalizedRoleName == null)
{
throw new ArgumentNullException(nameof(normalizedRoleName));
}
if (string.IsNullOrWhiteSpace(normalizedRoleName))
{
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(normalizedRoleName));
}
IdentityUserRole<string> userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName);
if (userRole != null)
{
user.Roles.Remove(userRole);
}
return Task.CompletedTask;
}
/// <summary>
/// Not supported in Umbraco
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default) => throw new NotImplementedException();
/// <inheritdoc />
public override Task SetNormalizedEmailAsync(TUser user, string normalizedEmail, CancellationToken cancellationToken)
=> SetEmailAsync(user, normalizedEmail, cancellationToken);
/// <inheritdoc />
public override Task SetNormalizedUserNameAsync(TUser user, string normalizedName, CancellationToken cancellationToken = default)
=> SetUserNameAsync(user, normalizedName, cancellationToken);
/// <inheritdoc />
public override async Task SetPasswordHashAsync(TUser user, string passwordHash, CancellationToken cancellationToken = default)
{
await base.SetPasswordHashAsync(user, passwordHash, cancellationToken);
user.LastPasswordChangeDateUtc = DateTime.UtcNow;
}
/// <summary>
/// Not supported in Umbraco, see comments above on GetTokenAsync, RemoveTokenAsync, SetTokenAsync
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
protected override Task AddUserTokenAsync(IdentityUserToken<string> token) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco, see comments above on GetTokenAsync, RemoveTokenAsync, SetTokenAsync
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
protected override Task<IdentityUserToken<string>> FindTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken) => throw new NotImplementedException();
/// <summary>
/// Not supported in Umbraco, see comments above on GetTokenAsync, RemoveTokenAsync, SetTokenAsync
/// </summary>
/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
protected override Task RemoveUserTokenAsync(IdentityUserToken<string> token) => throw new NotImplementedException();
}
}