using System; using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Linq; using System.Threading.Tasks; using System.Web.Security; using AutoMapper; using Microsoft.AspNet.Identity; using Microsoft.Owin; using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; namespace Umbraco.Core.Security { public class BackOfficeUserStore : DisposableObject, IUserStore, IUserPasswordStore, IUserEmailStore, IUserLoginStore, IUserRoleStore, IUserSecurityStampStore, IUserLockoutStore, IUserTwoFactorStore //TODO: This would require additional columns/tables for now people will need to implement this on their own //IUserPhoneNumberStore, //TODO: To do this we need to implement IQueryable - we'll have an IQuerable implementation soon with the UmbracoLinqPadDriver implementation //IQueryableUserStore { private readonly IUserService _userService; private readonly IExternalLoginService _externalLoginService; private bool _disposed = false; public BackOfficeUserStore(IUserService userService, IExternalLoginService externalLoginService, MembershipProviderBase usersMembershipProvider) { _userService = userService; _externalLoginService = externalLoginService; if (userService == null) throw new ArgumentNullException("userService"); if (usersMembershipProvider == null) throw new ArgumentNullException("usersMembershipProvider"); if (externalLoginService == null) throw new ArgumentNullException("externalLoginService"); _userService = userService; _externalLoginService = externalLoginService; if (usersMembershipProvider.PasswordFormat != MembershipPasswordFormat.Hashed) { throw new InvalidOperationException("Cannot use ASP.Net Identity with UmbracoMembersUserStore when the password format is not Hashed"); } } /// /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. /// protected override void DisposeResources() { _disposed = true; } /// /// Insert a new user /// /// /// public Task CreateAsync(BackOfficeIdentityUser user) { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); var userType = _userService.GetUserTypeByAlias( user.UserTypeAlias.IsNullOrWhiteSpace() ? _userService.GetDefaultMemberType() : user.UserTypeAlias); var member = new User(userType) { DefaultToLiveEditing = false, Email = user.Email, Language = user.Culture ?? Configuration.GlobalSettings.DefaultUILanguage, Name = user.Name, Username = user.UserName, StartContentId = user.StartContentId == 0 ? -1 : user.StartContentId, StartMediaId = user.StartMediaId == 0 ? -1 : user.StartMediaId, IsLockedOut = user.IsLockedOut, IsApproved = true }; UpdateMemberProperties(member, user); //the password must be 'something' it could be empty if authenticating // with an external provider so we'll just generate one and prefix it, the // prefix will help us determine if the password hasn't actually been specified yet. if (member.RawPasswordValue.IsNullOrWhiteSpace()) { //this will hash the guid with a salt so should be nicely random var aspHasher = new PasswordHasher(); member.RawPasswordValue = "___UIDEMPTYPWORD__" + aspHasher.HashPassword(Guid.NewGuid().ToString("N")); } _userService.Save(member); if (member.Id == 0) throw new DataException("Could not create the user, check logs for details"); //re-assign id user.Id = member.Id; return Task.FromResult(0); } /// /// Update a user /// /// /// public async Task UpdateAsync(BackOfficeIdentityUser user) { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); var asInt = user.Id.TryConvertTo(); if (asInt == false) { throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); } var found = _userService.GetUserById(asInt.Result); if (found != null) { if (UpdateMemberProperties(found, user)) { _userService.Save(found); } if (user.LoginsChanged) { var logins = await GetLoginsAsync(user); _externalLoginService.SaveUserLogins(found.Id, logins); } } } /// /// Delete a user /// /// /// public Task DeleteAsync(BackOfficeIdentityUser user) { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); var asInt = user.Id.TryConvertTo(); if (asInt == false) { throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); } var found = _userService.GetUserById(asInt.Result); if (found != null) { _userService.Delete(found); } _externalLoginService.DeleteUserLogins(asInt.Result); return Task.FromResult(0); } /// /// Finds a user /// /// /// public async Task FindByIdAsync(int userId) { ThrowIfDisposed(); var user = _userService.GetUserById(userId); if (user == null) { return null; } return await Task.FromResult(AssignLoginsCallback(Mapper.Map(user))); } /// /// Find a user by name /// /// /// public async Task FindByNameAsync(string userName) { ThrowIfDisposed(); var user = _userService.GetByUsername(userName); if (user == null) { return null; } var result = AssignLoginsCallback(Mapper.Map(user)); return await Task.FromResult(result); } /// /// Set the user password hash /// /// /// public Task SetPasswordHashAsync(BackOfficeIdentityUser user, string passwordHash) { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); if (passwordHash.IsNullOrWhiteSpace()) throw new ArgumentNullException("passwordHash"); user.PasswordHash = passwordHash; return Task.FromResult(0); } /// /// Get the user password hash /// /// /// public Task GetPasswordHashAsync(BackOfficeIdentityUser user) { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); return Task.FromResult(user.PasswordHash); } /// /// Returns true if a user has a password set /// /// /// public Task HasPasswordAsync(BackOfficeIdentityUser user) { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); return Task.FromResult(user.PasswordHash.IsNullOrWhiteSpace() == false); } /// /// Set the user email /// /// /// public Task SetEmailAsync(BackOfficeIdentityUser user, string email) { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); if (email.IsNullOrWhiteSpace()) throw new ArgumentNullException("email"); user.Email = email; return Task.FromResult(0); } /// /// Get the user email /// /// /// public Task GetEmailAsync(BackOfficeIdentityUser user) { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); return Task.FromResult(user.Email); } /// /// Returns true if the user email is confirmed /// /// /// public Task GetEmailConfirmedAsync(BackOfficeIdentityUser user) { ThrowIfDisposed(); throw new NotImplementedException(); } /// /// Sets whether the user email is confirmed /// /// /// public Task SetEmailConfirmedAsync(BackOfficeIdentityUser user, bool confirmed) { ThrowIfDisposed(); throw new NotImplementedException(); } /// /// Returns the user associated with this email /// /// /// public Task FindByEmailAsync(string email) { ThrowIfDisposed(); var user = _userService.GetByEmail(email); var result = user == null ? null : Mapper.Map(user); return Task.FromResult(AssignLoginsCallback(result)); } /// /// Adds a user login with the specified provider and key /// /// /// public Task AddLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login) { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); if (login == null) throw new ArgumentNullException("login"); var logins = user.Logins; var instance = new IdentityUserLogin(login.LoginProvider, login.ProviderKey, user.Id); var userLogin = instance; logins.Add(userLogin); return Task.FromResult(0); } /// /// Removes the user login with the specified combination if it exists /// /// /// public Task RemoveLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login) { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); if (login == null) throw new ArgumentNullException("login"); var provider = login.LoginProvider; var key = login.ProviderKey; var userLogin = user.Logins.SingleOrDefault((l => l.LoginProvider == provider && l.ProviderKey == key)); if (userLogin != null) user.Logins.Remove(userLogin); return Task.FromResult(0); } /// /// Returns the linked accounts for this user /// /// /// public Task> GetLoginsAsync(BackOfficeIdentityUser user) { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); return Task.FromResult((IList) user.Logins.Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey)).ToList()); } /// /// Returns the user associated with this login /// /// public Task FindAsync(UserLoginInfo login) { ThrowIfDisposed(); if (login == null) throw new ArgumentNullException("login"); //get all logins associated with the login id var result = _externalLoginService.Find(login).ToArray(); if (result.Any()) { //return the first member that matches the result var output = (from l in result select _userService.GetUserById(l.UserId) into user where user != null select Mapper.Map(user)).FirstOrDefault(); return Task.FromResult(AssignLoginsCallback(output)); } return Task.FromResult(null); } /// /// Adds a user to a role (section) /// /// /// public Task AddToRoleAsync(BackOfficeIdentityUser user, string roleName) { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); if (user.AllowedSections.InvariantContains(roleName)) return Task.FromResult(0); var asInt = user.Id.TryConvertTo(); if (asInt == false) { throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); } var found = _userService.GetUserById(asInt.Result); if (found != null) { found.AddAllowedSection(roleName); } return Task.FromResult(0); } /// /// Removes the role (allowed section) for the user /// /// /// public Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string roleName) { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); if (user.AllowedSections.InvariantContains(roleName) == false) return Task.FromResult(0); var asInt = user.Id.TryConvertTo(); if (asInt == false) { throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); } var found = _userService.GetUserById(asInt.Result); if (found != null) { found.RemoveAllowedSection(roleName); } return Task.FromResult(0); } /// /// Returns the roles for this user /// /// /// public Task> GetRolesAsync(BackOfficeIdentityUser user) { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); return Task.FromResult((IList)user.AllowedSections.ToList()); } /// /// Returns true if a user is in the role /// /// /// public Task IsInRoleAsync(BackOfficeIdentityUser user, string roleName) { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); return Task.FromResult(user.AllowedSections.InvariantContains(roleName)); } /// /// Set the security stamp for the user /// /// /// public Task SetSecurityStampAsync(BackOfficeIdentityUser user, string stamp) { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); user.SecurityStamp = stamp; return Task.FromResult(0); } /// /// Get the user security stamp /// /// /// public Task GetSecurityStampAsync(BackOfficeIdentityUser user) { ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("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.ToMd5() : user.SecurityStamp); } private BackOfficeIdentityUser AssignLoginsCallback(BackOfficeIdentityUser user) { if (user != null) { user.SetLoginsCallback(new Lazy>(() => _externalLoginService.GetAll(user.Id))); } return user; } /// /// Sets whether two factor authentication is enabled for the user /// /// /// public virtual Task SetTwoFactorEnabledAsync(BackOfficeIdentityUser user, bool enabled) { user.TwoFactorEnabled = false; return Task.FromResult(0); } /// /// Returns whether two factor authentication is enabled for the user /// /// /// public virtual Task GetTwoFactorEnabledAsync(BackOfficeIdentityUser user) { return Task.FromResult(false); } #region IUserLockoutStore /// /// Returns the DateTimeOffset that represents the end of a user's lockout, any time in the past should be considered not locked out. /// /// /// /// /// Currently we do not suport a timed lock out, when they are locked out, an admin will have to reset the status /// public Task GetLockoutEndDateAsync(BackOfficeIdentityUser user) { if (user == null) throw new ArgumentNullException("user"); return user.LockoutEndDateUtc.HasValue ? Task.FromResult(DateTimeOffset.MaxValue) : Task.FromResult(DateTimeOffset.MinValue); } /// /// Locks a user out until the specified end date (set to a past date, to unlock a user) /// /// /// public Task SetLockoutEndDateAsync(BackOfficeIdentityUser user, DateTimeOffset lockoutEnd) { if (user == null) throw new ArgumentNullException("user"); user.LockoutEndDateUtc = lockoutEnd.UtcDateTime; return Task.FromResult(0); } /// /// Used to record when an attempt to access the user has failed /// /// /// public Task IncrementAccessFailedCountAsync(BackOfficeIdentityUser user) { if (user == null) throw new ArgumentNullException("user"); user.AccessFailedCount++; return Task.FromResult(user.AccessFailedCount); } /// /// Used to reset the access failed count, typically after the account is successfully accessed /// /// /// public Task ResetAccessFailedCountAsync(BackOfficeIdentityUser user) { if (user == null) throw new ArgumentNullException("user"); user.AccessFailedCount = 0; return Task.FromResult(0); } /// /// Returns the current number of failed access attempts. This number usually will be reset whenever the password is /// verified or the account is locked out. /// /// /// public Task GetAccessFailedCountAsync(BackOfficeIdentityUser user) { if (user == null) throw new ArgumentNullException("user"); return Task.FromResult(user.AccessFailedCount); } /// /// Returns true /// /// /// public Task GetLockoutEnabledAsync(BackOfficeIdentityUser user) { if (user == null) throw new ArgumentNullException("user"); return Task.FromResult(user.LockoutEnabled); } /// /// Doesn't actually perform any function, users can always be locked out /// /// /// public Task SetLockoutEnabledAsync(BackOfficeIdentityUser user, bool enabled) { if (user == null) throw new ArgumentNullException("user"); user.LockoutEnabled = enabled; return Task.FromResult(0); } #endregion private bool UpdateMemberProperties(Models.Membership.IUser user, BackOfficeIdentityUser identityUser) { var anythingChanged = false; //don't assign anything if nothing has changed as this will trigger //the track changes of the model if (user.Name != identityUser.Name && identityUser.Name.IsNullOrWhiteSpace() == false) { anythingChanged = true; user.Name = identityUser.Name; } if (user.Email != identityUser.Email && identityUser.Email.IsNullOrWhiteSpace() == false) { anythingChanged = true; user.Email = identityUser.Email; } if (user.FailedPasswordAttempts != identityUser.AccessFailedCount) { anythingChanged = true; user.FailedPasswordAttempts = identityUser.AccessFailedCount; } if (user.IsLockedOut != identityUser.IsLockedOut) { anythingChanged = true; user.IsLockedOut = identityUser.IsLockedOut; if (user.IsLockedOut) { //need to set the last lockout date user.LastLockoutDate = DateTime.Now; } } if (user.Username != identityUser.UserName && identityUser.UserName.IsNullOrWhiteSpace() == false) { anythingChanged = true; user.Username = identityUser.UserName; } if (user.RawPasswordValue != identityUser.PasswordHash && identityUser.PasswordHash.IsNullOrWhiteSpace() == false) { anythingChanged = true; user.RawPasswordValue = identityUser.PasswordHash; } if (user.Language != identityUser.Culture && identityUser.Culture.IsNullOrWhiteSpace() == false) { anythingChanged = true; user.Language = identityUser.Culture; } if (user.StartMediaId != identityUser.StartMediaId) { anythingChanged = true; user.StartMediaId = identityUser.StartMediaId; } if (user.StartContentId != identityUser.StartContentId) { anythingChanged = true; user.StartContentId = identityUser.StartContentId; } if (user.SecurityStamp != identityUser.SecurityStamp) { anythingChanged = true; user.SecurityStamp = identityUser.SecurityStamp; } if (user.AllowedSections.ContainsAll(identityUser.AllowedSections) == false || identityUser.AllowedSections.ContainsAll(user.AllowedSections) == false) { anythingChanged = true; foreach (var allowedSection in user.AllowedSections) { user.RemoveAllowedSection(allowedSection); } foreach (var allowedApplication in identityUser.AllowedSections) { user.AddAllowedSection(allowedApplication); } } return anythingChanged; } private void ThrowIfDisposed() { if (_disposed) throw new ObjectDisposedException(GetType().Name); } } }