using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Linq; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Identity; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Security { /// /// A custom user store that uses Umbraco member data /// public class MemberUserStore : UserStoreBase, IdentityUserRole, IdentityUserLogin, IdentityUserToken, IdentityRoleClaim> { private const string genericIdentityErrorCode = "IdentityErrorUserStore"; private readonly IMemberService _memberService; private readonly UmbracoMapper _mapper; private readonly IScopeProvider _scopeProvider; /// /// Initializes a new instance of the class for the members identity store /// /// The member service /// The mapper for properties /// The scope provider /// The error describer public MemberUserStore(IMemberService memberService, UmbracoMapper mapper, IScopeProvider scopeProvider, IdentityErrorDescriber describer) : base(describer) { _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); _scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider)); } //TODO: why is this not supported? /// /// Not supported in Umbraco /// /// [EditorBrowsable(EditorBrowsableState.Never)] public override IQueryable Users => throw new NotImplementedException(); /// public override Task GetNormalizedUserNameAsync(MemberIdentityUser user, CancellationToken cancellationToken = default) => GetUserNameAsync(user, cancellationToken); /// public override Task SetNormalizedUserNameAsync(MemberIdentityUser user, string normalizedName, CancellationToken cancellationToken = default) => SetUserNameAsync(user, normalizedName, cancellationToken); /// public override Task CreateAsync(MemberIdentityUser user, CancellationToken cancellationToken = default) { try { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) { throw new ArgumentNullException(nameof(user)); } // create member IMember memberEntity = _memberService.CreateMember( user.UserName, user.Email, user.Name.IsNullOrWhiteSpace() ? user.UserName : user.Name, user.MemberTypeAlias.IsNullOrWhiteSpace() ? Constants.Security.DefaultMemberTypeAlias : user.MemberTypeAlias); UpdateMemberProperties(memberEntity, user); // create the member _memberService.Save(memberEntity); if (!memberEntity.HasIdentity) { throw new DataException("Could not create the member, check logs for details"); } // re-assign id user.Id = UserIdToString(memberEntity.Id); user.Key = memberEntity.Key; // [from backofficeuser] we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. // var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(MembersIdentityUser.Logins)); // TODO: confirm re externallogins implementation //if (isLoginsPropertyDirty) //{ // _externalLoginService.Save( // user.Id, // user.Logins.Select(x => new ExternalLogin( // x.LoginProvider, // x.ProviderKey, // x.UserData))); //} return Task.FromResult(IdentityResult.Success); } catch (Exception ex) { return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = genericIdentityErrorCode, Description = ex.Message })); } } /// public override Task UpdateAsync(MemberIdentityUser user, CancellationToken cancellationToken = default) { try { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) { throw new ArgumentNullException(nameof(user)); } Attempt asInt = user.Id.TryConvertTo(); if (asInt == false) { //TODO: should this be thrown, or an identity result? throw new InvalidOperationException("The user id must be an integer to work with Umbraco"); } using (IScope scope = _scopeProvider.CreateScope()) { 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)) { _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))); //} } scope.Complete(); return Task.FromResult(IdentityResult.Success); } } catch (Exception ex) { return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = genericIdentityErrorCode, Description = ex.Message })); } } /// public override Task DeleteAsync(MemberIdentityUser user, CancellationToken cancellationToken = default) { try { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) { throw new ArgumentNullException(nameof(user)); } IMember found = _memberService.GetById(UserIdToInt(user.Id)); if (found != null) { _memberService.Delete(found); } // TODO: when to implement external login service? //_externalLoginService.DeleteUserLogins(UserIdToInt(user.Id)); return Task.FromResult(IdentityResult.Success); } catch (Exception ex) { return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = genericIdentityErrorCode, Description = ex.Message })); } } /// public override Task FindByIdAsync(string userId, CancellationToken cancellationToken = default) => FindUserAsync(userId, cancellationToken); /// protected override Task FindUserAsync(string userId, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (string.IsNullOrWhiteSpace(userId)) { throw new ArgumentNullException(nameof(userId)); } IMember user = _memberService.GetById(UserIdToInt(userId)); if (user == null) { return Task.FromResult((MemberIdentityUser)null); } return Task.FromResult(AssignLoginsCallback(_mapper.Map(user))); } /// public override Task FindByNameAsync(string userName, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); IMember user = _memberService.GetByUsername(userName); if (user == null) { return Task.FromResult((MemberIdentityUser)null); } MemberIdentityUser result = AssignLoginsCallback(_mapper.Map(user)); return Task.FromResult(result); } /// 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; } /// public override async Task 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; } /// public override Task FindByEmailAsync(string email, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); IMember member = _memberService.GetByEmail(email); MemberIdentityUser result = member == null ? null : _mapper.Map(member); return Task.FromResult(AssignLoginsCallback(result)); } /// public override Task GetNormalizedEmailAsync(MemberIdentityUser user, CancellationToken cancellationToken) => GetEmailAsync(user, cancellationToken); /// public override Task SetNormalizedEmailAsync(MemberIdentityUser user, string normalizedEmail, CancellationToken cancellationToken) => SetEmailAsync(user, normalizedEmail, cancellationToken); /// public override Task AddLoginAsync(MemberIdentityUser user, UserLoginInfo login, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) { throw new ArgumentNullException(nameof(user)); } if (login == null) { throw new ArgumentNullException(nameof(login)); } if (string.IsNullOrWhiteSpace(login.LoginProvider)) { throw new ArgumentNullException(nameof(login.LoginProvider)); } if (string.IsNullOrWhiteSpace(login.ProviderKey)) { throw new ArgumentNullException(nameof(login.ProviderKey)); } ICollection logins = user.Logins; var instance = new IdentityUserLogin( login.LoginProvider, login.ProviderKey, user.Id.ToString()); IdentityUserLogin userLogin = instance; logins.Add(userLogin); return Task.CompletedTask; } /// public override Task RemoveLoginAsync(MemberIdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) { throw new ArgumentNullException(nameof(user)); } if (string.IsNullOrWhiteSpace(loginProvider)) { throw new ArgumentNullException(nameof(loginProvider)); } if (string.IsNullOrWhiteSpace(providerKey)) { throw new ArgumentNullException(nameof(providerKey)); } IIdentityUserLogin userLogin = user.Logins.SingleOrDefault(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey); if (userLogin != null) { user.Logins.Remove(userLogin); } return Task.CompletedTask; } /// public override Task> GetLoginsAsync(MemberIdentityUser user, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) { throw new ArgumentNullException(nameof(user)); } return Task.FromResult((IList)user.Logins.Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.LoginProvider)).ToList()); } /// protected override async Task> FindUserLoginAsync(string userId, string loginProvider, string providerKey, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (string.IsNullOrWhiteSpace(loginProvider)) { throw new ArgumentNullException(nameof(loginProvider)); } if (string.IsNullOrWhiteSpace(providerKey)) { throw new ArgumentNullException(nameof(providerKey)); } MemberIdentityUser user = await FindUserAsync(userId, cancellationToken); if (user == null) { return await Task.FromResult((IdentityUserLogin)null); } IList logins = await GetLoginsAsync(user, cancellationToken); UserLoginInfo found = logins.FirstOrDefault(x => x.ProviderKey == providerKey && x.LoginProvider == loginProvider); if (found == null) { return await Task.FromResult((IdentityUserLogin)null); } return new IdentityUserLogin { LoginProvider = found.LoginProvider, ProviderKey = found.ProviderKey, // TODO: We don't store this value so it will be null ProviderDisplayName = found.ProviderDisplayName, UserId = user.Id }; } /// protected override Task> FindUserLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (string.IsNullOrWhiteSpace(loginProvider)) { throw new ArgumentNullException(nameof(loginProvider)); } if (string.IsNullOrWhiteSpace(providerKey)) { throw new ArgumentNullException(nameof(providerKey)); } var logins = new List(); // TODO: external login needed //_externalLoginService.Find(loginProvider, providerKey).ToList(); if (logins.Count == 0) { return Task.FromResult((IdentityUserLogin)null); } IIdentityUserLogin found = logins[0]; return Task.FromResult(new IdentityUserLogin { LoginProvider = found.LoginProvider, ProviderKey = found.ProviderKey, // TODO: We don't store this value so it will be null ProviderDisplayName = null, UserId = found.UserId }); } /// 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 userRole = user.Roles.SingleOrDefault(r => r.RoleId == role); if (userRole == null) { _memberService.AssignRole(user.UserName, role); user.AddRole(role); } return Task.CompletedTask; } /// 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 userRole = user.Roles.SingleOrDefault(r => r.RoleId == role); if (userRole != null) { _memberService.DissociateRole(user.UserName, userRole.RoleId); user.Roles.Remove(userRole); } return Task.CompletedTask; } /// /// Gets a list of role names the specified user belongs to. /// public override Task> GetRolesAsync(MemberIdentityUser user, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) { throw new ArgumentNullException(nameof(user)); } IEnumerable currentRoles = _memberService.GetAllRoles(user.UserName); ICollection> roles = currentRoles.Select(role => new IdentityUserRole { RoleId = role, UserId = user.Id }).ToList(); user.Roles = roles; return Task.FromResult((IList)user.Roles.Select(x => x.RoleId).ToList()); } /// /// Returns true if a user is in the role /// public override Task IsInRoleAsync(MemberIdentityUser user, string roleName, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (user == null) { throw new ArgumentNullException(nameof(user)); } if (string.IsNullOrWhiteSpace(roleName)) { throw new ArgumentNullException(nameof(roleName)); } return Task.FromResult(user.Roles.Select(x => x.RoleId).InvariantContains(roleName)); } /// /// Lists all users of a given role. /// public override Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); if (string.IsNullOrWhiteSpace(roleName)) { throw new ArgumentNullException(nameof(roleName)); } IEnumerable members = _memberService.GetMembersByMemberType(roleName); IList membersIdentityUsers = members.Select(x => _mapper.Map(x)).ToList(); return Task.FromResult(membersIdentityUsers); } /// protected override Task FindRoleAsync(string roleName, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(roleName)) { throw new ArgumentNullException(nameof(roleName)); } IMemberGroup group = _memberService.GetAllRoles().SingleOrDefault(x => x.Name == roleName); if (group == null) { return Task.FromResult((UmbracoIdentityRole)null); } return Task.FromResult(new UmbracoIdentityRole(group.Name) { //TODO: what should the alias be? Id = group.Id.ToString() }); } /// protected override async Task> FindUserRoleAsync(string userId, string roleId, CancellationToken cancellationToken) { MemberIdentityUser user = await FindUserAsync(userId, cancellationToken); if (user == null) { return null; } IdentityUserRole found = user.Roles.FirstOrDefault(x => x.RoleId.InvariantEquals(roleId)); return found; } /// public override Task 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) { //TODO: implement //user.SetLoginsCallback(new Lazy>(() => _externalLoginService.GetAll(UserIdToInt(user.Id)))); } return user; } private bool UpdateMemberProperties(IMember member, MemberIdentityUser identityUserMember) { 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)) { 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(); 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)) { anythingChanged = true; member.LastPasswordChangeDate = identityUserMember.LastPasswordChangeDateUtc.Value.ToLocalTime(); } if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.Comments)) && member.Comments != identityUserMember.Comments && identityUserMember.Comments.IsNullOrWhiteSpace() == false) { anythingChanged = true; member.Comments = identityUserMember.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)) { anythingChanged = true; member.EmailConfirmedDate = identityUserMember.EmailConfirmed ? (DateTime?)DateTime.Now : null; } if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.Name)) && member.Name != identityUserMember.Name && identityUserMember.Name.IsNullOrWhiteSpace() == false) { anythingChanged = true; member.Name = identityUserMember.Name; } if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.Email)) && member.Email != identityUserMember.Email && identityUserMember.Email.IsNullOrWhiteSpace() == false) { anythingChanged = true; member.Email = identityUserMember.Email; } if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.AccessFailedCount)) && member.FailedPasswordAttempts != identityUserMember.AccessFailedCount) { anythingChanged = true; member.FailedPasswordAttempts = identityUserMember.AccessFailedCount; } if (member.IsLockedOut != identityUserMember.IsLockedOut) { anythingChanged = true; member.IsLockedOut = identityUserMember.IsLockedOut; if (member.IsLockedOut) { // need to set the last lockout date member.LastLockoutDate = DateTime.Now; } } if (member.IsApproved != identityUserMember.IsApproved) { anythingChanged = true; member.IsApproved = identityUserMember.IsApproved; } if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.UserName)) && member.Username != identityUserMember.UserName && identityUserMember.UserName.IsNullOrWhiteSpace() == false) { anythingChanged = true; member.Username = identityUserMember.UserName; } if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.PasswordHash)) && member.RawPasswordValue != identityUserMember.PasswordHash && identityUserMember.PasswordHash.IsNullOrWhiteSpace() == false) { anythingChanged = true; member.RawPasswordValue = identityUserMember.PasswordHash; member.PasswordConfiguration = identityUserMember.PasswordConfig; } if (member.SecurityStamp != identityUserMember.SecurityStamp) { anythingChanged = true; member.SecurityStamp = identityUserMember.SecurityStamp; } // TODO: Fix this for Groups too (as per backoffice comment) if (identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.Roles)) || identityUserMember.IsPropertyDirty(nameof(MemberIdentityUser.Groups))) { } // reset all changes identityUserMember.ResetDirtyProperties(false); return anythingChanged; } private static int UserIdToInt(string userId) { Attempt attempt = userId.TryConvertTo(); 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()); /// /// Not supported in Umbraco /// /// [EditorBrowsable(EditorBrowsableState.Never)] public override Task> GetClaimsAsync(MemberIdentityUser user, CancellationToken cancellationToken = default) => throw new NotImplementedException(); /// /// Not supported in Umbraco /// /// [EditorBrowsable(EditorBrowsableState.Never)] public override Task AddClaimsAsync(MemberIdentityUser user, IEnumerable claims, CancellationToken cancellationToken = default) => throw new NotImplementedException(); /// /// Not supported in Umbraco /// /// [EditorBrowsable(EditorBrowsableState.Never)] public override Task ReplaceClaimAsync(MemberIdentityUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default) => throw new NotImplementedException(); /// /// Not supported in Umbraco /// /// [EditorBrowsable(EditorBrowsableState.Never)] public override Task RemoveClaimsAsync(MemberIdentityUser user, IEnumerable claims, CancellationToken cancellationToken = default) => throw new NotImplementedException(); /// /// Not supported in Umbraco /// /// [EditorBrowsable(EditorBrowsableState.Never)] public override Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default) => throw new NotImplementedException(); /// /// Not supported in Umbraco /// /// [EditorBrowsable(EditorBrowsableState.Never)] protected override Task> FindTokenAsync(MemberIdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) => throw new NotImplementedException(); /// /// Not supported in Umbraco /// /// [EditorBrowsable(EditorBrowsableState.Never)] protected override Task AddUserTokenAsync(IdentityUserToken token) => throw new NotImplementedException(); /// /// Not supported in Umbraco /// /// [EditorBrowsable(EditorBrowsableState.Never)] protected override Task RemoveUserTokenAsync(IdentityUserToken token) => throw new NotImplementedException(); } }