From 332946dbd83002c6957244052a4b32f39f8ec9ae Mon Sep 17 00:00:00 2001 From: Scott Brady Date: Wed, 13 May 2020 18:30:57 +0100 Subject: [PATCH 01/13] Duplicated user store across to Umbraco.Web.BackOffice --- .../Identity/BackOfficeIdentityUser.cs | 434 +++++++++ .../Identity/BackOfficeUserStore.cs | 916 ++++++++++++++++++ .../Identity/IUserSessionStore.cs | 15 + .../Identity/IdentityUser.cs | 116 +++ .../Identity/UserLoginInfoWrapper.cs | 29 + 5 files changed, 1510 insertions(+) create mode 100644 src/Umbraco.Web.BackOffice/Identity/BackOfficeIdentityUser.cs create mode 100644 src/Umbraco.Web.BackOffice/Identity/BackOfficeUserStore.cs create mode 100644 src/Umbraco.Web.BackOffice/Identity/IUserSessionStore.cs create mode 100644 src/Umbraco.Web.BackOffice/Identity/IdentityUser.cs create mode 100644 src/Umbraco.Web.BackOffice/Identity/UserLoginInfoWrapper.cs diff --git a/src/Umbraco.Web.BackOffice/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Web.BackOffice/Identity/BackOfficeIdentityUser.cs new file mode 100644 index 0000000000..c1c21d3ab2 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Identity/BackOfficeIdentityUser.cs @@ -0,0 +1,434 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models.Entities; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Models.Membership; + +namespace Umbraco.Web.BackOffice.Identity +{ + public class BackOfficeIdentityUser : IdentityUser, IdentityUserClaim>, IRememberBeingDirty + { + private string _email; + private string _userName; + private int _id; + private bool _hasIdentity; + private DateTime? _lastLoginDateUtc; + private bool _emailConfirmed; + private string _name; + private int _accessFailedCount; + private string _passwordHash; + private string _culture; + private ObservableCollection _logins; + private Lazy> _getLogins; + private IReadOnlyUserGroup[] _groups; + private string[] _allowedSections; + private int[] _startMediaIds; + private int[] _startContentIds; + private DateTime? _lastPasswordChangeDateUtc; + + /// + /// Used to construct a new instance without an identity + /// + /// + /// This is allowed to be null (but would need to be filled in if trying to persist this instance) + /// + /// + public static BackOfficeIdentityUser CreateNew(IGlobalSettings globalSettings, string username, string email, string culture) + { + if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); + if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture)); + + var user = new BackOfficeIdentityUser(globalSettings, Array.Empty()); + user.DisableChangeTracking(); + user._userName = username; + user._email = email; + //we are setting minvalue here because the default is "0" which is the id of the admin user + //which we cannot allow because the admin user will always exist + user._id = int.MinValue; + user._hasIdentity = false; + user._culture = culture; + user.EnableChangeTracking(); + return user; + } + + private BackOfficeIdentityUser(IGlobalSettings globalSettings, IReadOnlyUserGroup[] groups) + { + _startMediaIds = Array.Empty(); + _startContentIds = Array.Empty(); + _allowedSections = Array.Empty(); + _culture = globalSettings.DefaultUILanguage; + + // must initialize before setting groups + _roles = new ObservableCollection>(); + _roles.CollectionChanged += _roles_CollectionChanged; + + // use the property setters - they do more than just setting a field + Groups = groups; + } + + /// + /// Creates an existing user with the specified groups + /// + /// + /// + /// + public BackOfficeIdentityUser(IGlobalSettings globalSettings, int userId, IEnumerable groups) + : this(globalSettings, groups.ToArray()) + { + // use the property setters - they do more than just setting a field + Id = userId; + } + + /// + /// Returns true if an Id has been set on this object this will be false if the object is new and not persisted to the database + /// + public bool HasIdentity => _hasIdentity; + + public int[] CalculatedMediaStartNodeIds { get; internal set; } + public int[] CalculatedContentStartNodeIds { get; internal set; } + + public override int Id + { + get => _id; + set + { + _id = value; + _hasIdentity = true; + } + } + + /// + /// Override Email so we can track changes to it + /// + public override string Email + { + get => _email; + set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _email, nameof(Email)); + } + + /// + /// Override UserName so we can track changes to it + /// + public override string UserName + { + get => _userName; + set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _userName, nameof(UserName)); + } + + /// + /// LastPasswordChangeDateUtc so we can track changes to it + /// + public override DateTime? LastPasswordChangeDateUtc + { + get { return _lastPasswordChangeDateUtc; } + set { _beingDirty.SetPropertyValueAndDetectChanges(value, ref _lastPasswordChangeDateUtc, nameof(LastPasswordChangeDateUtc)); } + } + + /// + /// Override LastLoginDateUtc so we can track changes to it + /// + public override DateTime? LastLoginDateUtc + { + get => _lastLoginDateUtc; + set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _lastLoginDateUtc, nameof(LastLoginDateUtc)); + } + + /// + /// Override EmailConfirmed so we can track changes to it + /// + public override bool EmailConfirmed + { + get => _emailConfirmed; + set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _emailConfirmed, nameof(EmailConfirmed)); + } + + /// + /// Gets/sets the user's real name + /// + public string Name + { + get => _name; + set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); + } + + /// + /// Override AccessFailedCount so we can track changes to it + /// + public override int AccessFailedCount + { + get => _accessFailedCount; + set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _accessFailedCount, nameof(AccessFailedCount)); + } + + /// + /// Override PasswordHash so we can track changes to it + /// + public override string PasswordHash + { + get => _passwordHash; + set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordHash, nameof(PasswordHash)); + } + + + /// + /// Content start nodes assigned to the User (not ones assigned to the user's groups) + /// + public int[] StartContentIds + { + get => _startContentIds; + set + { + if (value == null) value = new int[0]; + _beingDirty.SetPropertyValueAndDetectChanges(value, ref _startContentIds, nameof(StartContentIds), StartIdsComparer); + } + } + + /// + /// Media start nodes assigned to the User (not ones assigned to the user's groups) + /// + public int[] StartMediaIds + { + get => _startMediaIds; + set + { + if (value == null) value = new int[0]; + _beingDirty.SetPropertyValueAndDetectChanges(value, ref _startMediaIds, nameof(StartMediaIds), StartIdsComparer); + } + } + + /// + /// This is a readonly list of the user's allowed sections which are based on it's user groups + /// + public string[] AllowedSections + { + get { return _allowedSections ?? (_allowedSections = _groups.SelectMany(x => x.AllowedSections).Distinct().ToArray()); } + } + + public string Culture + { + get => _culture; + set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _culture, nameof(Culture)); + } + + public IReadOnlyUserGroup[] Groups + { + get => _groups; + set + { + //so they recalculate + _allowedSections = null; + + _groups = value; + + //now clear all roles and re-add them + _roles.CollectionChanged -= _roles_CollectionChanged; + _roles.Clear(); + foreach (var identityUserRole in _groups.Select(x => new IdentityUserRole + { + RoleId = x.Alias, + UserId = Id.ToString() + })) + { + _roles.Add(identityUserRole); + } + _roles.CollectionChanged += _roles_CollectionChanged; + + _beingDirty.SetPropertyValueAndDetectChanges(value, ref _groups, nameof(Groups), GroupsComparer); + } + } + + /// + /// Lockout is always enabled + /// + public override bool LockoutEnabled + { + get { return true; } + set + { + //do nothing + } + } + + /// + /// Based on the user's lockout end date, this will determine if they are locked out + /// + internal bool IsLockedOut + { + get + { + var isLocked = LockoutEndDateUtc.HasValue && LockoutEndDateUtc.Value.ToLocalTime() >= DateTime.Now; + return isLocked; + } + } + + /// + /// This is a 1:1 mapping with IUser.IsApproved + /// + internal bool IsApproved { get; set; } + + /// + /// Overridden to make the retrieval lazy + /// + public override ICollection Logins + { + get + { + if (_getLogins != null && _getLogins.IsValueCreated == false) + { + _logins = new ObservableCollection(); + foreach (var l in _getLogins.Value) + { + _logins.Add(l); + } + //now assign events + _logins.CollectionChanged += Logins_CollectionChanged; + } + return _logins; + } + } + + void Logins_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + _beingDirty.OnPropertyChanged(nameof(Logins)); + } + + private void _roles_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + _beingDirty.OnPropertyChanged(nameof(Roles)); + } + + private readonly ObservableCollection> _roles; + + /// + /// helper method to easily add a role without having to deal with IdentityUserRole{T} + /// + /// + /// + /// Adding a role this way will not reflect on the user's group's collection or it's allowed sections until the user is persisted + /// + public void AddRole(string role) + { + Roles.Add(new IdentityUserRole + { + UserId = Id.ToString(), + RoleId = role + }); + } + + /// + /// Override Roles because the value of these are the user's group aliases + /// + public override ICollection> Roles => _roles; + + /// + /// Used to set a lazy call back to populate the user's Login list + /// + /// + public void SetLoginsCallback(Lazy> callback) + { + _getLogins = callback ?? throw new ArgumentNullException(nameof(callback)); + } + + #region BeingDirty + + private readonly BeingDirty _beingDirty = new BeingDirty(); + + /// + public bool IsDirty() + { + return _beingDirty.IsDirty(); + } + + /// + public bool IsPropertyDirty(string propName) + { + return _beingDirty.IsPropertyDirty(propName); + } + + /// + public IEnumerable GetDirtyProperties() + { + return _beingDirty.GetDirtyProperties(); + } + + /// + public void ResetDirtyProperties() + { + _beingDirty.ResetDirtyProperties(); + } + + /// + public bool WasDirty() + { + return _beingDirty.WasDirty(); + } + + /// + public bool WasPropertyDirty(string propertyName) + { + return _beingDirty.WasPropertyDirty(propertyName); + } + + /// + public void ResetWereDirtyProperties() + { + _beingDirty.ResetWereDirtyProperties(); + } + + /// + public void ResetDirtyProperties(bool rememberDirty) + { + _beingDirty.ResetDirtyProperties(rememberDirty); + } + + /// + public IEnumerable GetWereDirtyProperties() + => _beingDirty.GetWereDirtyProperties(); + + /// + /// Disables change tracking. + /// + public void DisableChangeTracking() + { + _beingDirty.DisableChangeTracking(); + } + + /// + /// Enables change tracking. + /// + public void EnableChangeTracking() + { + _beingDirty.EnableChangeTracking(); + } + + public event PropertyChangedEventHandler PropertyChanged + { + add + { + _beingDirty.PropertyChanged += value; + } + remove + { + _beingDirty.PropertyChanged -= value; + } + } + + #endregion + + //Custom comparer for enumerables + private static readonly DelegateEqualityComparer GroupsComparer = new DelegateEqualityComparer( + (groups, enumerable) => groups.Select(x => x.Alias).UnsortedSequenceEqual(enumerable.Select(x => x.Alias)), + groups => groups.GetHashCode()); + + private static readonly DelegateEqualityComparer StartIdsComparer = new DelegateEqualityComparer( + (groups, enumerable) => groups.UnsortedSequenceEqual(enumerable), + groups => groups.GetHashCode()); + + } +} diff --git a/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserStore.cs b/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserStore.cs new file mode 100644 index 0000000000..6815fdc71e --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserStore.cs @@ -0,0 +1,916 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Mapping; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Services; + +namespace Umbraco.Web.BackOffice.Identity +{ + public class BackOfficeUserStore : DisposableObjectSlim, + IUserPasswordStore, + IUserEmailStore, + IUserLoginStore, + IUserRoleStore, + IUserSecurityStampStore, + IUserLockoutStore, + IUserTwoFactorStore, + IUserSessionStore + + // 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 IEntityService _entityService; + private readonly IExternalLoginService _externalLoginService; + private readonly IGlobalSettings _globalSettings; + private readonly UmbracoMapper _mapper; + private bool _disposed = false; + + public BackOfficeUserStore(IUserService userService, IEntityService entityService, IExternalLoginService externalLoginService, IGlobalSettings globalSettings, UmbracoMapper mapper) + { + _userService = userService; + _entityService = entityService; + _externalLoginService = externalLoginService; + _globalSettings = globalSettings; + if (userService == null) throw new ArgumentNullException("userService"); + if (externalLoginService == null) throw new ArgumentNullException("externalLoginService"); + _mapper = mapper; + + _userService = userService; + _externalLoginService = externalLoginService; + } + + /// + /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. + /// + protected override void DisposeResources() + { + _disposed = true; + } + + public Task GetUserIdAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(user)); + + return Task.FromResult(user.Id.ToString()); + } + + public Task GetUserNameAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(user)); + + return Task.FromResult(user.UserName); + } + + public Task SetUserNameAsync(BackOfficeIdentityUser user, string userName, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(user)); + + user.UserName = userName; + return Task.CompletedTask; + } + + public Task GetNormalizedUserNameAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) + { + return GetUserNameAsync(user, cancellationToken); + } + + public Task SetNormalizedUserNameAsync(BackOfficeIdentityUser user, string normalizedName, CancellationToken cancellationToken) + { + return SetUserNameAsync(user, normalizedName, cancellationToken); + } + + /// + /// Insert a new user + /// + /// + /// + /// + public Task CreateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(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. + //this will hash the guid with a salt so should be nicely random + var aspHasher = new PasswordHasher(); + var emptyPasswordValue = Constants.Security.EmptyPasswordPrefix + + aspHasher.HashPassword(user, Guid.NewGuid().ToString("N")); + + var userEntity = new User(_globalSettings, user.Name, user.Email, user.UserName, emptyPasswordValue) + { + Language = user.Culture ?? _globalSettings.DefaultUILanguage, + StartContentIds = user.StartContentIds ?? new int[] { }, + StartMediaIds = user.StartMediaIds ?? new int[] { }, + IsLockedOut = user.IsLockedOut, + }; + + UpdateMemberProperties(userEntity, user); + + // TODO: We should deal with Roles --> User Groups here which we currently are not doing + + _userService.Save(userEntity); + + if (!userEntity.HasIdentity) throw new DataException("Could not create the user, check logs for details"); + + //re-assign id + user.Id = userEntity.Id; + + return Task.FromResult(IdentityResult.Success); + } + + /// + /// Update a user + /// + /// + /// + /// + public async Task UpdateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(user)); + + var found = _userService.GetUserById(user.Id); + if (found != null) + { + // we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. + var isLoginsPropertyDirty = user.IsPropertyDirty("Logins"); + + if (UpdateMemberProperties(found, user)) + { + _userService.Save(found); + } + + if (isLoginsPropertyDirty) + { + var logins = await GetLoginsAsync(user); + _externalLoginService.SaveUserLogins(found.Id, logins.Select(UserLoginInfoWrapper.Wrap)); + } + } + + return IdentityResult.Success; + } + + /// + /// Delete a user + /// + /// + /// + public Task DeleteAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(user)); + + var found = _userService.GetUserById(user.Id); + if (found != null) + { + _userService.Delete(found); + } + _externalLoginService.DeleteUserLogins(user.Id); + + return Task.FromResult(IdentityResult.Success); + } + + /// + /// Finds a user + /// + /// + /// + /// + public async Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + var user = _userService.GetUserById(UserIdToInt(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, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + 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, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(user)); + if (passwordHash == null) throw new ArgumentNullException(nameof(passwordHash)); + if (string.IsNullOrEmpty(passwordHash)) throw new ArgumentException("Value can't be empty.", nameof(passwordHash)); + + user.PasswordHash = passwordHash; + + return Task.CompletedTask; + } + + /// + /// Get the user password hash + /// + /// + /// + /// + public Task GetPasswordHashAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(user)); + + return Task.FromResult(user.PasswordHash); + } + + /// + /// Returns true if a user has a password set + /// + /// + /// + /// + public Task HasPasswordAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(user)); + + return Task.FromResult(string.IsNullOrEmpty(user.PasswordHash) == false); + } + + /// + /// Set the user email + /// + /// + /// + /// + public Task SetEmailAsync(BackOfficeIdentityUser user, string email, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(user)); + if (email.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(email)); + + user.Email = email; + + return Task.CompletedTask; + } + + /// + /// Get the user email + /// + /// + /// + /// + public Task GetEmailAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(user)); + + return Task.FromResult(user.Email); + } + + /// + /// Returns true if the user email is confirmed + /// + /// + /// + /// + public Task GetEmailConfirmedAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(user)); + + return Task.FromResult(user.EmailConfirmed); + } + + /// + /// Sets whether the user email is confirmed + /// + /// + /// + /// + public Task SetEmailConfirmedAsync(BackOfficeIdentityUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + user.EmailConfirmed = confirmed; + return Task.CompletedTask; + } + + /// + /// Returns the user associated with this email + /// + /// + /// + /// + public Task FindByEmailAsync(string email, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + var user = _userService.GetByEmail(email); + var result = user == null + ? null + : _mapper.Map(user); + + return Task.FromResult(AssignLoginsCallback(result)); + } + + public Task GetNormalizedEmailAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) + { + return GetEmailAsync(user, cancellationToken); + } + + public Task SetNormalizedEmailAsync(BackOfficeIdentityUser user, string normalizedEmail, CancellationToken cancellationToken) + { + return SetEmailAsync(user, normalizedEmail, cancellationToken); + } + + /// + /// Adds a user login with the specified provider and key + /// + /// + /// + /// + /// + public Task AddLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(user)); + if (login == null) throw new ArgumentNullException(nameof(login)); + + var logins = user.Logins; + var instance = new IdentityUserLogin(login.LoginProvider, login.ProviderKey, user.Id); + var userLogin = instance; + logins.Add(userLogin); + + return Task.CompletedTask; + } + + /// + /// Removes the user login with the specified combination if it exists + /// + /// + /// + /// + /// + /// + public Task RemoveLoginAsync(BackOfficeIdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(user)); + + var userLogin = user.Logins.SingleOrDefault(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey); + if (userLogin != null) user.Logins.Remove(userLogin); + + return Task.CompletedTask; + } + + /// + /// Returns the linked accounts for this user + /// + /// + /// + /// + public Task> GetLoginsAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + 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()); + } + + /// + /// Returns the user associated with this login + /// + /// + public Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + //get all logins associated with the login id + var result = _externalLoginService.Find(UserLoginInfoWrapper.Wrap(new UserLoginInfo(loginProvider, providerKey, loginProvider))).ToArray(); + if (result.Any()) + { + //return the first user that matches the result + BackOfficeIdentityUser output = null; + foreach (var l in result) + { + var user = _userService.GetUserById(l.UserId); + if (user != null) + { + output = _mapper.Map(user); + break; + } + } + + return Task.FromResult(AssignLoginsCallback(output)); + } + + return Task.FromResult(null); + } + + + /// + /// Adds a user to a role (user group) + /// + /// + /// + /// + /// + public Task AddToRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) + { + 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)); + + var userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName); + + if (userRole == null) + { + user.AddRole(normalizedRoleName); + } + + return Task.CompletedTask; + } + + /// + /// Removes the role (user group) for the user + /// + /// + /// + /// + /// + public Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) + { + 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)); + + var userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName); + + if (userRole != null) + { + user.Roles.Remove(userRole); + } + + return Task.CompletedTask; + } + + /// + /// Returns the roles (user groups) for this user + /// + /// + /// + /// + public Task> GetRolesAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(user)); + return Task.FromResult((IList)user.Roles.Select(x => x.RoleId).ToList()); + } + + /// + /// Returns true if a user is in the role + /// + /// + /// + /// + /// + public Task IsInRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(user)); + return Task.FromResult(user.Roles.Select(x => x.RoleId).InvariantContains(normalizedRoleName)); + } + + /// + /// Lists all users of a given role. + /// + /// + /// Identity Role names are equal to Umbraco UserGroup alias. + /// + public Task> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (normalizedRoleName == null) throw new ArgumentNullException(nameof(normalizedRoleName)); + + var userGroup = _userService.GetUserGroupByAlias(normalizedRoleName); + + var users = _userService.GetAllInGroup(userGroup.Id); + IList backOfficeIdentityUsers = users.Select(x => _mapper.Map(x)).ToList(); + + return Task.FromResult(backOfficeIdentityUsers); + } + + /// + /// Set the security stamp for the user + /// + /// + /// + /// + /// + public Task SetSecurityStampAsync(BackOfficeIdentityUser user, string stamp, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(user)); + + user.SecurityStamp = stamp; + return Task.CompletedTask; + } + + /// + /// Get the user security stamp + /// + /// + /// + /// + public Task GetSecurityStampAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + 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) + { + 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, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + user.TwoFactorEnabled = false; + return Task.CompletedTask; + } + + /// + /// Returns whether two factor authentication is enabled for the user + /// + /// + /// + public virtual Task GetTwoFactorEnabledAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + 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 support a timed lock out, when they are locked out, an admin will have to reset the status + /// + public Task GetLockoutEndDateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(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) + /// + /// + /// + /// + /// + /// Currently we do not support a timed lock out, when they are locked out, an admin will have to reset the status + /// + public Task SetLockoutEndDateAsync(BackOfficeIdentityUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(user)); + + user.LockoutEndDateUtc = lockoutEnd.Value.UtcDateTime; + return Task.CompletedTask; + } + + /// + /// Used to record when an attempt to access the user has failed + /// + /// + /// + /// + public Task IncrementAccessFailedCountAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(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, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(user)); + + user.AccessFailedCount = 0; + return Task.CompletedTask; + } + + /// + /// 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, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(user)); + return Task.FromResult(user.AccessFailedCount); + } + + /// + /// Returns true + /// + /// + /// + /// + public Task GetLockoutEnabledAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(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, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException(nameof(user)); + + user.LockoutEnabled = enabled; + return Task.CompletedTask; + } + #endregion + + private bool UpdateMemberProperties(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 (identityUser.IsPropertyDirty("LastLoginDateUtc") + || (user.LastLoginDate != default(DateTime) && identityUser.LastLoginDateUtc.HasValue == false) + || identityUser.LastLoginDateUtc.HasValue && user.LastLoginDate.ToUniversalTime() != identityUser.LastLoginDateUtc.Value) + { + anythingChanged = true; + //if the LastLoginDate is being set to MinValue, don't convert it ToLocalTime + var dt = identityUser.LastLoginDateUtc == DateTime.MinValue ? DateTime.MinValue : identityUser.LastLoginDateUtc.Value.ToLocalTime(); + user.LastLoginDate = dt; + } + if (identityUser.IsPropertyDirty("LastPasswordChangeDateUtc") + || (user.LastPasswordChangeDate != default(DateTime) && identityUser.LastPasswordChangeDateUtc.HasValue == false) + || identityUser.LastPasswordChangeDateUtc.HasValue && user.LastPasswordChangeDate.ToUniversalTime() != identityUser.LastPasswordChangeDateUtc.Value) + { + anythingChanged = true; + user.LastPasswordChangeDate = identityUser.LastPasswordChangeDateUtc.Value.ToLocalTime(); + } + if (identityUser.IsPropertyDirty("EmailConfirmed") + || (user.EmailConfirmedDate.HasValue && user.EmailConfirmedDate.Value != default(DateTime) && identityUser.EmailConfirmed == false) + || ((user.EmailConfirmedDate.HasValue == false || user.EmailConfirmedDate.Value == default(DateTime)) && identityUser.EmailConfirmed)) + { + anythingChanged = true; + user.EmailConfirmedDate = identityUser.EmailConfirmed ? (DateTime?)DateTime.Now : null; + } + if (identityUser.IsPropertyDirty("Name") + && user.Name != identityUser.Name && identityUser.Name.IsNullOrWhiteSpace() == false) + { + anythingChanged = true; + user.Name = identityUser.Name; + } + if (identityUser.IsPropertyDirty("Email") + && user.Email != identityUser.Email && identityUser.Email.IsNullOrWhiteSpace() == false) + { + anythingChanged = true; + user.Email = identityUser.Email; + } + if (identityUser.IsPropertyDirty("AccessFailedCount") + && 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 (identityUser.IsPropertyDirty("UserName") + && user.Username != identityUser.UserName && identityUser.UserName.IsNullOrWhiteSpace() == false) + { + anythingChanged = true; + user.Username = identityUser.UserName; + } + if (identityUser.IsPropertyDirty("PasswordHash") + && user.RawPasswordValue != identityUser.PasswordHash && identityUser.PasswordHash.IsNullOrWhiteSpace() == false) + { + anythingChanged = true; + user.RawPasswordValue = identityUser.PasswordHash; + } + + if (identityUser.IsPropertyDirty("Culture") + && user.Language != identityUser.Culture && identityUser.Culture.IsNullOrWhiteSpace() == false) + { + anythingChanged = true; + user.Language = identityUser.Culture; + } + if (identityUser.IsPropertyDirty("StartMediaIds") + && user.StartMediaIds.UnsortedSequenceEqual(identityUser.StartMediaIds) == false) + { + anythingChanged = true; + user.StartMediaIds = identityUser.StartMediaIds; + } + if (identityUser.IsPropertyDirty("StartContentIds") + && user.StartContentIds.UnsortedSequenceEqual(identityUser.StartContentIds) == false) + { + anythingChanged = true; + user.StartContentIds = identityUser.StartContentIds; + } + if (user.SecurityStamp != identityUser.SecurityStamp) + { + anythingChanged = true; + user.SecurityStamp = identityUser.SecurityStamp; + } + + // TODO: Fix this for Groups too + if (identityUser.IsPropertyDirty("Roles") || identityUser.IsPropertyDirty("Groups")) + { + 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(); + + if (userGroupAliases.ContainsAll(combinedAliases) == false + || combinedAliases.ContainsAll(userGroupAliases) == false) + { + 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; + } + } + + //we should re-set the calculated start nodes + identityUser.CalculatedMediaStartNodeIds = user.CalculateMediaStartNodeIds(_entityService); + identityUser.CalculatedContentStartNodeIds = user.CalculateContentStartNodeIds(_entityService); + + //reset all changes + identityUser.ResetDirtyProperties(false); + + return anythingChanged; + } + + private void ThrowIfDisposed() + { + if (_disposed) throw new ObjectDisposedException(GetType().Name); + } + + public Task ValidateSessionIdAsync(string userId, string sessionId) + { + Guid guidSessionId; + if (Guid.TryParse(sessionId, out guidSessionId)) + { + return Task.FromResult(_userService.ValidateLoginSession(UserIdToInt(userId), guidSessionId)); + } + + return Task.FromResult(false); + } + + private static int UserIdToInt(string userId) + { + var attempt = userId.TryConvertTo(); + if (attempt.Success) return attempt.Result; + + throw new InvalidOperationException("Unable to convert user ID to int", attempt.Exception); + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Identity/IUserSessionStore.cs b/src/Umbraco.Web.BackOffice/Identity/IUserSessionStore.cs new file mode 100644 index 0000000000..756eec3cb8 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Identity/IUserSessionStore.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; + +namespace Umbraco.Web.BackOffice.Identity +{ + /// + /// An IUserStore interface part to implement if the store supports validating user session Ids + /// + /// + public interface IUserSessionStore : IUserStore + where TUser : class + { + Task ValidateSessionIdAsync(string userId, string sessionId); + } +} diff --git a/src/Umbraco.Web.BackOffice/Identity/IdentityUser.cs b/src/Umbraco.Web.BackOffice/Identity/IdentityUser.cs new file mode 100644 index 0000000000..b0399bf228 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Identity/IdentityUser.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Models.Identity; + +namespace Umbraco.Web.BackOffice.Identity +{ + /// + /// Default IUser implementation + /// + /// + /// + /// This class normally exists inside of the EntityFramework library, not sure why MS chose to explicitly put it there but we don't want + /// references to that so we will create our own here + /// + public class IdentityUser + where TLogin : IIdentityUserLogin + //NOTE: Making our role id a string + where TRole : IdentityUserRole + where TClaim : IdentityUserClaim + { + /// + /// Initializes a new instance of the class. + /// + public IdentityUser() + { + Claims = new List(); + Roles = new List(); + Logins = new List(); + } + + /// + /// Last login date + /// + public virtual DateTime? LastLoginDateUtc { get; set; } + + /// + /// Email + /// + public virtual string Email { get; set; } + + /// + /// True if the email is confirmed, default is false + /// + public virtual bool EmailConfirmed { get; set; } + + /// + /// The salted/hashed form of the user password + /// + public virtual string PasswordHash { get; set; } + + /// + /// A random value that should change whenever a users credentials have changed (password changed, login removed) + /// + public virtual string SecurityStamp { get; set; } + + /// + /// PhoneNumber for the user + /// + public virtual string PhoneNumber { get; set; } + + /// + /// True if the phone number is confirmed, default is false + /// + public virtual bool PhoneNumberConfirmed { get; set; } + + /// + /// Is two factor enabled for the user + /// + public virtual bool TwoFactorEnabled { get; set; } + + /// + /// DateTime in UTC when lockout ends, any time in the past is considered not locked out. + /// + public virtual DateTime? LockoutEndDateUtc { get; set; } + + /// + /// DateTime in UTC when the password was last changed. + /// + public virtual DateTime? LastPasswordChangeDateUtc { get; set; } + + /// + /// Is lockout enabled for this user + /// + public virtual bool LockoutEnabled { get; set; } + + /// + /// Used to record failures for the purposes of lockout + /// + public virtual int AccessFailedCount { get; set; } + + /// + /// Navigation property for user roles + /// + public virtual ICollection Roles { get; } + + /// + /// Navigation property for user claims + /// + public virtual ICollection Claims { get; } + + /// + /// Navigation property for user logins + /// + public virtual ICollection Logins { get; } + + /// + /// User ID (Primary Key) + /// + public virtual TKey Id { get; set; } + + /// + /// User name + /// + public virtual string UserName { get; set; } + } +} diff --git a/src/Umbraco.Web.BackOffice/Identity/UserLoginInfoWrapper.cs b/src/Umbraco.Web.BackOffice/Identity/UserLoginInfoWrapper.cs new file mode 100644 index 0000000000..0c6780ae5d --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Identity/UserLoginInfoWrapper.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Identity; +using Umbraco.Core.Models.Identity; + +namespace Umbraco.Web.BackOffice.Identity +{ + internal class UserLoginInfoWrapper : IUserLoginInfo + { + private readonly UserLoginInfo _info; + + public static IUserLoginInfo Wrap(UserLoginInfo info) => new UserLoginInfoWrapper(info); + + private UserLoginInfoWrapper(UserLoginInfo info) + { + _info = info; + } + + public string LoginProvider + { + get => _info.LoginProvider; + set => _info.LoginProvider = value; + } + + public string ProviderKey + { + get => _info.ProviderKey; + set => _info.ProviderKey = value; + } + } +} From 2c78f2f1d74aec9d0be8d668eba450bafd48ff79 Mon Sep 17 00:00:00 2001 From: Scott Brady Date: Thu, 14 May 2020 22:21:19 +0100 Subject: [PATCH 02/13] More migration of back office identity. Blocked by password validator --- .../Extensions/ClaimsPrincipalExtensions.cs | 44 ++ .../Identity/BackOfficeUserManager.cs | 561 ++++++++++++++++++ .../BackOfficeUserPasswordCheckerResult.cs | 12 + .../Identity/BackOfficeUserValidator.cs | 19 + .../IBackOfficeUserPasswordChecker.cs | 24 + .../Identity/IdentityAuditEventArgs.cs | 132 +++++ .../Identity/NopLookupNormalizer.cs | 14 + .../Identity/PasswordSecurity.cs | 223 +++++++ .../Identity/UmbracoBackOfficeIdentity.cs | 232 ++++++++ .../Identity/UserAwarePasswordHasher.cs | 38 ++ 10 files changed, 1299 insertions(+) create mode 100644 src/Umbraco.Web.BackOffice/Extensions/ClaimsPrincipalExtensions.cs create mode 100644 src/Umbraco.Web.BackOffice/Identity/BackOfficeUserManager.cs create mode 100644 src/Umbraco.Web.BackOffice/Identity/BackOfficeUserPasswordCheckerResult.cs create mode 100644 src/Umbraco.Web.BackOffice/Identity/BackOfficeUserValidator.cs create mode 100644 src/Umbraco.Web.BackOffice/Identity/IBackOfficeUserPasswordChecker.cs create mode 100644 src/Umbraco.Web.BackOffice/Identity/IdentityAuditEventArgs.cs create mode 100644 src/Umbraco.Web.BackOffice/Identity/NopLookupNormalizer.cs create mode 100644 src/Umbraco.Web.BackOffice/Identity/PasswordSecurity.cs create mode 100644 src/Umbraco.Web.BackOffice/Identity/UmbracoBackOfficeIdentity.cs create mode 100644 src/Umbraco.Web.BackOffice/Identity/UserAwarePasswordHasher.cs diff --git a/src/Umbraco.Web.BackOffice/Extensions/ClaimsPrincipalExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/ClaimsPrincipalExtensions.cs new file mode 100644 index 0000000000..833522c2c1 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Extensions/ClaimsPrincipalExtensions.cs @@ -0,0 +1,44 @@ +using System; +using System.Linq; +using System.Security.Claims; +using System.Security.Principal; +using Umbraco.Core; +using Umbraco.Web.BackOffice.Identity; + +namespace Umbraco.Extensions +{ + public static class ClaimsPrincipalExtensions + { + /// + /// This will return the current back office identity if the IPrincipal is the correct type + /// + /// + /// + public static UmbracoBackOfficeIdentity GetUmbracoIdentity(this IPrincipal user) + { + //If it's already a UmbracoBackOfficeIdentity + if (user.Identity is UmbracoBackOfficeIdentity backOfficeIdentity) return backOfficeIdentity; + + //Check if there's more than one identity assigned and see if it's a UmbracoBackOfficeIdentity and use that + if (user is ClaimsPrincipal claimsPrincipal) + { + backOfficeIdentity = claimsPrincipal.Identities.OfType().FirstOrDefault(); + if (backOfficeIdentity != null) return backOfficeIdentity; + } + + //Otherwise convert to a UmbracoBackOfficeIdentity if it's auth'd and has the back office session + if (user.Identity is ClaimsIdentity claimsIdentity && claimsIdentity.IsAuthenticated && claimsIdentity.HasClaim(x => x.Type == Constants.Security.SessionIdClaimType)) + { + try + { + return UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity); + } + catch (InvalidOperationException) + { + } + } + + return null; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserManager.cs b/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserManager.cs new file mode 100644 index 0000000000..79da59fce5 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserManager.cs @@ -0,0 +1,561 @@ +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Mapping; +using Umbraco.Core.Security; +using Umbraco.Core.Services; +using Umbraco.Net; + +namespace Umbraco.Web.BackOffice.Identity +{ + public class BackOfficeUserManager : BackOfficeUserManager + { + public const string OwinMarkerKey = "Umbraco.Web.Security.Identity.BackOfficeUserManagerMarker"; + + public BackOfficeUserManager( + IPasswordConfiguration passwordConfiguration, + IIpResolver ipResolver, + IUserStore store, + IOptions optionsAccessor, + IEnumerable> userValidators, + IEnumerable> passwordValidators, + ILookupNormalizer keyNormalizer, + IdentityErrorDescriber errors, + ILogger> logger) + : base(passwordConfiguration, ipResolver, store, optionsAccessor, userValidators, passwordValidators, keyNormalizer, errors, null, logger) + { + InitUserManager(this); + } + + #region Static Create methods + + /// + /// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager + /// + public static BackOfficeUserManager Create( + IUserService userService, + IEntityService entityService, + IExternalLoginService externalLoginService, + IGlobalSettings globalSettings, + UmbracoMapper mapper, + IPasswordConfiguration passwordConfiguration, + IIpResolver ipResolver, + IdentityErrorDescriber errors, + IDataProtectionProvider dataProtectionProvider, + ILogger> logger) + { + var store = new BackOfficeUserStore(userService, entityService, externalLoginService, globalSettings, mapper); + + return Create( + passwordConfiguration, + ipResolver, + store, + errors, + dataProtectionProvider, + logger); + } + + /// + /// Creates a BackOfficeUserManager instance with all default options and a custom BackOfficeUserManager instance + /// + public static BackOfficeUserManager Create( + IPasswordConfiguration passwordConfiguration, + IIpResolver ipResolver, + IUserStore customUserStore, + IdentityErrorDescriber errors, + IDataProtectionProvider dataProtectionProvider, + ILogger> logger) + { + var options = new IdentityOptions(); + + // Configure validation logic for usernames + var userValidators = new List> { new BackOfficeUserValidator() }; + options.User.RequireUniqueEmail = true; + + // Configure validation logic for passwords + var passwordValidators = new List> { new PasswordValidator() }; + options.Password.RequiredLength = passwordConfiguration.RequiredLength; + options.Password.RequireNonAlphanumeric = passwordConfiguration.RequireNonLetterOrDigit; + options.Password.RequireDigit = passwordConfiguration.RequireDigit; + options.Password.RequireLowercase = passwordConfiguration.RequireLowercase; + options.Password.RequireUppercase = passwordConfiguration.RequireUppercase; + + // Ensure Umbraco security stamp claim type is used + options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier; + options.ClaimsIdentity.UserNameClaimType = ClaimTypes.Name; + options.ClaimsIdentity.RoleClaimType = ClaimTypes.Role; + options.ClaimsIdentity.SecurityStampClaimType = Constants.Web.SecurityStampClaimType; + + options.Lockout.AllowedForNewUsers = true; + options.Lockout.MaxFailedAccessAttempts = passwordConfiguration.MaxFailedAccessAttemptsBeforeLockout; + //NOTE: This just needs to be in the future, we currently don't support a lockout timespan, it's either they are locked + // or they are not locked, but this determines what is set on the account lockout date which corresponds to whether they are + // locked out or not. + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromDays(30); + + return new BackOfficeUserManager( + passwordConfiguration, + ipResolver, + customUserStore, + new OptionsWrapper(options), + userValidators, + passwordValidators, + new NopLookupNormalizer(), + errors, + logger); + } + + #endregion + } + + public class BackOfficeUserManager : UserManager + where T : BackOfficeIdentityUser + { + private PasswordGenerator _passwordGenerator; + + public BackOfficeUserManager( + IPasswordConfiguration passwordConfiguration, + IIpResolver ipResolver, + IUserStore store, + IOptions optionsAccessor, + IEnumerable> userValidators, + IEnumerable> passwordValidators, + ILookupNormalizer keyNormalizer, + IdentityErrorDescriber errors, + IServiceProvider services, + ILogger> logger) + : base(store, optionsAccessor, null, userValidators, passwordValidators, keyNormalizer, errors, services, logger) + { + PasswordConfiguration = passwordConfiguration ?? throw new ArgumentNullException(nameof(passwordConfiguration)); + IpResolver = ipResolver ?? throw new ArgumentNullException(nameof(ipResolver)); + } + + #region What we do not currently support + // TODO: We could support this - but a user claims will mostly just be what is in the auth cookie + public override bool SupportsUserClaim => false; + + // TODO: Support this + public override bool SupportsQueryableUsers => false; + + /// + /// Developers will need to override this to support custom 2 factor auth + /// + public override bool SupportsUserTwoFactor => false; + + // TODO: Support this + public override bool SupportsUserPhoneNumber => false; + #endregion + + /// + /// Initializes the user manager with the correct options + /// + protected void InitUserManager(BackOfficeUserManager manager) + { + // use a custom hasher based on our membership provider + PasswordHasher = GetDefaultPasswordHasher(PasswordConfiguration); + } + + /// + /// Used to validate a user's session + /// + /// + /// + /// + public virtual async Task ValidateSessionIdAsync(string userId, string sessionId) + { + var userSessionStore = Store as IUserSessionStore; + //if this is not set, for backwards compat (which would be super rare), we'll just approve it + if (userSessionStore == null) return true; + + return await userSessionStore.ValidateSessionIdAsync(userId, sessionId); + } + + /// + /// This will determine which password hasher to use based on what is defined in config + /// + /// + protected virtual IPasswordHasher GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration) + { + //we can use the user aware password hasher (which will be the default and preferred way) + return new UserAwarePasswordHasher(new PasswordSecurity(passwordConfiguration)); + } + + /// + /// Gets/sets the default back office user password checker + /// + public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get; set; } + public IPasswordConfiguration PasswordConfiguration { get; } + public IIpResolver IpResolver { get; } + + /// + /// Helper method to generate a password for a user based on the current password validator + /// + /// + public string GeneratePassword() + { + if (_passwordGenerator == null) _passwordGenerator = new PasswordGenerator(PasswordConfiguration); + var password = _passwordGenerator.GeneratePassword(); + return password; + } + + /// + /// Override to check the user approval value as well as the user lock out date, by default this only checks the user's locked out date + /// + /// + /// + /// + /// In the ASP.NET Identity world, there is only one value for being locked out, in Umbraco we have 2 so when checking this for Umbraco we need to check both values + /// + public override async Task IsLockedOutAsync(T user) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + + if (user.IsApproved == false) return true; + + return await base.IsLockedOutAsync(user); + } + + #region Overrides for password logic + + /// + /// Logic used to validate a username and password + /// + /// + /// + /// + /// + /// By default this uses the standard ASP.Net Identity approach which is: + /// * Get password store + /// * Call VerifyPasswordAsync with the password store + user + password + /// * Uses the PasswordHasher.VerifyHashedPassword to compare the stored password + /// + /// In some cases people want simple custom control over the username/password check, for simplicity + /// sake, developers would like the users to simply validate against an LDAP directory but the user + /// data remains stored inside of Umbraco. + /// See: http://issues.umbraco.org/issue/U4-7032 for the use cases. + /// + /// We've allowed this check to be overridden with a simple callback so that developers don't actually + /// have to implement/override this class. + /// + public override async Task CheckPasswordAsync(T user, string password) + { + if (BackOfficeUserPasswordChecker != null) + { + var result = await BackOfficeUserPasswordChecker.CheckPasswordAsync(user, password); + + if (user.HasIdentity == false) + { + return false; + } + + //if the result indicates to not fallback to the default, then return true if the credentials are valid + if (result != BackOfficeUserPasswordCheckerResult.FallbackToDefaultChecker) + { + return result == BackOfficeUserPasswordCheckerResult.ValidCredentials; + } + } + + //we cannot proceed if the user passed in does not have an identity + if (user.HasIdentity == false) + return false; + + //use the default behavior + return await base.CheckPasswordAsync(user, password); + } + + /// + /// This is a special method that will reset the password but will raise the Password Changed event instead of the reset event + /// + /// + /// + /// + /// + /// + /// We use this because in the back office the only way an admin can change another user's password without first knowing their password + /// is to generate a token and reset it, however, when we do this we want to track a password change, not a password reset + /// + public async Task ChangePasswordWithResetAsync(int userId, string token, string newPassword) + { + var user = await base.FindByIdAsync(userId.ToString()); + if (user == null) throw new InvalidOperationException("Could not find user"); + + var result = await base.ResetPasswordAsync(user, token, newPassword); + if (result.Succeeded) RaisePasswordChangedEvent(userId); + return result; + } + + public override async Task ChangePasswordAsync(T user, string currentPassword, string newPassword) + { + var result = await base.ChangePasswordAsync(user, currentPassword, newPassword); + if (result.Succeeded) RaisePasswordChangedEvent(user.Id); + return result; + } + + /// + /// Override to determine how to hash the password + /// + /// + /// + /// + /// + /// + /// This method is called anytime the password needs to be hashed for storage (i.e. including when reset password is used) + /// + protected override async Task UpdatePasswordHash(T user, string newPassword, bool validatePassword) + { + user.LastPasswordChangeDateUtc = DateTime.UtcNow; + + if (validatePassword) + { + var validate = await ValidatePasswordAsync(user, newPassword); + if (!validate.Succeeded) + { + return validate; + } + } + + var passwordStore = Store as IUserPasswordStore; + if (passwordStore == null) throw new NotSupportedException("The current user store does not implement " + typeof(IUserPasswordStore<>)); + + var hash = newPassword != null ? PasswordHasher.HashPassword(user, newPassword) : null; + await passwordStore.SetPasswordHashAsync(user, hash, CancellationToken); + await UpdateSecurityStampInternal(user); + return IdentityResult.Success; + } + + /// + /// This is copied from the underlying .NET base class since they decided to not expose it + /// + /// + /// + private async Task UpdateSecurityStampInternal(T user) + { + if (SupportsUserSecurityStamp == false) return; + await GetSecurityStore().SetSecurityStampAsync(user, NewSecurityStamp(), CancellationToken.None); + } + + /// + /// This is copied from the underlying .NET base class since they decided to not expose it + /// + /// + private IUserSecurityStampStore GetSecurityStore() + { + var store = Store as IUserSecurityStampStore; + if (store == null) throw new NotSupportedException("The current user store does not implement " + typeof(IUserSecurityStampStore<>)); + return store; + } + + /// + /// This is copied from the underlying .NET base class since they decided to not expose it + /// + /// + private static string NewSecurityStamp() + { + return Guid.NewGuid().ToString(); + } + + #endregion + + public override async Task SetLockoutEndDateAsync(T user, DateTimeOffset? lockoutEnd) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + + var result = await base.SetLockoutEndDateAsync(user, lockoutEnd); + + // The way we unlock is by setting the lockoutEnd date to the current datetime + if (result.Succeeded && lockoutEnd >= DateTimeOffset.UtcNow) + { + RaiseAccountLockedEvent(user.Id); + } + else + { + RaiseAccountUnlockedEvent(user.Id); + //Resets the login attempt fails back to 0 when unlock is clicked + await ResetAccessFailedCountAsync(user); + } + + return result; + } + + public override async Task ResetAccessFailedCountAsync(T user) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + + var lockoutStore = (IUserLockoutStore)Store; + var accessFailedCount = await GetAccessFailedCountAsync(user); + + if (accessFailedCount == 0) + return IdentityResult.Success; + + await lockoutStore.ResetAccessFailedCountAsync(user, CancellationToken.None); + //raise the event now that it's reset + RaiseResetAccessFailedCountEvent(user.Id); + return await UpdateAsync(user); + } + + /// + /// Overrides the Microsoft ASP.NET user management method + /// + /// + /// + /// returns a Async Task + /// + /// + /// Doesn't set fail attempts back to 0 + /// + public override async Task AccessFailedAsync(T user) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + + var lockoutStore = Store as IUserLockoutStore; + if (lockoutStore == null) throw new NotSupportedException("The current user store does not implement " + typeof(IUserLockoutStore<>)); + + var count = await lockoutStore.IncrementAccessFailedCountAsync(user, CancellationToken.None); + + if (count >= Options.Lockout.MaxFailedAccessAttempts) + { + await lockoutStore.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.Add(Options.Lockout.DefaultLockoutTimeSpan), + CancellationToken.None); + //NOTE: in normal aspnet identity this would do set the number of failed attempts back to 0 + //here we are persisting the value for the back office + } + + var result = await UpdateAsync(user); + + //Slightly confusing: this will return a Success if we successfully update the AccessFailed count + if (result.Succeeded) RaiseLoginFailedEvent(user.Id); + + return result; + } + + internal void RaiseAccountLockedEvent(int userId) + { + OnAccountLocked(new IdentityAuditEventArgs(AuditEvent.AccountLocked, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); + } + + internal void RaiseAccountUnlockedEvent(int userId) + { + OnAccountUnlocked(new IdentityAuditEventArgs(AuditEvent.AccountUnlocked, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); + } + + internal void RaiseForgotPasswordRequestedEvent(int userId) + { + OnForgotPasswordRequested(new IdentityAuditEventArgs(AuditEvent.ForgotPasswordRequested, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); + } + + internal void RaiseForgotPasswordChangedSuccessEvent(int userId) + { + OnForgotPasswordChangedSuccess(new IdentityAuditEventArgs(AuditEvent.ForgotPasswordChangedSuccess, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); + } + + internal void RaiseLoginFailedEvent(int userId) + { + OnLoginFailed(new IdentityAuditEventArgs(AuditEvent.LoginFailed, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); + } + + internal void RaiseInvalidLoginAttemptEvent(string username) + { + OnLoginFailed(new IdentityAuditEventArgs(AuditEvent.LoginFailed, IpResolver.GetCurrentRequestIpAddress(), username, string.Format("Attempted login for username '{0}' failed", username))); + } + + internal void RaiseLoginRequiresVerificationEvent(int userId) + { + OnLoginRequiresVerification(new IdentityAuditEventArgs(AuditEvent.LoginRequiresVerification, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); + } + + internal void RaiseLoginSuccessEvent(int userId) + { + OnLoginSuccess(new IdentityAuditEventArgs(AuditEvent.LoginSucces, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); + } + + internal void RaiseLogoutSuccessEvent(int userId) + { + OnLogoutSuccess(new IdentityAuditEventArgs(AuditEvent.LogoutSuccess, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); + } + + internal void RaisePasswordChangedEvent(int userId) + { + OnPasswordChanged(new IdentityAuditEventArgs(AuditEvent.PasswordChanged, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); + } + + internal void RaiseResetAccessFailedCountEvent(int userId) + { + OnResetAccessFailedCount(new IdentityAuditEventArgs(AuditEvent.ResetAccessFailedCount, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); + } + + public static event EventHandler AccountLocked; + public static event EventHandler AccountUnlocked; + public static event EventHandler ForgotPasswordRequested; + public static event EventHandler ForgotPasswordChangedSuccess; + public static event EventHandler LoginFailed; + public static event EventHandler LoginRequiresVerification; + public static event EventHandler LoginSuccess; + public static event EventHandler LogoutSuccess; + public static event EventHandler PasswordChanged; + public static event EventHandler PasswordReset; + public static event EventHandler ResetAccessFailedCount; + + protected virtual void OnAccountLocked(IdentityAuditEventArgs e) + { + if (AccountLocked != null) AccountLocked(this, e); + } + + protected virtual void OnAccountUnlocked(IdentityAuditEventArgs e) + { + if (AccountUnlocked != null) AccountUnlocked(this, e); + } + + protected virtual void OnForgotPasswordRequested(IdentityAuditEventArgs e) + { + if (ForgotPasswordRequested != null) ForgotPasswordRequested(this, e); + } + + protected virtual void OnForgotPasswordChangedSuccess(IdentityAuditEventArgs e) + { + if (ForgotPasswordChangedSuccess != null) ForgotPasswordChangedSuccess(this, e); + } + + protected virtual void OnLoginFailed(IdentityAuditEventArgs e) + { + if (LoginFailed != null) LoginFailed(this, e); + } + + protected virtual void OnLoginRequiresVerification(IdentityAuditEventArgs e) + { + if (LoginRequiresVerification != null) LoginRequiresVerification(this, e); + } + + protected virtual void OnLoginSuccess(IdentityAuditEventArgs e) + { + if (LoginSuccess != null) LoginSuccess(this, e); + } + + protected virtual void OnLogoutSuccess(IdentityAuditEventArgs e) + { + if (LogoutSuccess != null) LogoutSuccess(this, e); + } + + protected virtual void OnPasswordChanged(IdentityAuditEventArgs e) + { + if (PasswordChanged != null) PasswordChanged(this, e); + } + + protected virtual void OnPasswordReset(IdentityAuditEventArgs e) + { + if (PasswordReset != null) PasswordReset(this, e); + } + + protected virtual void OnResetAccessFailedCount(IdentityAuditEventArgs e) + { + if (ResetAccessFailedCount != null) ResetAccessFailedCount(this, e); + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserPasswordCheckerResult.cs b/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserPasswordCheckerResult.cs new file mode 100644 index 0000000000..72c0707115 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserPasswordCheckerResult.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Web.BackOffice.Identity +{ + /// + /// The result returned from the IBackOfficeUserPasswordChecker + /// + public enum BackOfficeUserPasswordCheckerResult + { + ValidCredentials, + InvalidCredentials, + FallbackToDefaultChecker + } +} diff --git a/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserValidator.cs b/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserValidator.cs new file mode 100644 index 0000000000..f80d8b90db --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserValidator.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; + +namespace Umbraco.Web.BackOffice.Identity +{ + public class BackOfficeUserValidator : UserValidator + where T : BackOfficeIdentityUser + { + public override async Task ValidateAsync(UserManager manager, T user) + { + // Don't validate if the user's email or username hasn't changed otherwise it's just wasting SQL queries. + if (user.IsPropertyDirty("Email") || user.IsPropertyDirty("UserName")) + { + return await base.ValidateAsync(manager, user); + } + return IdentityResult.Success; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Identity/IBackOfficeUserPasswordChecker.cs b/src/Umbraco.Web.BackOffice/Identity/IBackOfficeUserPasswordChecker.cs new file mode 100644 index 0000000000..b94e261506 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Identity/IBackOfficeUserPasswordChecker.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; + +namespace Umbraco.Web.BackOffice.Identity +{ + /// + /// Used by the BackOfficeUserManager to check the username/password which allows for developers to more easily + /// set the logic for this procedure. + /// + public interface IBackOfficeUserPasswordChecker + { + /// + /// Checks a password for a user + /// + /// + /// + /// + /// + /// This will allow a developer to auto-link a local account which is required if the user queried doesn't exist locally. + /// The user parameter will always contain the username, if the user doesn't exist locally, the other properties will not be filled in. + /// A developer can then create a local account by filling in the properties and using UserManager.CreateAsync + /// + Task CheckPasswordAsync(BackOfficeIdentityUser user, string password); + } +} diff --git a/src/Umbraco.Web.BackOffice/Identity/IdentityAuditEventArgs.cs b/src/Umbraco.Web.BackOffice/Identity/IdentityAuditEventArgs.cs new file mode 100644 index 0000000000..cb0b04ef56 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Identity/IdentityAuditEventArgs.cs @@ -0,0 +1,132 @@ +using System; +using System.Threading; +using Umbraco.Extensions; + + +namespace Umbraco.Web.BackOffice.Identity +{ + /// + /// This class is used by events raised from the BackofficeUserManager + /// + public class IdentityAuditEventArgs : EventArgs + { + /// + /// The action that got triggered from the audit event + /// + public AuditEvent Action { get; private set; } + + /// + /// Current date/time in UTC format + /// + public DateTime DateTimeUtc { get; private set; } + + /// + /// The source IP address of the user performing the action + /// + public string IpAddress { get; private set; } + + /// + /// The user affected by the event raised + /// + public int AffectedUser { get; private set; } + + /// + /// If a user is performing an action on a different user, then this will be set. Otherwise it will be -1 + /// + public int PerformingUser { get; private set; } + + /// + /// An optional comment about the action being logged + /// + public string Comment { get; private set; } + + /// + /// This property is always empty except in the LoginFailed event for an unknown user trying to login + /// + public string Username { get; private set; } + + + /// + /// Default constructor + /// + /// + /// + /// + /// + /// + public IdentityAuditEventArgs(AuditEvent action, string ipAddress, string comment = null, int performingUser = -1, int affectedUser = -1) + { + DateTimeUtc = DateTime.UtcNow; + Action = action; + + IpAddress = ipAddress; + Comment = comment; + AffectedUser = affectedUser; + + PerformingUser = performingUser == -1 + ? GetCurrentRequestBackofficeUserId() + : performingUser; + } + + /// + /// Creates an instance without a performing or affected user (the id will be set to -1) + /// + /// + /// + /// + /// + public IdentityAuditEventArgs(AuditEvent action, string ipAddress, string username, string comment) + { + DateTimeUtc = DateTime.UtcNow; + Action = action; + + IpAddress = ipAddress; + Username = username; + Comment = comment; + + PerformingUser = -1; + } + + public IdentityAuditEventArgs(AuditEvent action, string ipAddress, string username, string comment, int performingUser) + { + DateTimeUtc = DateTime.UtcNow; + Action = action; + + IpAddress = ipAddress; + Username = username; + Comment = comment; + + PerformingUser = performingUser == -1 + ? GetCurrentRequestBackofficeUserId() + : performingUser; + } + + /// + /// Returns the current logged in backoffice user's Id logging if there is one + /// + /// + protected int GetCurrentRequestBackofficeUserId() + { + var userId = -1; + var backOfficeIdentity = Thread.CurrentPrincipal.GetUmbracoIdentity(); + if (backOfficeIdentity != null) + int.TryParse(backOfficeIdentity.Id.ToString(), out userId); + return userId; + } + } + + public enum AuditEvent + { + AccountLocked, + AccountUnlocked, + ForgotPasswordRequested, + ForgotPasswordChangedSuccess, + LoginFailed, + LoginRequiresVerification, + LoginSucces, + LogoutSuccess, + PasswordChanged, + PasswordReset, + ResetAccessFailedCount + } +} diff --git a/src/Umbraco.Web.BackOffice/Identity/NopLookupNormalizer.cs b/src/Umbraco.Web.BackOffice/Identity/NopLookupNormalizer.cs new file mode 100644 index 0000000000..c846498098 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Identity/NopLookupNormalizer.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Identity; + +namespace Umbraco.Web.BackOffice.Identity +{ + /// + /// No-op lookup normalizer to maintain compatibility with ASP.NET Identity 2 + /// + public class NopLookupNormalizer : ILookupNormalizer + { + public string NormalizeName(string name) => name; + + public string NormalizeEmail(string email) => email; + } +} diff --git a/src/Umbraco.Web.BackOffice/Identity/PasswordSecurity.cs b/src/Umbraco.Web.BackOffice/Identity/PasswordSecurity.cs new file mode 100644 index 0000000000..49405d0c5a --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Identity/PasswordSecurity.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Security; + +namespace Umbraco.Web.BackOffice.Identity +{ + /// + /// Handles password hashing and formatting + /// + public class PasswordSecurity + { + public IPasswordConfiguration PasswordConfiguration { get; } + public PasswordGenerator _generator; + public ConfiguredPasswordValidator _validator; + + /// + /// Constructor + /// + /// + public PasswordSecurity(IPasswordConfiguration passwordConfiguration) + { + PasswordConfiguration = passwordConfiguration; + } + + /// + /// Checks if the password passes validation rules + /// + /// + /// + public async Task>> IsValidPasswordAsync(string password) + { + if (_validator == null) + _validator = new ConfiguredPasswordValidator(PasswordConfiguration); + var result = await _validator.ValidateAsync(password); + if (result.Succeeded) + return Attempt>.Succeed(); + + return Attempt>.Fail(result.Errors); + } + + public string GeneratePassword() + { + if (_generator == null) + _generator = new PasswordGenerator(PasswordConfiguration); + return _generator.GeneratePassword(); + } + + /// + /// Returns a hashed password value used to store in a data store + /// + /// + /// + public string HashPasswordForStorage(string password) + { + if (string.IsNullOrWhiteSpace(password)) + throw new ArgumentException("password cannot be empty", nameof(password)); + + string salt; + var hashed = HashNewPassword(password, out salt); + return FormatPasswordForStorage(hashed, salt); + } + + /// + /// If the password format is a hashed keyed algorithm then we will pre-pend the salt used to hash the password + /// to the hashed password itself. + /// + /// + /// + /// + public string FormatPasswordForStorage(string hashedPassword, string salt) + { + return salt + hashedPassword; + } + + /// + /// Hashes a password with a given salt + /// + /// + /// + /// + public string HashPassword(string pass, string salt) + { + //This is the correct way to implement this (as per the sql membership provider) + + var bytes = Encoding.Unicode.GetBytes(pass); + var saltBytes = Convert.FromBase64String(salt); + byte[] inArray; + + var hashAlgorithm = GetHashAlgorithm(pass); + var algorithm = hashAlgorithm as KeyedHashAlgorithm; + if (algorithm != null) + { + var keyedHashAlgorithm = algorithm; + if (keyedHashAlgorithm.Key.Length == saltBytes.Length) + { + //if the salt bytes is the required key length for the algorithm, use it as-is + keyedHashAlgorithm.Key = saltBytes; + } + else if (keyedHashAlgorithm.Key.Length < saltBytes.Length) + { + //if the salt bytes is too long for the required key length for the algorithm, reduce it + var numArray2 = new byte[keyedHashAlgorithm.Key.Length]; + Buffer.BlockCopy(saltBytes, 0, numArray2, 0, numArray2.Length); + keyedHashAlgorithm.Key = numArray2; + } + else + { + //if the salt bytes is too short for the required key length for the algorithm, extend it + var numArray2 = new byte[keyedHashAlgorithm.Key.Length]; + var dstOffset = 0; + while (dstOffset < numArray2.Length) + { + var count = Math.Min(saltBytes.Length, numArray2.Length - dstOffset); + Buffer.BlockCopy(saltBytes, 0, numArray2, dstOffset, count); + dstOffset += count; + } + keyedHashAlgorithm.Key = numArray2; + } + inArray = keyedHashAlgorithm.ComputeHash(bytes); + } + else + { + var buffer = new byte[saltBytes.Length + bytes.Length]; + Buffer.BlockCopy(saltBytes, 0, buffer, 0, saltBytes.Length); + Buffer.BlockCopy(bytes, 0, buffer, saltBytes.Length, bytes.Length); + inArray = hashAlgorithm.ComputeHash(buffer); + } + + return Convert.ToBase64String(inArray); + } + + /// + /// Verifies if the password matches the expected hash+salt of the stored password string + /// + /// The password. + /// The value of the password stored in a data store. + /// + public bool VerifyPassword(string password, string dbPassword) + { + if (string.IsNullOrWhiteSpace(dbPassword)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(dbPassword)); + + if (dbPassword.StartsWith(Constants.Security.EmptyPasswordPrefix)) + return false; + + var storedHashedPass = ParseStoredHashPassword(dbPassword, out var salt); + var hashed = HashPassword(password, salt); + return storedHashedPass == hashed; + } + + /// + /// Create a new password hash and a new salt + /// + /// + /// + /// + public string HashNewPassword(string newPassword, out string salt) + { + salt = GenerateSalt(); + return HashPassword(newPassword, salt); + } + + /// + /// Parses out the hashed password and the salt from the stored password string value + /// + /// + /// returns the salt + /// + public string ParseStoredHashPassword(string storedString, out string salt) + { + if (string.IsNullOrWhiteSpace(storedString)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(storedString)); + + var saltLen = GenerateSalt(); + salt = storedString.Substring(0, saltLen.Length); + return storedString.Substring(saltLen.Length); + } + + public static string GenerateSalt() + { + var numArray = new byte[16]; + new RNGCryptoServiceProvider().GetBytes(numArray); + return Convert.ToBase64String(numArray); + } + + /// + /// Return the hash algorithm to use + /// + /// + /// + public HashAlgorithm GetHashAlgorithm(string password) + { + if (PasswordConfiguration.HashAlgorithmType.IsNullOrWhiteSpace()) + throw new InvalidOperationException("No hash algorithm type specified"); + + var alg = HashAlgorithm.Create(PasswordConfiguration.HashAlgorithmType); + if (alg == null) + throw new InvalidOperationException($"The hash algorithm specified {PasswordConfiguration.HashAlgorithmType} cannot be resolved"); + + return alg; + } + + /// + /// Encodes the password. + /// + /// The password. + /// The encoded password. + private string LegacyEncodePassword(string password) + { + var hashAlgorith = GetHashAlgorithm(password); + var encodedPassword = Convert.ToBase64String(hashAlgorith.ComputeHash(Encoding.Unicode.GetBytes(password))); + return encodedPassword; + } + + + + + + } +} diff --git a/src/Umbraco.Web.BackOffice/Identity/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Web.BackOffice/Identity/UmbracoBackOfficeIdentity.cs new file mode 100644 index 0000000000..ca3e6d742f --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Identity/UmbracoBackOfficeIdentity.cs @@ -0,0 +1,232 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Umbraco.Core; + +namespace Umbraco.Web.BackOffice.Identity +{ + + /// + /// A custom user identity for the Umbraco backoffice + /// + /// + /// This inherits from FormsIdentity for backwards compatibility reasons since we still support the forms auth cookie, in v8 we can + /// change over to 'pure' asp.net identity and just inherit from ClaimsIdentity. + /// + [Serializable] + public class UmbracoBackOfficeIdentity : ClaimsIdentity + { + public static UmbracoBackOfficeIdentity FromClaimsIdentity(ClaimsIdentity identity) + { + return new UmbracoBackOfficeIdentity(identity); + } + + /// + /// Creates a new UmbracoBackOfficeIdentity + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public UmbracoBackOfficeIdentity(int userId, string username, string realName, + IEnumerable startContentNodes, IEnumerable startMediaNodes, string culture, + string sessionId, string securityStamp, IEnumerable allowedApps, IEnumerable roles) + : base(Enumerable.Empty(), Constants.Security.BackOfficeAuthenticationType) //this ctor is used to ensure the IsAuthenticated property is true + { + if (allowedApps == null) throw new ArgumentNullException(nameof(allowedApps)); + if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); + if (string.IsNullOrWhiteSpace(realName)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(realName)); + if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture)); + if (string.IsNullOrWhiteSpace(sessionId)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(sessionId)); + if (string.IsNullOrWhiteSpace(securityStamp)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(securityStamp)); + AddRequiredClaims(userId, username, realName, startContentNodes, startMediaNodes, culture, sessionId, securityStamp, allowedApps, roles); + } + + /// + /// Creates a new UmbracoBackOfficeIdentity + /// + /// + /// The original identity created by the ClaimsIdentityFactory + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public UmbracoBackOfficeIdentity(ClaimsIdentity childIdentity, + int userId, string username, string realName, + IEnumerable startContentNodes, IEnumerable startMediaNodes, string culture, + string sessionId, string securityStamp, IEnumerable allowedApps, IEnumerable roles) + : base(childIdentity.Claims, Constants.Security.BackOfficeAuthenticationType) + { + if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); + if (string.IsNullOrWhiteSpace(realName)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(realName)); + if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture)); + if (string.IsNullOrWhiteSpace(sessionId)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(sessionId)); + if (string.IsNullOrWhiteSpace(securityStamp)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(securityStamp)); + Actor = childIdentity; + AddRequiredClaims(userId, username, realName, startContentNodes, startMediaNodes, culture, sessionId, securityStamp, allowedApps, roles); + } + + /// + /// Create a back office identity based on an existing claims identity + /// + /// + private UmbracoBackOfficeIdentity(ClaimsIdentity identity) + : base(identity.Claims, Constants.Security.BackOfficeAuthenticationType) + { + Actor = identity; + + //validate that all claims exist + foreach (var t in RequiredBackOfficeIdentityClaimTypes) + { + //if the identity doesn't have the claim, or the claim value is null + if (identity.HasClaim(x => x.Type == t) == false || identity.HasClaim(x => x.Type == t && x.Value.IsNullOrWhiteSpace())) + { + throw new InvalidOperationException("Cannot create a " + typeof(UmbracoBackOfficeIdentity) + " from " + typeof(ClaimsIdentity) + " since the required claim " + t + " is missing"); + } + } + } + + public const string Issuer = Constants.Security.BackOfficeAuthenticationType; + + /// + /// Returns the required claim types for a back office identity + /// + /// + /// This does not include the role claim type or allowed apps type since that is a collection and in theory could be empty + /// + public static IEnumerable RequiredBackOfficeIdentityClaimTypes => new[] + { + ClaimTypes.NameIdentifier, //id + ClaimTypes.Name, //username + ClaimTypes.GivenName, + Constants.Security.StartContentNodeIdClaimType, + Constants.Security.StartMediaNodeIdClaimType, + ClaimTypes.Locality, + Constants.Security.SessionIdClaimType, + Constants.Web.SecurityStampClaimType + }; + + /// + /// Adds claims based on the ctor data + /// + private void AddRequiredClaims(int userId, string username, string realName, + IEnumerable startContentNodes, IEnumerable startMediaNodes, string culture, + string sessionId, string securityStamp, IEnumerable allowedApps, IEnumerable roles) + { + //This is the id that 'identity' uses to check for the user id + if (HasClaim(x => x.Type == ClaimTypes.NameIdentifier) == false) + AddClaim(new Claim(ClaimTypes.NameIdentifier, userId.ToInvariantString(), ClaimValueTypes.Integer32, Issuer, Issuer, this)); + + if (HasClaim(x => x.Type == ClaimTypes.Name) == false) + AddClaim(new Claim(ClaimTypes.Name, username, ClaimValueTypes.String, Issuer, Issuer, this)); + + if (HasClaim(x => x.Type == ClaimTypes.GivenName) == false) + AddClaim(new Claim(ClaimTypes.GivenName, realName, ClaimValueTypes.String, Issuer, Issuer, this)); + + if (HasClaim(x => x.Type == Constants.Security.StartContentNodeIdClaimType) == false && startContentNodes != null) + { + foreach (var startContentNode in startContentNodes) + { + AddClaim(new Claim(Constants.Security.StartContentNodeIdClaimType, startContentNode.ToInvariantString(), ClaimValueTypes.Integer32, Issuer, Issuer, this)); + } + } + + if (HasClaim(x => x.Type == Constants.Security.StartMediaNodeIdClaimType) == false && startMediaNodes != null) + { + foreach (var startMediaNode in startMediaNodes) + { + AddClaim(new Claim(Constants.Security.StartMediaNodeIdClaimType, startMediaNode.ToInvariantString(), ClaimValueTypes.Integer32, Issuer, Issuer, this)); + } + } + + if (HasClaim(x => x.Type == ClaimTypes.Locality) == false) + AddClaim(new Claim(ClaimTypes.Locality, culture, ClaimValueTypes.String, Issuer, Issuer, this)); + + if (HasClaim(x => x.Type == Constants.Security.SessionIdClaimType) == false && SessionId.IsNullOrWhiteSpace() == false) + AddClaim(new Claim(Constants.Security.SessionIdClaimType, sessionId, ClaimValueTypes.String, Issuer, Issuer, this)); + + //The security stamp claim is also required... this is because this claim type is hard coded + // by the SecurityStampValidator, see: https://katanaproject.codeplex.com/workitem/444 + if (HasClaim(x => x.Type == Constants.Web.SecurityStampClaimType) == false) + AddClaim(new Claim(Constants.Web.SecurityStampClaimType, securityStamp, ClaimValueTypes.String, Issuer, Issuer, this)); + + //Add each app as a separate claim + if (HasClaim(x => x.Type == Constants.Security.AllowedApplicationsClaimType) == false && allowedApps != null) + { + foreach (var application in allowedApps) + { + AddClaim(new Claim(Constants.Security.AllowedApplicationsClaimType, application, ClaimValueTypes.String, Issuer, Issuer, this)); + } + } + + //Claims are added by the ClaimsIdentityFactory because our UserStore supports roles, however this identity might + // not be made with that factory if it was created with a different ticket so perform the check + if (HasClaim(x => x.Type == DefaultRoleClaimType) == false && roles != null) + { + //manually add them + foreach (var roleName in roles) + { + AddClaim(new Claim(RoleClaimType, roleName, ClaimValueTypes.String, Issuer, Issuer, this)); + } + } + + } + + /// + /// + /// Gets the type of authenticated identity. + /// + /// + /// The type of authenticated identity. This property always returns "UmbracoBackOffice". + /// + public override string AuthenticationType => Issuer; + + private int[] _startContentNodes; + public int[] StartContentNodes => _startContentNodes ?? (_startContentNodes = FindAll(x => x.Type == Constants.Security.StartContentNodeIdClaimType).Select(app => int.TryParse(app.Value, out var i) ? i : default).Where(x => x != default).ToArray()); + + private int[] _startMediaNodes; + public int[] StartMediaNodes => _startMediaNodes ?? (_startMediaNodes = FindAll(x => x.Type == Constants.Security.StartMediaNodeIdClaimType).Select(app => int.TryParse(app.Value, out var i) ? i : default).Where(x => x != default).ToArray()); + + private string[] _allowedApplications; + public string[] AllowedApplications => _allowedApplications ?? (_allowedApplications = FindAll(x => x.Type == Constants.Security.AllowedApplicationsClaimType).Select(app => app.Value).ToArray()); + + public int Id => int.Parse(this.FindFirstValue(ClaimTypes.NameIdentifier)); + + public string RealName => this.FindFirstValue(ClaimTypes.GivenName); + + public string Username => this.FindFirstValue(ClaimTypes.Name); + + public string Culture => this.FindFirstValue(ClaimTypes.Locality); + + public string SessionId + { + get => this.FindFirstValue(Constants.Security.SessionIdClaimType); + set + { + var existing = FindFirst(Constants.Security.SessionIdClaimType); + if (existing != null) + TryRemoveClaim(existing); + AddClaim(new Claim(Constants.Security.SessionIdClaimType, value, ClaimValueTypes.String, Issuer, Issuer, this)); + } + } + + public string SecurityStamp => this.FindFirstValue(Constants.Web.SecurityStampClaimType); + + public string[] Roles => this.FindAll(x => x.Type == DefaultRoleClaimType).Select(role => role.Value).ToArray(); + + } +} diff --git a/src/Umbraco.Web.BackOffice/Identity/UserAwarePasswordHasher.cs b/src/Umbraco.Web.BackOffice/Identity/UserAwarePasswordHasher.cs new file mode 100644 index 0000000000..fb2b1e71a0 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Identity/UserAwarePasswordHasher.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Identity; + +namespace Umbraco.Web.BackOffice.Identity +{ + public class UserAwarePasswordHasher : IPasswordHasher + where T : BackOfficeIdentityUser + { + private readonly PasswordSecurity _passwordSecurity; + + public UserAwarePasswordHasher(PasswordSecurity passwordSecurity) + { + _passwordSecurity = passwordSecurity; + } + + public string HashPassword(string password) + { + return _passwordSecurity.HashPasswordForStorage(password); + } + + public string HashPassword(T user, string password) + { + // TODO: Implement the logic for this, we need to lookup the password format for the user and hash accordingly: http://issues.umbraco.org/issue/U4-10089 + //NOTE: For now this just falls back to the hashing we are currently using + + return HashPassword(password); + } + + public PasswordVerificationResult VerifyHashedPassword(T user, string hashedPassword, string providedPassword) + { + // TODO: Implement the logic for this, we need to lookup the password format for the user and hash accordingly: http://issues.umbraco.org/issue/U4-10089 + //NOTE: For now this just falls back to the hashing we are currently using + + return _passwordSecurity.VerifyPassword(providedPassword, hashedPassword) + ? PasswordVerificationResult.Success + : PasswordVerificationResult.Failed; + } + } +} From c9f97065234c76f339ab3ccd4482f17956b4ab7d Mon Sep 17 00:00:00 2001 From: Scott Brady Date: Fri, 15 May 2020 15:21:15 +0100 Subject: [PATCH 03/13] Initial DI attempt --- .../Umbraco.Tests.UnitTests.csproj | 13 +- ...kOfficeServiceCollectionExtensionsTests.cs | 30 +++ .../Controllers/BackOfficeController.cs | 1 + ...oBackOfficeApplicationBuilderExtensions.cs | 1 - ...coBackOfficeServiceCollectionExtensions.cs | 20 ++ .../Identity/BackOfficeUserManager.cs | 2 +- .../Identity/PasswordSecurity.cs | 223 ------------------ .../Identity/UserAwarePasswordHasher.cs | 38 --- src/Umbraco.Web.UI.NetCore/Startup.cs | 5 +- 9 files changed, 57 insertions(+), 276 deletions(-) create mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensionsTests.cs create mode 100644 src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs delete mode 100644 src/Umbraco.Web.BackOffice/Identity/PasswordSecurity.cs delete mode 100644 src/Umbraco.Web.BackOffice/Identity/UserAwarePasswordHasher.cs diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj index f2cb3489cc..498467bf56 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj @@ -6,20 +6,9 @@ false - - - - - - - - - - - - + diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000000..677e919c19 --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensionsTests.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using NUnit.Framework; +using Umbraco.Extensions; +using Umbraco.Web.BackOffice.Identity; + +namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Extensions +{ + [TestFixture] + public class UmbracoBackOfficeServiceCollectionExtensionsTests + { + [Test] + public void AddUmbracoBackOfficeIdentity_ExpectBackOfficeUserStoreResolvable() + { + var services = new ServiceCollection(); + + var mockEnvironment = new Mock(); + + services.AddUmbracoCore(mockEnvironment.Object); + services.AddUmbracoBackOfficeIdentity(); + + var serviceProvider = services.BuildServiceProvider(); + + var userStore = serviceProvider.GetService>(); + Assert.AreEqual(typeof(BackOfficeUserStore), userStore.GetType()); + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs index d776749e6b..bce46b3129 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs @@ -14,6 +14,7 @@ using Umbraco.Core.WebAssets; using Umbraco.Net; using Umbraco.Web.BackOffice.ActionResults; using Umbraco.Web.BackOffice.Filters; +using Umbraco.Web.BackOffice.Identity; using Umbraco.Web.Common.ActionResults; using Umbraco.Web.WebAssets; diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeApplicationBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeApplicationBuilderExtensions.cs index d1cec6ea3d..45a6cecd92 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeApplicationBuilderExtensions.cs @@ -1,7 +1,6 @@ using System; using Microsoft.AspNetCore.Builder; using SixLabors.ImageSharp.Web.DependencyInjection; -using Umbraco.Extensions; namespace Umbraco.Extensions { diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs new file mode 100644 index 0000000000..bb0de07d37 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Core.Mapping; +using Umbraco.Web.BackOffice.Identity; + +namespace Umbraco.Extensions +{ + public static class UmbracoBackOfficeServiceCollectionExtensions + { + public static void AddUmbracoBackOfficeIdentity(this IServiceCollection services) + { + services.AddIdentity() + .AddDefaultTokenProviders() + .AddUserStore(); + + // .AddClaimsPrincipalFactory>() // TODO: extract custom claims principal factory + // .AddUserManager>() + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserManager.cs b/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserManager.cs index 79da59fce5..a45ca9ed6e 100644 --- a/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserManager.cs +++ b/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserManager.cs @@ -185,7 +185,7 @@ namespace Umbraco.Web.BackOffice.Identity protected virtual IPasswordHasher GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration) { //we can use the user aware password hasher (which will be the default and preferred way) - return new UserAwarePasswordHasher(new PasswordSecurity(passwordConfiguration)); + return new PasswordHasher(); } /// diff --git a/src/Umbraco.Web.BackOffice/Identity/PasswordSecurity.cs b/src/Umbraco.Web.BackOffice/Identity/PasswordSecurity.cs deleted file mode 100644 index 49405d0c5a..0000000000 --- a/src/Umbraco.Web.BackOffice/Identity/PasswordSecurity.cs +++ /dev/null @@ -1,223 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Security.Cryptography; -using System.Text; -using System.Threading.Tasks; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Security; - -namespace Umbraco.Web.BackOffice.Identity -{ - /// - /// Handles password hashing and formatting - /// - public class PasswordSecurity - { - public IPasswordConfiguration PasswordConfiguration { get; } - public PasswordGenerator _generator; - public ConfiguredPasswordValidator _validator; - - /// - /// Constructor - /// - /// - public PasswordSecurity(IPasswordConfiguration passwordConfiguration) - { - PasswordConfiguration = passwordConfiguration; - } - - /// - /// Checks if the password passes validation rules - /// - /// - /// - public async Task>> IsValidPasswordAsync(string password) - { - if (_validator == null) - _validator = new ConfiguredPasswordValidator(PasswordConfiguration); - var result = await _validator.ValidateAsync(password); - if (result.Succeeded) - return Attempt>.Succeed(); - - return Attempt>.Fail(result.Errors); - } - - public string GeneratePassword() - { - if (_generator == null) - _generator = new PasswordGenerator(PasswordConfiguration); - return _generator.GeneratePassword(); - } - - /// - /// Returns a hashed password value used to store in a data store - /// - /// - /// - public string HashPasswordForStorage(string password) - { - if (string.IsNullOrWhiteSpace(password)) - throw new ArgumentException("password cannot be empty", nameof(password)); - - string salt; - var hashed = HashNewPassword(password, out salt); - return FormatPasswordForStorage(hashed, salt); - } - - /// - /// If the password format is a hashed keyed algorithm then we will pre-pend the salt used to hash the password - /// to the hashed password itself. - /// - /// - /// - /// - public string FormatPasswordForStorage(string hashedPassword, string salt) - { - return salt + hashedPassword; - } - - /// - /// Hashes a password with a given salt - /// - /// - /// - /// - public string HashPassword(string pass, string salt) - { - //This is the correct way to implement this (as per the sql membership provider) - - var bytes = Encoding.Unicode.GetBytes(pass); - var saltBytes = Convert.FromBase64String(salt); - byte[] inArray; - - var hashAlgorithm = GetHashAlgorithm(pass); - var algorithm = hashAlgorithm as KeyedHashAlgorithm; - if (algorithm != null) - { - var keyedHashAlgorithm = algorithm; - if (keyedHashAlgorithm.Key.Length == saltBytes.Length) - { - //if the salt bytes is the required key length for the algorithm, use it as-is - keyedHashAlgorithm.Key = saltBytes; - } - else if (keyedHashAlgorithm.Key.Length < saltBytes.Length) - { - //if the salt bytes is too long for the required key length for the algorithm, reduce it - var numArray2 = new byte[keyedHashAlgorithm.Key.Length]; - Buffer.BlockCopy(saltBytes, 0, numArray2, 0, numArray2.Length); - keyedHashAlgorithm.Key = numArray2; - } - else - { - //if the salt bytes is too short for the required key length for the algorithm, extend it - var numArray2 = new byte[keyedHashAlgorithm.Key.Length]; - var dstOffset = 0; - while (dstOffset < numArray2.Length) - { - var count = Math.Min(saltBytes.Length, numArray2.Length - dstOffset); - Buffer.BlockCopy(saltBytes, 0, numArray2, dstOffset, count); - dstOffset += count; - } - keyedHashAlgorithm.Key = numArray2; - } - inArray = keyedHashAlgorithm.ComputeHash(bytes); - } - else - { - var buffer = new byte[saltBytes.Length + bytes.Length]; - Buffer.BlockCopy(saltBytes, 0, buffer, 0, saltBytes.Length); - Buffer.BlockCopy(bytes, 0, buffer, saltBytes.Length, bytes.Length); - inArray = hashAlgorithm.ComputeHash(buffer); - } - - return Convert.ToBase64String(inArray); - } - - /// - /// Verifies if the password matches the expected hash+salt of the stored password string - /// - /// The password. - /// The value of the password stored in a data store. - /// - public bool VerifyPassword(string password, string dbPassword) - { - if (string.IsNullOrWhiteSpace(dbPassword)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(dbPassword)); - - if (dbPassword.StartsWith(Constants.Security.EmptyPasswordPrefix)) - return false; - - var storedHashedPass = ParseStoredHashPassword(dbPassword, out var salt); - var hashed = HashPassword(password, salt); - return storedHashedPass == hashed; - } - - /// - /// Create a new password hash and a new salt - /// - /// - /// - /// - public string HashNewPassword(string newPassword, out string salt) - { - salt = GenerateSalt(); - return HashPassword(newPassword, salt); - } - - /// - /// Parses out the hashed password and the salt from the stored password string value - /// - /// - /// returns the salt - /// - public string ParseStoredHashPassword(string storedString, out string salt) - { - if (string.IsNullOrWhiteSpace(storedString)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(storedString)); - - var saltLen = GenerateSalt(); - salt = storedString.Substring(0, saltLen.Length); - return storedString.Substring(saltLen.Length); - } - - public static string GenerateSalt() - { - var numArray = new byte[16]; - new RNGCryptoServiceProvider().GetBytes(numArray); - return Convert.ToBase64String(numArray); - } - - /// - /// Return the hash algorithm to use - /// - /// - /// - public HashAlgorithm GetHashAlgorithm(string password) - { - if (PasswordConfiguration.HashAlgorithmType.IsNullOrWhiteSpace()) - throw new InvalidOperationException("No hash algorithm type specified"); - - var alg = HashAlgorithm.Create(PasswordConfiguration.HashAlgorithmType); - if (alg == null) - throw new InvalidOperationException($"The hash algorithm specified {PasswordConfiguration.HashAlgorithmType} cannot be resolved"); - - return alg; - } - - /// - /// Encodes the password. - /// - /// The password. - /// The encoded password. - private string LegacyEncodePassword(string password) - { - var hashAlgorith = GetHashAlgorithm(password); - var encodedPassword = Convert.ToBase64String(hashAlgorith.ComputeHash(Encoding.Unicode.GetBytes(password))); - return encodedPassword; - } - - - - - - } -} diff --git a/src/Umbraco.Web.BackOffice/Identity/UserAwarePasswordHasher.cs b/src/Umbraco.Web.BackOffice/Identity/UserAwarePasswordHasher.cs deleted file mode 100644 index fb2b1e71a0..0000000000 --- a/src/Umbraco.Web.BackOffice/Identity/UserAwarePasswordHasher.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Microsoft.AspNetCore.Identity; - -namespace Umbraco.Web.BackOffice.Identity -{ - public class UserAwarePasswordHasher : IPasswordHasher - where T : BackOfficeIdentityUser - { - private readonly PasswordSecurity _passwordSecurity; - - public UserAwarePasswordHasher(PasswordSecurity passwordSecurity) - { - _passwordSecurity = passwordSecurity; - } - - public string HashPassword(string password) - { - return _passwordSecurity.HashPasswordForStorage(password); - } - - public string HashPassword(T user, string password) - { - // TODO: Implement the logic for this, we need to lookup the password format for the user and hash accordingly: http://issues.umbraco.org/issue/U4-10089 - //NOTE: For now this just falls back to the hashing we are currently using - - return HashPassword(password); - } - - public PasswordVerificationResult VerifyHashedPassword(T user, string hashedPassword, string providedPassword) - { - // TODO: Implement the logic for this, we need to lookup the password format for the user and hash accordingly: http://issues.umbraco.org/issue/U4-10089 - //NOTE: For now this just falls back to the hashing we are currently using - - return _passwordSecurity.VerifyPassword(providedPassword, hashedPassword) - ? PasswordVerificationResult.Success - : PasswordVerificationResult.Failed; - } - } -} diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index a1f0fdae23..8dacc79614 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -2,10 +2,12 @@ using System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Umbraco.Extensions; +using Umbraco.Web.BackOffice.Identity; namespace Umbraco.Web.UI.BackOffice { @@ -31,11 +33,12 @@ namespace Umbraco.Web.UI.BackOffice // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) - { + { services.AddUmbracoConfiguration(_config); services.AddUmbracoCore(_env, out var factory); services.AddUmbracoWebComponents(); services.AddUmbracoRuntimeMinifier(_config); + services.AddUmbracoBackOfficeIdentity(); services.AddMvc(); From e872ba1a64efb1937c8b210bba437024a1144a73 Mon Sep 17 00:00:00 2001 From: Scott Brady Date: Fri, 15 May 2020 16:52:51 +0100 Subject: [PATCH 04/13] Resolvable user store --- ...coBackOfficeServiceCollectionExtensions.cs | 11 ++- .../Identity/IdentityMapDefinition.cs | 82 +++++++++++++++++++ src/Umbraco.Web.UI.NetCore/Startup.cs | 2 +- 3 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 src/Umbraco.Web.BackOffice/Identity/IdentityMapDefinition.cs diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs index bb0de07d37..9c3f791f03 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs @@ -1,5 +1,7 @@ -using Microsoft.AspNetCore.Identity; +using System.Collections.Generic; +using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; +using Umbraco.Core.Composing; using Umbraco.Core.Mapping; using Umbraco.Web.BackOffice.Identity; @@ -7,8 +9,13 @@ namespace Umbraco.Extensions { public static class UmbracoBackOfficeServiceCollectionExtensions { - public static void AddUmbracoBackOfficeIdentity(this IServiceCollection services) + public static void AddUmbracoBackOfficeIdentity(this IServiceCollection services, IFactory factory) { + // UmbracoMapper - hack? + services.AddSingleton(); + services.AddSingleton(s => new MapDefinitionCollection(new[] {s.GetService()})); + services.AddSingleton(); + services.AddIdentity() .AddDefaultTokenProviders() .AddUserStore(); diff --git a/src/Umbraco.Web.BackOffice/Identity/IdentityMapDefinition.cs b/src/Umbraco.Web.BackOffice/Identity/IdentityMapDefinition.cs new file mode 100644 index 0000000000..ec89871ace --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Identity/IdentityMapDefinition.cs @@ -0,0 +1,82 @@ +using System; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Mapping; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Services; + +namespace Umbraco.Web.BackOffice.Identity +{ + public class IdentityMapDefinition : IMapDefinition + { + private readonly ILocalizedTextService _textService; + private readonly IEntityService _entityService; + private readonly IGlobalSettings _globalSettings; + + public IdentityMapDefinition(ILocalizedTextService textService, IEntityService entityService, IGlobalSettings globalSettings) + { + _textService = textService; + _entityService = entityService; + _globalSettings = globalSettings; + } + + public void DefineMaps(UmbracoMapper mapper) + { + mapper.Define( + (source, context) => + { + var target = new BackOfficeIdentityUser(_globalSettings, source.Id, source.Groups); + target.DisableChangeTracking(); + return target; + }, + (source, target, context) => + { + Map(source, target); + target.ResetDirtyProperties(true); + target.EnableChangeTracking(); + }); + } + + // Umbraco.Code.MapAll -Id -Groups -LockoutEnabled -PhoneNumber -PhoneNumberConfirmed -TwoFactorEnabled + private void Map(IUser source, BackOfficeIdentityUser target) + { + // well, the ctor has been fixed + /* + // these two are already set in ctor but BackOfficeIdentityUser ctor is CompletelyBroken + target.Id = source.Id; + target.Groups = source.Groups.ToArray(); + */ + + target.CalculatedMediaStartNodeIds = source.CalculateMediaStartNodeIds(_entityService); + target.CalculatedContentStartNodeIds = source.CalculateContentStartNodeIds(_entityService); + target.Email = source.Email; + target.UserName = source.Username; + target.LastPasswordChangeDateUtc = source.LastPasswordChangeDate.ToUniversalTime(); + target.LastLoginDateUtc = source.LastLoginDate.ToUniversalTime(); + target.EmailConfirmed = source.EmailConfirmedDate.HasValue; + target.Name = source.Name; + target.AccessFailedCount = source.FailedPasswordAttempts; + target.PasswordHash = GetPasswordHash(source.RawPasswordValue); + target.StartContentIds = source.StartContentIds; + target.StartMediaIds = source.StartMediaIds; + target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); // project CultureInfo to string + target.IsApproved = source.IsApproved; + target.SecurityStamp = source.SecurityStamp; + target.LockoutEndDateUtc = source.IsLockedOut ? DateTime.MaxValue.ToUniversalTime() : (DateTime?) null; + + // this was in AutoMapper but does not have a setter anyways + //target.AllowedSections = source.AllowedSections.ToArray(), + + // these were marked as ignored for AutoMapper but don't have a setter anyways + //target.Logins =; + //target.Claims =; + //target.Roles =; + } + + private static string GetPasswordHash(string storedPass) + { + return storedPass.StartsWith(Constants.Security.EmptyPasswordPrefix) ? null : storedPass; + } + } +} diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index 8dacc79614..ed3ccda37b 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -38,7 +38,7 @@ namespace Umbraco.Web.UI.BackOffice services.AddUmbracoCore(_env, out var factory); services.AddUmbracoWebComponents(); services.AddUmbracoRuntimeMinifier(_config); - services.AddUmbracoBackOfficeIdentity(); + services.AddUmbracoBackOfficeIdentity(factory); services.AddMvc(); From 8317518b4c2be01261d68a369ca6bafabd6492f6 Mon Sep 17 00:00:00 2001 From: Scott Brady Date: Sun, 17 May 2020 07:56:59 +0100 Subject: [PATCH 05/13] Resolvable user manager. Removed password configuration and generation for now --- ...coBackOfficeServiceCollectionExtensions.cs | 37 +++++- .../Identity/BackOfficeUserManager.cs | 122 ++---------------- 2 files changed, 41 insertions(+), 118 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs index 9c3f791f03..5d9634a436 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs @@ -1,9 +1,15 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Security.Claims; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; +using Umbraco.Core; using Umbraco.Core.Composing; +using Umbraco.Core.Configuration; using Umbraco.Core.Mapping; +using Umbraco.Net; using Umbraco.Web.BackOffice.Identity; +using Umbraco.Web.Common.AspNetCore; namespace Umbraco.Extensions { @@ -15,13 +21,34 @@ namespace Umbraco.Extensions services.AddSingleton(); services.AddSingleton(s => new MapDefinitionCollection(new[] {s.GetService()})); services.AddSingleton(); - - services.AddIdentity() + + services.AddScoped(); + + services.AddIdentity(options => + { + options.User.RequireUniqueEmail = true; + + // TODO: Configure password configuration + /*options.Password.RequiredLength = passwordConfiguration.RequiredLength; + options.Password.RequireNonAlphanumeric = passwordConfiguration.RequireNonLetterOrDigit; + options.Password.RequireDigit = passwordConfiguration.RequireDigit; + options.Password.RequireLowercase = passwordConfiguration.RequireLowercase; + options.Password.RequireUppercase = passwordConfiguration.RequireUppercase; + options.Lockout.MaxFailedAccessAttempts = passwordConfiguration.MaxFailedAccessAttemptsBeforeLockout;*/ + + options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier; + options.ClaimsIdentity.UserNameClaimType = ClaimTypes.Name; + options.ClaimsIdentity.RoleClaimType = ClaimTypes.Role; + options.ClaimsIdentity.SecurityStampClaimType = Constants.Web.SecurityStampClaimType; + + options.Lockout.AllowedForNewUsers = true; + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromDays(30); + }) .AddDefaultTokenProviders() - .AddUserStore(); + .AddUserStore() + .AddUserManager(); // .AddClaimsPrincipalFactory>() // TODO: extract custom claims principal factory - // .AddUserManager>() } } } diff --git a/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserManager.cs b/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserManager.cs index a45ca9ed6e..335edea43f 100644 --- a/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserManager.cs +++ b/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserManager.cs @@ -1,140 +1,43 @@ using System; using System.Collections.Generic; -using System.Security.Claims; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Umbraco.Core; using Umbraco.Core.Configuration; -using Umbraco.Core.Mapping; using Umbraco.Core.Security; -using Umbraco.Core.Services; using Umbraco.Net; namespace Umbraco.Web.BackOffice.Identity { public class BackOfficeUserManager : BackOfficeUserManager { - public const string OwinMarkerKey = "Umbraco.Web.Security.Identity.BackOfficeUserManagerMarker"; - - public BackOfficeUserManager( - IPasswordConfiguration passwordConfiguration, - IIpResolver ipResolver, - IUserStore store, - IOptions optionsAccessor, - IEnumerable> userValidators, - IEnumerable> passwordValidators, - ILookupNormalizer keyNormalizer, - IdentityErrorDescriber errors, - ILogger> logger) - : base(passwordConfiguration, ipResolver, store, optionsAccessor, userValidators, passwordValidators, keyNormalizer, errors, null, logger) + public BackOfficeUserManager(IIpResolver ipResolver, IUserStore store, IOptions optionsAccessor, IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger> logger) + : base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) { - InitUserManager(this); } - - #region Static Create methods - - /// - /// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager - /// - public static BackOfficeUserManager Create( - IUserService userService, - IEntityService entityService, - IExternalLoginService externalLoginService, - IGlobalSettings globalSettings, - UmbracoMapper mapper, - IPasswordConfiguration passwordConfiguration, - IIpResolver ipResolver, - IdentityErrorDescriber errors, - IDataProtectionProvider dataProtectionProvider, - ILogger> logger) - { - var store = new BackOfficeUserStore(userService, entityService, externalLoginService, globalSettings, mapper); - - return Create( - passwordConfiguration, - ipResolver, - store, - errors, - dataProtectionProvider, - logger); - } - - /// - /// Creates a BackOfficeUserManager instance with all default options and a custom BackOfficeUserManager instance - /// - public static BackOfficeUserManager Create( - IPasswordConfiguration passwordConfiguration, - IIpResolver ipResolver, - IUserStore customUserStore, - IdentityErrorDescriber errors, - IDataProtectionProvider dataProtectionProvider, - ILogger> logger) - { - var options = new IdentityOptions(); - - // Configure validation logic for usernames - var userValidators = new List> { new BackOfficeUserValidator() }; - options.User.RequireUniqueEmail = true; - - // Configure validation logic for passwords - var passwordValidators = new List> { new PasswordValidator() }; - options.Password.RequiredLength = passwordConfiguration.RequiredLength; - options.Password.RequireNonAlphanumeric = passwordConfiguration.RequireNonLetterOrDigit; - options.Password.RequireDigit = passwordConfiguration.RequireDigit; - options.Password.RequireLowercase = passwordConfiguration.RequireLowercase; - options.Password.RequireUppercase = passwordConfiguration.RequireUppercase; - - // Ensure Umbraco security stamp claim type is used - options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier; - options.ClaimsIdentity.UserNameClaimType = ClaimTypes.Name; - options.ClaimsIdentity.RoleClaimType = ClaimTypes.Role; - options.ClaimsIdentity.SecurityStampClaimType = Constants.Web.SecurityStampClaimType; - - options.Lockout.AllowedForNewUsers = true; - options.Lockout.MaxFailedAccessAttempts = passwordConfiguration.MaxFailedAccessAttemptsBeforeLockout; - //NOTE: This just needs to be in the future, we currently don't support a lockout timespan, it's either they are locked - // or they are not locked, but this determines what is set on the account lockout date which corresponds to whether they are - // locked out or not. - options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromDays(30); - - return new BackOfficeUserManager( - passwordConfiguration, - ipResolver, - customUserStore, - new OptionsWrapper(options), - userValidators, - passwordValidators, - new NopLookupNormalizer(), - errors, - logger); - } - - #endregion } public class BackOfficeUserManager : UserManager where T : BackOfficeIdentityUser { private PasswordGenerator _passwordGenerator; - + public BackOfficeUserManager( - IPasswordConfiguration passwordConfiguration, IIpResolver ipResolver, IUserStore store, IOptions optionsAccessor, + IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger> logger) - : base(store, optionsAccessor, null, userValidators, passwordValidators, keyNormalizer, errors, services, logger) + : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) { - PasswordConfiguration = passwordConfiguration ?? throw new ArgumentNullException(nameof(passwordConfiguration)); + // PasswordConfiguration = passwordConfiguration ?? throw new ArgumentNullException(nameof(passwordConfiguration)); IpResolver = ipResolver ?? throw new ArgumentNullException(nameof(ipResolver)); } @@ -153,16 +56,7 @@ namespace Umbraco.Web.BackOffice.Identity // TODO: Support this public override bool SupportsUserPhoneNumber => false; #endregion - - /// - /// Initializes the user manager with the correct options - /// - protected void InitUserManager(BackOfficeUserManager manager) - { - // use a custom hasher based on our membership provider - PasswordHasher = GetDefaultPasswordHasher(PasswordConfiguration); - } - + /// /// Used to validate a user's session /// @@ -201,6 +95,8 @@ namespace Umbraco.Web.BackOffice.Identity /// public string GeneratePassword() { + throw new NotImplementedException(); + if (_passwordGenerator == null) _passwordGenerator = new PasswordGenerator(PasswordConfiguration); var password = _passwordGenerator.GeneratePassword(); return password; From 462762735097068d28397b4bc3f60ead98e8438f Mon Sep 17 00:00:00 2001 From: Scott Brady Date: Sun, 17 May 2020 08:48:36 +0100 Subject: [PATCH 06/13] Moved .NET standard back office identity code into infrastructure (lowest common dependency) --- .../BackOffice}/BackOfficeIdentityUser.cs | 3 +- .../BackOffice}/BackOfficeUserManager.cs | 2 +- .../BackOfficeUserPasswordCheckerResult.cs | 2 +- .../BackOffice}/BackOfficeUserStore.cs | 3 +- .../BackOffice}/BackOfficeUserValidator.cs | 2 +- .../Extensions/ClaimsPrincipalExtensions.cs | 2 +- .../IBackOfficeUserPasswordChecker.cs | 2 +- .../BackOffice}/IUserSessionStore.cs | 2 +- .../BackOffice}/IdentityAuditEventArgs.cs | 0 .../BackOffice}/IdentityMapDefinition.cs | 0 .../BackOffice}/IdentityUser.cs | 0 .../BackOffice}/NopLookupNormalizer.cs | 2 +- .../BackOffice}/UmbracoBackOfficeIdentity.cs | 3 +- .../BackOffice}/UserLoginInfoWrapper.cs | 2 +- .../Install/InstallSteps/NewInstallStep.cs | 38 +++++++++---------- .../Umbraco.Infrastructure.csproj | 3 +- ...kOfficeServiceCollectionExtensionsTests.cs | 4 +- ...coBackOfficeServiceCollectionExtensions.cs | 4 +- 18 files changed, 35 insertions(+), 39 deletions(-) rename src/{Umbraco.Web.BackOffice/Identity => Umbraco.Infrastructure/BackOffice}/BackOfficeIdentityUser.cs (99%) rename src/{Umbraco.Web.BackOffice/Identity => Umbraco.Infrastructure/BackOffice}/BackOfficeUserManager.cs (99%) rename src/{Umbraco.Web.BackOffice/Identity => Umbraco.Infrastructure/BackOffice}/BackOfficeUserPasswordCheckerResult.cs (85%) rename src/{Umbraco.Web.BackOffice/Identity => Umbraco.Infrastructure/BackOffice}/BackOfficeUserStore.cs (99%) rename src/{Umbraco.Web.BackOffice/Identity => Umbraco.Infrastructure/BackOffice}/BackOfficeUserValidator.cs (93%) rename src/{Umbraco.Web.BackOffice => Umbraco.Infrastructure/BackOffice}/Extensions/ClaimsPrincipalExtensions.cs (97%) rename src/{Umbraco.Web.BackOffice/Identity => Umbraco.Infrastructure/BackOffice}/IBackOfficeUserPasswordChecker.cs (96%) rename src/{Umbraco.Web.BackOffice/Identity => Umbraco.Infrastructure/BackOffice}/IUserSessionStore.cs (91%) rename src/{Umbraco.Web.BackOffice/Identity => Umbraco.Infrastructure/BackOffice}/IdentityAuditEventArgs.cs (100%) rename src/{Umbraco.Web.BackOffice/Identity => Umbraco.Infrastructure/BackOffice}/IdentityMapDefinition.cs (100%) rename src/{Umbraco.Web.BackOffice/Identity => Umbraco.Infrastructure/BackOffice}/IdentityUser.cs (100%) rename src/{Umbraco.Web.BackOffice/Identity => Umbraco.Infrastructure/BackOffice}/NopLookupNormalizer.cs (89%) rename src/{Umbraco.Web.BackOffice/Identity => Umbraco.Infrastructure/BackOffice}/UmbracoBackOfficeIdentity.cs (99%) rename src/{Umbraco.Web.BackOffice/Identity => Umbraco.Infrastructure/BackOffice}/UserLoginInfoWrapper.cs (94%) diff --git a/src/Umbraco.Web.BackOffice/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityUser.cs similarity index 99% rename from src/Umbraco.Web.BackOffice/Identity/BackOfficeIdentityUser.cs rename to src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityUser.cs index c1c21d3ab2..4eca7a5e57 100644 --- a/src/Umbraco.Web.BackOffice/Identity/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityUser.cs @@ -4,13 +4,12 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; -using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Models.Entities; using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; -namespace Umbraco.Web.BackOffice.Identity +namespace Umbraco.Core.BackOffice { public class BackOfficeIdentityUser : IdentityUser, IdentityUserClaim>, IRememberBeingDirty { diff --git a/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserManager.cs b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs similarity index 99% rename from src/Umbraco.Web.BackOffice/Identity/BackOfficeUserManager.cs rename to src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs index 335edea43f..6d2ad081c3 100644 --- a/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserManager.cs +++ b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs @@ -9,7 +9,7 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Security; using Umbraco.Net; -namespace Umbraco.Web.BackOffice.Identity +namespace Umbraco.Core.BackOffice { public class BackOfficeUserManager : BackOfficeUserManager { diff --git a/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserPasswordCheckerResult.cs b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserPasswordCheckerResult.cs similarity index 85% rename from src/Umbraco.Web.BackOffice/Identity/BackOfficeUserPasswordCheckerResult.cs rename to src/Umbraco.Infrastructure/BackOffice/BackOfficeUserPasswordCheckerResult.cs index 72c0707115..7936fab682 100644 --- a/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserPasswordCheckerResult.cs +++ b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserPasswordCheckerResult.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Web.BackOffice.Identity +namespace Umbraco.Core.BackOffice { /// /// The result returned from the IBackOfficeUserPasswordChecker diff --git a/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs similarity index 99% rename from src/Umbraco.Web.BackOffice/Identity/BackOfficeUserStore.cs rename to src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs index 6815fdc71e..b75735e688 100644 --- a/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; -using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Mapping; using Umbraco.Core.Models; @@ -13,7 +12,7 @@ using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; -namespace Umbraco.Web.BackOffice.Identity +namespace Umbraco.Core.BackOffice { public class BackOfficeUserStore : DisposableObjectSlim, IUserPasswordStore, diff --git a/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserValidator.cs b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserValidator.cs similarity index 93% rename from src/Umbraco.Web.BackOffice/Identity/BackOfficeUserValidator.cs rename to src/Umbraco.Infrastructure/BackOffice/BackOfficeUserValidator.cs index f80d8b90db..131bd08ac9 100644 --- a/src/Umbraco.Web.BackOffice/Identity/BackOfficeUserValidator.cs +++ b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserValidator.cs @@ -1,7 +1,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; -namespace Umbraco.Web.BackOffice.Identity +namespace Umbraco.Core.BackOffice { public class BackOfficeUserValidator : UserValidator where T : BackOfficeIdentityUser diff --git a/src/Umbraco.Web.BackOffice/Extensions/ClaimsPrincipalExtensions.cs b/src/Umbraco.Infrastructure/BackOffice/Extensions/ClaimsPrincipalExtensions.cs similarity index 97% rename from src/Umbraco.Web.BackOffice/Extensions/ClaimsPrincipalExtensions.cs rename to src/Umbraco.Infrastructure/BackOffice/Extensions/ClaimsPrincipalExtensions.cs index 833522c2c1..a7eea63983 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/ClaimsPrincipalExtensions.cs +++ b/src/Umbraco.Infrastructure/BackOffice/Extensions/ClaimsPrincipalExtensions.cs @@ -3,7 +3,7 @@ using System.Linq; using System.Security.Claims; using System.Security.Principal; using Umbraco.Core; -using Umbraco.Web.BackOffice.Identity; +using Umbraco.Core.BackOffice; namespace Umbraco.Extensions { diff --git a/src/Umbraco.Web.BackOffice/Identity/IBackOfficeUserPasswordChecker.cs b/src/Umbraco.Infrastructure/BackOffice/IBackOfficeUserPasswordChecker.cs similarity index 96% rename from src/Umbraco.Web.BackOffice/Identity/IBackOfficeUserPasswordChecker.cs rename to src/Umbraco.Infrastructure/BackOffice/IBackOfficeUserPasswordChecker.cs index b94e261506..5874337f4a 100644 --- a/src/Umbraco.Web.BackOffice/Identity/IBackOfficeUserPasswordChecker.cs +++ b/src/Umbraco.Infrastructure/BackOffice/IBackOfficeUserPasswordChecker.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; -namespace Umbraco.Web.BackOffice.Identity +namespace Umbraco.Core.BackOffice { /// /// Used by the BackOfficeUserManager to check the username/password which allows for developers to more easily diff --git a/src/Umbraco.Web.BackOffice/Identity/IUserSessionStore.cs b/src/Umbraco.Infrastructure/BackOffice/IUserSessionStore.cs similarity index 91% rename from src/Umbraco.Web.BackOffice/Identity/IUserSessionStore.cs rename to src/Umbraco.Infrastructure/BackOffice/IUserSessionStore.cs index 756eec3cb8..69d5408cf7 100644 --- a/src/Umbraco.Web.BackOffice/Identity/IUserSessionStore.cs +++ b/src/Umbraco.Infrastructure/BackOffice/IUserSessionStore.cs @@ -1,7 +1,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; -namespace Umbraco.Web.BackOffice.Identity +namespace Umbraco.Core.BackOffice { /// /// An IUserStore interface part to implement if the store supports validating user session Ids diff --git a/src/Umbraco.Web.BackOffice/Identity/IdentityAuditEventArgs.cs b/src/Umbraco.Infrastructure/BackOffice/IdentityAuditEventArgs.cs similarity index 100% rename from src/Umbraco.Web.BackOffice/Identity/IdentityAuditEventArgs.cs rename to src/Umbraco.Infrastructure/BackOffice/IdentityAuditEventArgs.cs diff --git a/src/Umbraco.Web.BackOffice/Identity/IdentityMapDefinition.cs b/src/Umbraco.Infrastructure/BackOffice/IdentityMapDefinition.cs similarity index 100% rename from src/Umbraco.Web.BackOffice/Identity/IdentityMapDefinition.cs rename to src/Umbraco.Infrastructure/BackOffice/IdentityMapDefinition.cs diff --git a/src/Umbraco.Web.BackOffice/Identity/IdentityUser.cs b/src/Umbraco.Infrastructure/BackOffice/IdentityUser.cs similarity index 100% rename from src/Umbraco.Web.BackOffice/Identity/IdentityUser.cs rename to src/Umbraco.Infrastructure/BackOffice/IdentityUser.cs diff --git a/src/Umbraco.Web.BackOffice/Identity/NopLookupNormalizer.cs b/src/Umbraco.Infrastructure/BackOffice/NopLookupNormalizer.cs similarity index 89% rename from src/Umbraco.Web.BackOffice/Identity/NopLookupNormalizer.cs rename to src/Umbraco.Infrastructure/BackOffice/NopLookupNormalizer.cs index c846498098..6797d62e6c 100644 --- a/src/Umbraco.Web.BackOffice/Identity/NopLookupNormalizer.cs +++ b/src/Umbraco.Infrastructure/BackOffice/NopLookupNormalizer.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Identity; -namespace Umbraco.Web.BackOffice.Identity +namespace Umbraco.Core.BackOffice { /// /// No-op lookup normalizer to maintain compatibility with ASP.NET Identity 2 diff --git a/src/Umbraco.Web.BackOffice/Identity/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Infrastructure/BackOffice/UmbracoBackOfficeIdentity.cs similarity index 99% rename from src/Umbraco.Web.BackOffice/Identity/UmbracoBackOfficeIdentity.cs rename to src/Umbraco.Infrastructure/BackOffice/UmbracoBackOfficeIdentity.cs index ca3e6d742f..c1ec692339 100644 --- a/src/Umbraco.Web.BackOffice/Identity/UmbracoBackOfficeIdentity.cs +++ b/src/Umbraco.Infrastructure/BackOffice/UmbracoBackOfficeIdentity.cs @@ -2,9 +2,8 @@ using System.Collections.Generic; using System.Linq; using System.Security.Claims; -using Umbraco.Core; -namespace Umbraco.Web.BackOffice.Identity +namespace Umbraco.Core.BackOffice { /// diff --git a/src/Umbraco.Web.BackOffice/Identity/UserLoginInfoWrapper.cs b/src/Umbraco.Infrastructure/BackOffice/UserLoginInfoWrapper.cs similarity index 94% rename from src/Umbraco.Web.BackOffice/Identity/UserLoginInfoWrapper.cs rename to src/Umbraco.Infrastructure/BackOffice/UserLoginInfoWrapper.cs index 0c6780ae5d..ab6af35519 100644 --- a/src/Umbraco.Web.BackOffice/Identity/UserLoginInfoWrapper.cs +++ b/src/Umbraco.Infrastructure/BackOffice/UserLoginInfoWrapper.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Identity; using Umbraco.Core.Models.Identity; -namespace Umbraco.Web.BackOffice.Identity +namespace Umbraco.Core.BackOffice { internal class UserLoginInfoWrapper : IUserLoginInfo { diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs index 3ae92d0179..b63805db07 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Specialized; -using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; @@ -10,8 +9,8 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Migrations.Install; using Umbraco.Core.Services; using Umbraco.Web.Install.Models; +using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Web.Security; namespace Umbraco.Web.Install.InstallSteps { @@ -34,8 +33,9 @@ namespace Umbraco.Web.Install.InstallSteps private readonly ISecuritySettings _securitySettings; private readonly IConnectionStrings _connectionStrings; private readonly ICookieManager _cookieManager; + private readonly BackOfficeUserManager _userManager; - public NewInstallStep(IUserService userService, DatabaseBuilder databaseBuilder, IGlobalSettings globalSettings, IUserPasswordConfiguration passwordConfiguration, ISecuritySettings securitySettings, IConnectionStrings connectionStrings, ICookieManager cookieManager) + public NewInstallStep(IUserService userService, DatabaseBuilder databaseBuilder, IGlobalSettings globalSettings, IUserPasswordConfiguration passwordConfiguration, ISecuritySettings securitySettings, IConnectionStrings connectionStrings, ICookieManager cookieManager, BackOfficeUserManager userManager) { _userService = userService ?? throw new ArgumentNullException(nameof(userService)); _databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder)); @@ -44,6 +44,7 @@ namespace Umbraco.Web.Install.InstallSteps _securitySettings = securitySettings ?? throw new ArgumentNullException(nameof(securitySettings)); _connectionStrings = connectionStrings ?? throw new ArgumentNullException(nameof(connectionStrings)); _cookieManager = cookieManager; + _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); } public override async Task ExecuteAsync(UserModel user) @@ -60,22 +61,21 @@ namespace Umbraco.Web.Install.InstallSteps _userService.Save(admin); //TODO: This needs to be reintroduced, when users are compatible with ASP.NET Core Identity. - // var userManager = _httpContextAccessor.GetRequiredHttpContext().GetOwinContext().GetBackOfficeUserManager(); - // var membershipUser = await userManager.FindByIdAsync(Constants.Security.SuperUserId.ToString()); - // if (membershipUser == null) - // { - // throw new InvalidOperationException( - // $"No user found in membership provider with id of {Constants.Security.SuperUserId}."); - // } - // - // //To change the password here we actually need to reset it since we don't have an old one to use to change - // var resetToken = await userManager.GeneratePasswordResetTokenAsync(membershipUser); - // if (string.IsNullOrWhiteSpace(resetToken)) - // throw new InvalidOperationException("Could not reset password: unable to generate internal reset token"); - // - // var resetResult = await userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, user.Password.Trim()); - // if (!resetResult.Succeeded) - // throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors.ToErrorMessage())); + var membershipUser = await _userManager.FindByIdAsync(Constants.Security.SuperUserId.ToString()); + if (membershipUser == null) + { + throw new InvalidOperationException( + $"No user found in membership provider with id of {Constants.Security.SuperUserId}."); + } + + //To change the password here we actually need to reset it since we don't have an old one to use to change + var resetToken = await _userManager.GeneratePasswordResetTokenAsync(membershipUser); + if (string.IsNullOrWhiteSpace(resetToken)) + throw new InvalidOperationException("Could not reset password: unable to generate internal reset token"); + + var resetResult = await _userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, user.Password.Trim()); + if (!resetResult.Succeeded) + throw new InvalidOperationException("Could not reset password: " + string.Join(", ", "error" /*resetResult.Errors.ToErrorMessage()*/)); diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index 177b35b27d..aaac2324ae 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 @@ -16,6 +16,7 @@ + diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensionsTests.cs index 677e919c19..c65053c8cf 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensionsTests.cs @@ -1,10 +1,10 @@ -using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Moq; using NUnit.Framework; using Umbraco.Extensions; -using Umbraco.Web.BackOffice.Identity; +using Umbraco.Core.BackOffice; namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Extensions { diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs index 5d9634a436..379381e6e9 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs @@ -1,14 +1,12 @@ using System; -using System.Collections.Generic; using System.Security.Claims; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Umbraco.Core; +using Umbraco.Core.BackOffice; using Umbraco.Core.Composing; -using Umbraco.Core.Configuration; using Umbraco.Core.Mapping; using Umbraco.Net; -using Umbraco.Web.BackOffice.Identity; using Umbraco.Web.Common.AspNetCore; namespace Umbraco.Extensions From d12c6ddf625817026006adb4f307fd301c6d00e1 Mon Sep 17 00:00:00 2001 From: Scott Brady Date: Sun, 17 May 2020 08:59:40 +0100 Subject: [PATCH 07/13] Cleanup --- .../BackOffice/IdentityMapDefinition.cs | 3 +-- src/Umbraco.Infrastructure/BackOffice/IdentityUser.cs | 2 +- .../Install/InstallSteps/NewInstallStep.cs | 3 --- .../UmbracoBackOfficeServiceCollectionExtensionsTests.cs | 6 +++--- src/Umbraco.Web.UI.NetCore/Startup.cs | 2 -- 5 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Infrastructure/BackOffice/IdentityMapDefinition.cs b/src/Umbraco.Infrastructure/BackOffice/IdentityMapDefinition.cs index ec89871ace..59590907f6 100644 --- a/src/Umbraco.Infrastructure/BackOffice/IdentityMapDefinition.cs +++ b/src/Umbraco.Infrastructure/BackOffice/IdentityMapDefinition.cs @@ -1,12 +1,11 @@ using System; -using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; -namespace Umbraco.Web.BackOffice.Identity +namespace Umbraco.Core.BackOffice { public class IdentityMapDefinition : IMapDefinition { diff --git a/src/Umbraco.Infrastructure/BackOffice/IdentityUser.cs b/src/Umbraco.Infrastructure/BackOffice/IdentityUser.cs index b0399bf228..9de30360ae 100644 --- a/src/Umbraco.Infrastructure/BackOffice/IdentityUser.cs +++ b/src/Umbraco.Infrastructure/BackOffice/IdentityUser.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using Umbraco.Core.Models.Identity; -namespace Umbraco.Web.BackOffice.Identity +namespace Umbraco.Core.BackOffice { /// /// Default IUser implementation diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs index b63805db07..133272e8de 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs @@ -77,9 +77,6 @@ namespace Umbraco.Web.Install.InstallSteps if (!resetResult.Succeeded) throw new InvalidOperationException("Could not reset password: " + string.Join(", ", "error" /*resetResult.Errors.ToErrorMessage()*/)); - - - if (user.SubscribeToNewsLetter) { if (_httpClient == null) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensionsTests.cs index c65053c8cf..b5b7cf9a02 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensionsTests.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Moq; @@ -11,7 +11,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Extensions [TestFixture] public class UmbracoBackOfficeServiceCollectionExtensionsTests { - [Test] + /*[Test] public void AddUmbracoBackOfficeIdentity_ExpectBackOfficeUserStoreResolvable() { var services = new ServiceCollection(); @@ -25,6 +25,6 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Extensions var userStore = serviceProvider.GetService>(); Assert.AreEqual(typeof(BackOfficeUserStore), userStore.GetType()); - } + }*/ } } diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index ed3ccda37b..613095437b 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -2,12 +2,10 @@ using System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; -using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Umbraco.Extensions; -using Umbraco.Web.BackOffice.Identity; namespace Umbraco.Web.UI.BackOffice { From c6210ba7f20b4b72660de7c241d32e7da736c77a Mon Sep 17 00:00:00 2001 From: Scott Brady Date: Sun, 17 May 2020 10:39:30 +0100 Subject: [PATCH 08/13] Working installation. Use of Thread.CurrentPrincipal removed for now --- .../BackOffice/IdentityAuditEventArgs.cs | 6 +-- .../Testing/UmbracoIntegrationTest.cs | 4 ++ ...kOfficeServiceCollectionExtensionsTests.cs | 40 +++++++++++++++++++ ...kOfficeServiceCollectionExtensionsTests.cs | 30 -------------- ...coBackOfficeServiceCollectionExtensions.cs | 3 +- src/Umbraco.Web.UI.NetCore/Startup.cs | 2 +- 6 files changed, 49 insertions(+), 36 deletions(-) create mode 100644 src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs delete mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensionsTests.cs diff --git a/src/Umbraco.Infrastructure/BackOffice/IdentityAuditEventArgs.cs b/src/Umbraco.Infrastructure/BackOffice/IdentityAuditEventArgs.cs index cb0b04ef56..1991f248f1 100644 --- a/src/Umbraco.Infrastructure/BackOffice/IdentityAuditEventArgs.cs +++ b/src/Umbraco.Infrastructure/BackOffice/IdentityAuditEventArgs.cs @@ -3,7 +3,7 @@ using System.Threading; using Umbraco.Extensions; -namespace Umbraco.Web.BackOffice.Identity +namespace Umbraco.Core.BackOffice { /// /// This class is used by events raised from the BackofficeUserManager @@ -108,9 +108,9 @@ namespace Umbraco.Web.BackOffice.Identity protected int GetCurrentRequestBackofficeUserId() { var userId = -1; - var backOfficeIdentity = Thread.CurrentPrincipal.GetUmbracoIdentity(); + /*var backOfficeIdentity = Thread.CurrentPrincipal.GetUmbracoIdentity(); if (backOfficeIdentity != null) - int.TryParse(backOfficeIdentity.Id.ToString(), out userId); + int.TryParse(backOfficeIdentity.Id.ToString(), out userId);*/ return userId; } } diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index a38e002810..8f9ec4d833 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -109,6 +109,8 @@ namespace Umbraco.Tests.Integration.Testing // Add it! services.AddUmbracoConfiguration(hostContext.Configuration); services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, NoAppCache.Instance, testHelper.GetLoggingConfiguration(), out _); + + CustomTestSetup(services); }); var host = await hostBuilder.StartAsync(); @@ -123,6 +125,8 @@ namespace Umbraco.Tests.Integration.Testing #region Common services + protected virtual Action CustomTestSetup => services => { }; + /// /// Returns the DI container /// diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000000..3b61f31b66 --- /dev/null +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs @@ -0,0 +1,40 @@ +using System; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Moq; +using NUnit.Framework; +using Umbraco.Extensions; +using Umbraco.Core.BackOffice; +using Umbraco.Core.Cache; +using Umbraco.Core.Composing; +using Umbraco.Tests.Integration.Extensions; +using Umbraco.Tests.Integration.Implementations; +using Umbraco.Tests.Integration.Testing; + +namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Extensions +{ + [TestFixture] + public class UmbracoBackOfficeServiceCollectionExtensionsTests : UmbracoIntegrationTest + { + [Test] + public void AddUmbracoBackOfficeIdentity_ExpectBackOfficeUserStoreResolvable() + { + var userStore = Services.GetService>(); + + Assert.IsNotNull(userStore); + Assert.AreEqual(typeof(BackOfficeUserStore), userStore.GetType()); + } + + [Test] + public void AddUmbracoBackOfficeIdentity_ExpectBackOfficeUserManagerResolvable() + { + var userManager = Services.GetService(); + + Assert.NotNull(userManager); + } + + protected override Action CustomTestSetup => (services) => services.AddUmbracoBackOfficeIdentity(); + } +} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensionsTests.cs deleted file mode 100644 index b5b7cf9a02..0000000000 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensionsTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.DependencyInjection; -using Moq; -using NUnit.Framework; -using Umbraco.Extensions; -using Umbraco.Core.BackOffice; - -namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Extensions -{ - [TestFixture] - public class UmbracoBackOfficeServiceCollectionExtensionsTests - { - /*[Test] - public void AddUmbracoBackOfficeIdentity_ExpectBackOfficeUserStoreResolvable() - { - var services = new ServiceCollection(); - - var mockEnvironment = new Mock(); - - services.AddUmbracoCore(mockEnvironment.Object); - services.AddUmbracoBackOfficeIdentity(); - - var serviceProvider = services.BuildServiceProvider(); - - var userStore = serviceProvider.GetService>(); - Assert.AreEqual(typeof(BackOfficeUserStore), userStore.GetType()); - }*/ - } -} diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs index 379381e6e9..9d3e2a9e3f 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Umbraco.Core; using Umbraco.Core.BackOffice; -using Umbraco.Core.Composing; using Umbraco.Core.Mapping; using Umbraco.Net; using Umbraco.Web.Common.AspNetCore; @@ -13,7 +12,7 @@ namespace Umbraco.Extensions { public static class UmbracoBackOfficeServiceCollectionExtensions { - public static void AddUmbracoBackOfficeIdentity(this IServiceCollection services, IFactory factory) + public static void AddUmbracoBackOfficeIdentity(this IServiceCollection services) { // UmbracoMapper - hack? services.AddSingleton(); diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index 613095437b..27e276138e 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -36,7 +36,7 @@ namespace Umbraco.Web.UI.BackOffice services.AddUmbracoCore(_env, out var factory); services.AddUmbracoWebComponents(); services.AddUmbracoRuntimeMinifier(_config); - services.AddUmbracoBackOfficeIdentity(factory); + services.AddUmbracoBackOfficeIdentity(); services.AddMvc(); From c92c633edbb30f956a54156c6510fb2aea038df9 Mon Sep 17 00:00:00 2001 From: Scott Brady Date: Mon, 18 May 2020 08:21:34 +0100 Subject: [PATCH 09/13] Reduced duplicated code --- .../Models/Identity}/IdentityUser.cs | 3 +- .../BackOfficeClaimsPrincipalFactory.cs | 5 +- .../BackOffice/BackOfficeIdentityUser.cs | 8 +- .../BackOffice/UserLoginInfoWrapper.cs | 2 +- .../NopLookupNormalizerTests.cs | 2 +- ...kOfficeServiceCollectionExtensionsTests.cs | 16 +- .../BackOfficeClaimsPrincipalFactoryTests.cs | 11 +- .../BackOffice/NopLookupNormalizerTests.cs | 56 +++ .../UmbracoBackOfficeIdentityTests.cs | 9 +- .../Security/BackOfficeUserManagerTests.cs | 4 +- .../OwinDataProtectorTokenProviderTests.cs | 2 +- .../UmbracoSecurityStampValidatorTests.cs | 33 +- .../AuthenticateEverythingMiddleware.cs | 2 +- .../TestControllerActivatorBase.cs | 1 + src/Umbraco.Tests/Umbraco.Tests.csproj | 3 - .../Web/Controllers/UsersControllerTests.cs | 3 +- .../Controllers/BackOfficeController.cs | 1 - ...coBackOfficeServiceCollectionExtensions.cs | 21 +- .../Compose/AuditEventsComponent.cs | 3 +- .../WebMappingProfiles.cs | 2 +- .../Editors/AuthenticationController.cs | 9 +- .../Editors/BackOfficeController.cs | 2 +- src/Umbraco.Web/Editors/PasswordChanger.cs | 6 +- src/Umbraco.Web/Editors/UsersController.cs | 2 +- .../Models/Identity/BackOfficeIdentityUser.cs | 434 ------------------ .../Models/Identity/IdentityMapDefinition.cs | 82 ---- .../Models/Identity/IdentityUser.cs | 116 ----- .../Models/Identity/UserLoginInfoWrapper.cs | 29 -- src/Umbraco.Web/OwinExtensions.cs | 2 +- ...eDirectoryBackOfficeUserPasswordChecker.cs | 2 +- .../Security/AppBuilderExtensions.cs | 2 +- .../Security/AuthenticationExtensions.cs | 35 +- .../BackOfficeCookieAuthenticationProvider.cs | 8 +- .../Security/BackOfficeSignInManager.cs | 4 +- .../Security/BackOfficeUserManager.cs | 2 +- .../Security/BackOfficeUserManagerMarker.cs | 2 +- .../Security/BackOfficeUserStore.cs | 3 +- .../Security/BackOfficeUserValidator.cs | 2 +- .../Security/ExternalSignInAutoLinkOptions.cs | 2 +- .../Security/FixWindowsAuthMiddlware.cs | 2 +- .../Security/IBackOfficeUserManagerMarker.cs | 2 +- .../IBackOfficeUserPasswordChecker.cs | 3 +- src/Umbraco.Web/Security/IUserSessionStore.cs | 15 - .../Security/IdentityAuditEventArgs.cs | 1 + .../Security/NopLookupNormalizer.cs | 14 - .../OwinDataProtectorTokenProvider.cs | 2 +- .../PreviewAuthenticationMiddleware.cs | 4 +- .../Security/UmbracoBackOfficeIdentity.cs | 232 ---------- .../Security/UmbracoSecureDataFormat.cs | 2 +- .../Security/UmbracoSecurityStampValidator.cs | 2 +- .../Security/UserAwarePasswordHasher.cs | 2 +- src/Umbraco.Web/Security/WebSecurity.cs | 3 +- src/Umbraco.Web/Umbraco.Web.csproj | 8 - .../CheckIfUserTicketDataIsStaleAttribute.cs | 3 +- .../WebApi/UmbracoAuthorizedApiController.cs | 8 +- 55 files changed, 158 insertions(+), 1076 deletions(-) rename src/{Umbraco.Infrastructure/BackOffice => Umbraco.Core/Models/Identity}/IdentityUser.cs (98%) rename src/{Umbraco.Web/Security => Umbraco.Infrastructure/BackOffice}/BackOfficeClaimsPrincipalFactory.cs (95%) rename src/{Umbraco.Tests/Security => Umbraco.Tests.Integration/Umbraco.Web.BackOffice}/NopLookupNormalizerTests.cs (97%) rename src/{Umbraco.Tests/Security => Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice}/BackOfficeClaimsPrincipalFactoryTests.cs (95%) create mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/NopLookupNormalizerTests.cs rename src/{Umbraco.Tests/Security => Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice}/UmbracoBackOfficeIdentityTests.cs (97%) delete mode 100644 src/Umbraco.Web/Models/Identity/BackOfficeIdentityUser.cs delete mode 100644 src/Umbraco.Web/Models/Identity/IdentityMapDefinition.cs delete mode 100644 src/Umbraco.Web/Models/Identity/IdentityUser.cs delete mode 100644 src/Umbraco.Web/Models/Identity/UserLoginInfoWrapper.cs delete mode 100644 src/Umbraco.Web/Security/IUserSessionStore.cs delete mode 100644 src/Umbraco.Web/Security/NopLookupNormalizer.cs delete mode 100644 src/Umbraco.Web/Security/UmbracoBackOfficeIdentity.cs diff --git a/src/Umbraco.Infrastructure/BackOffice/IdentityUser.cs b/src/Umbraco.Core/Models/Identity/IdentityUser.cs similarity index 98% rename from src/Umbraco.Infrastructure/BackOffice/IdentityUser.cs rename to src/Umbraco.Core/Models/Identity/IdentityUser.cs index 9de30360ae..093e42c1e7 100644 --- a/src/Umbraco.Infrastructure/BackOffice/IdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityUser.cs @@ -1,8 +1,7 @@ using System; using System.Collections.Generic; -using Umbraco.Core.Models.Identity; -namespace Umbraco.Core.BackOffice +namespace Umbraco.Core.Models.Identity { /// /// Default IUser implementation diff --git a/src/Umbraco.Web/Security/BackOfficeClaimsPrincipalFactory.cs b/src/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactory.cs similarity index 95% rename from src/Umbraco.Web/Security/BackOfficeClaimsPrincipalFactory.cs rename to src/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactory.cs index fe22981831..a463a84d4b 100644 --- a/src/Umbraco.Web/Security/BackOfficeClaimsPrincipalFactory.cs +++ b/src/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactory.cs @@ -4,10 +4,9 @@ using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; -using Umbraco.Core.Security; -using Umbraco.Web.Models.Identity; +using Umbraco.Core.BackOffice; -namespace Umbraco.Web.Security +namespace Umbraco.Core.BackOffice { public class BackOfficeClaimsPrincipalFactory : UserClaimsPrincipalFactory where TUser : BackOfficeIdentityUser diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityUser.cs b/src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityUser.cs index 4eca7a5e57..ea160ef1cf 100644 --- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Infrastructure/BackOffice/BackOfficeIdentityUser.cs @@ -89,8 +89,8 @@ namespace Umbraco.Core.BackOffice /// public bool HasIdentity => _hasIdentity; - public int[] CalculatedMediaStartNodeIds { get; internal set; } - public int[] CalculatedContentStartNodeIds { get; internal set; } + public int[] CalculatedMediaStartNodeIds { get; set; } + public int[] CalculatedContentStartNodeIds { get; set; } public override int Id { @@ -257,7 +257,7 @@ namespace Umbraco.Core.BackOffice /// /// Based on the user's lockout end date, this will determine if they are locked out /// - internal bool IsLockedOut + public bool IsLockedOut { get { @@ -269,7 +269,7 @@ namespace Umbraco.Core.BackOffice /// /// This is a 1:1 mapping with IUser.IsApproved /// - internal bool IsApproved { get; set; } + public bool IsApproved { get; set; } /// /// Overridden to make the retrieval lazy diff --git a/src/Umbraco.Infrastructure/BackOffice/UserLoginInfoWrapper.cs b/src/Umbraco.Infrastructure/BackOffice/UserLoginInfoWrapper.cs index ab6af35519..a441d0299a 100644 --- a/src/Umbraco.Infrastructure/BackOffice/UserLoginInfoWrapper.cs +++ b/src/Umbraco.Infrastructure/BackOffice/UserLoginInfoWrapper.cs @@ -3,7 +3,7 @@ using Umbraco.Core.Models.Identity; namespace Umbraco.Core.BackOffice { - internal class UserLoginInfoWrapper : IUserLoginInfo + public class UserLoginInfoWrapper : IUserLoginInfo { private readonly UserLoginInfo _info; diff --git a/src/Umbraco.Tests/Security/NopLookupNormalizerTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/NopLookupNormalizerTests.cs similarity index 97% rename from src/Umbraco.Tests/Security/NopLookupNormalizerTests.cs rename to src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/NopLookupNormalizerTests.cs index 2abecbb4dd..1c4e08a4de 100644 --- a/src/Umbraco.Tests/Security/NopLookupNormalizerTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/NopLookupNormalizerTests.cs @@ -1,6 +1,6 @@ using System; using NUnit.Framework; -using Umbraco.Web.Security; +using Umbraco.Core.BackOffice; namespace Umbraco.Tests.Security { diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs index 3b61f31b66..51fce283f8 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UmbracoBackOfficeServiceCollectionExtensionsTests.cs @@ -1,16 +1,9 @@ using System; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Moq; using NUnit.Framework; using Umbraco.Extensions; using Umbraco.Core.BackOffice; -using Umbraco.Core.Cache; -using Umbraco.Core.Composing; -using Umbraco.Tests.Integration.Extensions; -using Umbraco.Tests.Integration.Implementations; using Umbraco.Tests.Integration.Testing; namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Extensions @@ -27,6 +20,15 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Extensions Assert.AreEqual(typeof(BackOfficeUserStore), userStore.GetType()); } + [Test] + public void AddUmbracoBackOfficeIdentity_ExpectBackOfficeClaimsPrincipalFactoryResolvable() + { + var principalFactory = Services.GetService>(); + + Assert.IsNotNull(principalFactory); + Assert.AreEqual(typeof(BackOfficeClaimsPrincipalFactory), principalFactory.GetType()); + } + [Test] public void AddUmbracoBackOfficeIdentity_ExpectBackOfficeUserManagerResolvable() { diff --git a/src/Umbraco.Tests/Security/BackOfficeClaimsPrincipalFactoryTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs similarity index 95% rename from src/Umbraco.Tests/Security/BackOfficeClaimsPrincipalFactoryTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs index b7b516318c..3c01f89554 100644 --- a/src/Umbraco.Tests/Security/BackOfficeClaimsPrincipalFactoryTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs @@ -7,13 +7,12 @@ using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration; using Umbraco.Core.Models.Membership; -using Umbraco.Core.Security; -using Umbraco.Web.Models.Identity; -using Umbraco.Web.Security; +using Umbraco.Extensions; -namespace Umbraco.Tests.Security +namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice { [TestFixture] public class BackOfficeClaimsPrincipalFactoryTests @@ -131,7 +130,7 @@ namespace Umbraco.Tests.Security const string expectedClaimType = ClaimTypes.Role; const string expectedClaimValue = "b87309fb-4caf-48dc-b45a-2b752d051508"; - _testUser.Roles.Add(new Core.Models.Identity.IdentityUserRole{RoleId = expectedClaimValue}); + _testUser.Roles.Add(new global::Umbraco.Core.Models.Identity.IdentityUserRole{RoleId = expectedClaimValue}); _mockUserManager.Setup(x => x.SupportsUserRole).Returns(true); _mockUserManager.Setup(x => x.GetRolesAsync(_testUser)).ReturnsAsync(new[] {expectedClaimValue}); @@ -148,7 +147,7 @@ namespace Umbraco.Tests.Security const string expectedClaimType = "custom"; const string expectedClaimValue = "val"; - _testUser.Claims.Add(new Core.Models.Identity.IdentityUserClaim {ClaimType = expectedClaimType, ClaimValue = expectedClaimValue}); + _testUser.Claims.Add(new global::Umbraco.Core.Models.Identity.IdentityUserClaim {ClaimType = expectedClaimType, ClaimValue = expectedClaimValue}); _mockUserManager.Setup(x => x.SupportsUserClaim).Returns(true); _mockUserManager.Setup(x => x.GetClaimsAsync(_testUser)).ReturnsAsync( new List {new Claim(expectedClaimType, expectedClaimValue)}); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/NopLookupNormalizerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/NopLookupNormalizerTests.cs new file mode 100644 index 0000000000..e492b060b5 --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/NopLookupNormalizerTests.cs @@ -0,0 +1,56 @@ +using System; +using NUnit.Framework; +using Umbraco.Core.BackOffice; + +namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice +{ + public class NopLookupNormalizerTests + { + [Test] + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + public void NormalizeName_When_Name_Null_Or_Whitespace_Expect_Same_Returned(string name) + { + var sut = new NopLookupNormalizer(); + + var normalizedName = sut.NormalizeName(name); + + Assert.AreEqual(name, normalizedName); + } + + [Test] + public void NormalizeName_Expect_Input_Returned() + { + var name = Guid.NewGuid().ToString(); + var sut = new NopLookupNormalizer(); + + var normalizedName = sut.NormalizeName(name); + + Assert.AreEqual(name, normalizedName); + } + [Test] + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + public void NormalizeEmail_When_Name_Null_Or_Whitespace_Expect_Same_Returned(string email) + { + var sut = new NopLookupNormalizer(); + + var normalizedEmail = sut.NormalizeEmail(email); + + Assert.AreEqual(email, normalizedEmail); + } + + [Test] + public void NormalizeEmail_Expect_Input_Returned() + { + var email = $"{Guid.NewGuid()}@umbraco"; + var sut = new NopLookupNormalizer(); + + var normalizedEmail = sut.NormalizeEmail(email); + + Assert.AreEqual(email, normalizedEmail); + } + } +} diff --git a/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentityTests.cs similarity index 97% rename from src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentityTests.cs index 9c16d0c35a..5d0cec0e6e 100644 --- a/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentityTests.cs @@ -1,16 +1,11 @@ using System; using System.Linq; using System.Security.Claims; -using System.Text; -using System.Threading.Tasks; -using System.Web.Security; -using Newtonsoft.Json; using NUnit.Framework; using Umbraco.Core; -using Umbraco.Core.Security; -using Umbraco.Core.Services; +using Umbraco.Core.BackOffice; -namespace Umbraco.Tests.Security +namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice { [TestFixture] public class UmbracoBackOfficeIdentityTests diff --git a/src/Umbraco.Tests/Security/BackOfficeUserManagerTests.cs b/src/Umbraco.Tests/Security/BackOfficeUserManagerTests.cs index 30ed101297..99a2b323dd 100644 --- a/src/Umbraco.Tests/Security/BackOfficeUserManagerTests.cs +++ b/src/Umbraco.Tests/Security/BackOfficeUserManagerTests.cs @@ -6,11 +6,11 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Owin.Security.DataProtection; using Moq; using NUnit.Framework; +using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration; using Umbraco.Core.Models.Membership; using Umbraco.Net; -using Umbraco.Web.Models.Identity; -using Umbraco.Web.Security; +using BackOfficeUserManager = Umbraco.Web.Security.BackOfficeUserManager; namespace Umbraco.Tests.Security { diff --git a/src/Umbraco.Tests/Security/OwinDataProtectorTokenProviderTests.cs b/src/Umbraco.Tests/Security/OwinDataProtectorTokenProviderTests.cs index 5e9b731aa9..7b1ca53104 100644 --- a/src/Umbraco.Tests/Security/OwinDataProtectorTokenProviderTests.cs +++ b/src/Umbraco.Tests/Security/OwinDataProtectorTokenProviderTests.cs @@ -6,9 +6,9 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Owin.Security.DataProtection; using Moq; using NUnit.Framework; +using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration; using Umbraco.Core.Models.Membership; -using Umbraco.Web.Models.Identity; using Umbraco.Web.Security; namespace Umbraco.Tests.Security diff --git a/src/Umbraco.Tests/Security/UmbracoSecurityStampValidatorTests.cs b/src/Umbraco.Tests/Security/UmbracoSecurityStampValidatorTests.cs index 92fa032271..4adfe15ad7 100644 --- a/src/Umbraco.Tests/Security/UmbracoSecurityStampValidatorTests.cs +++ b/src/Umbraco.Tests/Security/UmbracoSecurityStampValidatorTests.cs @@ -11,18 +11,19 @@ using Microsoft.Owin.Security.Cookies; using Moq; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration; using Umbraco.Core.Models.Membership; using Umbraco.Net; -using Umbraco.Web.Models.Identity; using Umbraco.Web.Security; + namespace Umbraco.Tests.Security { public class UmbracoSecurityStampValidatorTests { private Mock _mockOwinContext; - private Mock> _mockUserManager; + private Mock> _mockUserManager; private Mock _mockSignInManager; private AuthenticationTicket _testAuthTicket; @@ -34,7 +35,7 @@ namespace Umbraco.Tests.Security public void OnValidateIdentity_When_GetUserIdCallback_Is_Null_Expect_ArgumentNullException() { Assert.Throws(() => UmbracoSecurityStampValidator - .OnValidateIdentity, BackOfficeIdentityUser>( + .OnValidateIdentity, BackOfficeIdentityUser>( TimeSpan.MaxValue, null, null)); } @@ -42,7 +43,7 @@ namespace Umbraco.Tests.Security public async Task OnValidateIdentity_When_Validation_Interval_Not_Met_Expect_No_Op() { var func = UmbracoSecurityStampValidator - .OnValidateIdentity, BackOfficeIdentityUser>( + .OnValidateIdentity, BackOfficeIdentityUser>( TimeSpan.MaxValue, null, identity => throw new Exception()); _testAuthTicket.Properties.IssuedUtc = DateTimeOffset.UtcNow; @@ -61,11 +62,11 @@ namespace Umbraco.Tests.Security public void OnValidateIdentity_When_Time_To_Validate_But_No_UserManager_Expect_InvalidOperationException() { var func = UmbracoSecurityStampValidator - .OnValidateIdentity, BackOfficeIdentityUser>( + .OnValidateIdentity, BackOfficeIdentityUser>( TimeSpan.MinValue, null, identity => throw new Exception()); - _mockOwinContext.Setup(x => x.Get>(It.IsAny())) - .Returns((BackOfficeUserManager) null); + _mockOwinContext.Setup(x => x.Get>(It.IsAny())) + .Returns((Umbraco.Web.Security.BackOfficeUserManager) null); var context = new CookieValidateIdentityContext( _mockOwinContext.Object, @@ -79,7 +80,7 @@ namespace Umbraco.Tests.Security public void OnValidateIdentity_When_Time_To_Validate_But_No_SignInManager_Expect_InvalidOperationException() { var func = UmbracoSecurityStampValidator - .OnValidateIdentity, BackOfficeIdentityUser>( + .OnValidateIdentity, BackOfficeIdentityUser>( TimeSpan.MinValue, null, identity => throw new Exception()); _mockOwinContext.Setup(x => x.Get(It.IsAny())) @@ -99,7 +100,7 @@ namespace Umbraco.Tests.Security var userId = Guid.NewGuid().ToString(); var func = UmbracoSecurityStampValidator - .OnValidateIdentity, BackOfficeIdentityUser>( + .OnValidateIdentity, BackOfficeIdentityUser>( TimeSpan.MinValue, null, identity => userId); _mockUserManager.Setup(x => x.FindByIdAsync(userId)) @@ -122,7 +123,7 @@ namespace Umbraco.Tests.Security var userId = Guid.NewGuid().ToString(); var func = UmbracoSecurityStampValidator - .OnValidateIdentity, BackOfficeIdentityUser>( + .OnValidateIdentity, BackOfficeIdentityUser>( TimeSpan.MinValue, null, identity => userId); _mockUserManager.Setup(x => x.FindByIdAsync(userId)).ReturnsAsync(_testUser); @@ -145,7 +146,7 @@ namespace Umbraco.Tests.Security var userId = Guid.NewGuid().ToString(); var func = UmbracoSecurityStampValidator - .OnValidateIdentity, BackOfficeIdentityUser>( + .OnValidateIdentity, BackOfficeIdentityUser>( TimeSpan.MinValue, null, identity => userId); _mockUserManager.Setup(x => x.FindByIdAsync(userId)).ReturnsAsync(_testUser); @@ -169,7 +170,7 @@ namespace Umbraco.Tests.Security var userId = Guid.NewGuid().ToString(); var func = UmbracoSecurityStampValidator - .OnValidateIdentity, BackOfficeIdentityUser>( + .OnValidateIdentity, BackOfficeIdentityUser>( TimeSpan.MinValue, null, identity => userId); _mockUserManager.Setup(x => x.FindByIdAsync(userId)).ReturnsAsync(_testUser); @@ -193,7 +194,7 @@ namespace Umbraco.Tests.Security var expectedIdentity = new ClaimsIdentity(new List {new Claim("sub", "bob")}); var regenFuncCalled = false; - Func, BackOfficeIdentityUser, Task> regenFunc = + Func, BackOfficeIdentityUser, Task> regenFunc = (signInManager, userManager, user) => { regenFuncCalled = true; @@ -201,7 +202,7 @@ namespace Umbraco.Tests.Security }; var func = UmbracoSecurityStampValidator - .OnValidateIdentity, BackOfficeIdentityUser>( + .OnValidateIdentity, BackOfficeIdentityUser>( TimeSpan.MinValue, regenFunc, identity => userId); _mockUserManager.Setup(x => x.FindByIdAsync(userId)).ReturnsAsync(_testUser); @@ -249,7 +250,7 @@ namespace Umbraco.Tests.Security new AuthenticationProperties()); _testOptions = new CookieAuthenticationOptions { AuthenticationType = _testAuthType }; - _mockUserManager = new Mock>( + _mockUserManager = new Mock>( new Mock().Object, new Mock().Object, new Mock>().Object, @@ -266,7 +267,7 @@ namespace Umbraco.Tests.Security new Mock().Object); _mockOwinContext = new Mock(); - _mockOwinContext.Setup(x => x.Get>(It.IsAny())) + _mockOwinContext.Setup(x => x.Get>(It.IsAny())) .Returns(_mockUserManager.Object); _mockOwinContext.Setup(x => x.Get(It.IsAny())) .Returns(_mockSignInManager.Object); diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingMiddleware.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingMiddleware.cs index 5031d178bf..72941633e7 100644 --- a/src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingMiddleware.cs +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingMiddleware.cs @@ -4,7 +4,7 @@ using Microsoft.Owin; using Microsoft.Owin.Security; using Microsoft.Owin.Security.Infrastructure; using Owin; -using Umbraco.Core.Security; +using Umbraco.Core.BackOffice; namespace Umbraco.Tests.TestHelpers.ControllerTesting { diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs index 66310caea9..793d31a5d3 100644 --- a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs @@ -8,6 +8,7 @@ using System.Web.Http.Controllers; using System.Web.Http.Dispatcher; using System.Web.Security; using Moq; +using Umbraco.Core.BackOffice; using Umbraco.Core.Cache; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Dictionary; diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 77aba483d7..22f5348501 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -147,10 +147,8 @@ - - @@ -275,7 +273,6 @@ - diff --git a/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs index 8c40085e50..80deb5a800 100644 --- a/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs @@ -28,7 +28,6 @@ using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.ControllerTesting; @@ -46,8 +45,8 @@ using Umbraco.Web.Routing; using Umbraco.Core.Media; using Umbraco.Net; using Umbraco.Persistance.SqlCe; -using Umbraco.Web.Models.Identity; using Umbraco.Web.Security; +using BackOfficeIdentityUser = Umbraco.Core.BackOffice.BackOfficeIdentityUser; namespace Umbraco.Tests.Web.Controllers { diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs index bce46b3129..d776749e6b 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs @@ -14,7 +14,6 @@ using Umbraco.Core.WebAssets; using Umbraco.Net; using Umbraco.Web.BackOffice.ActionResults; using Umbraco.Web.BackOffice.Filters; -using Umbraco.Web.BackOffice.Identity; using Umbraco.Web.Common.ActionResults; using Umbraco.Web.WebAssets; diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs index 9d3e2a9e3f..4325e10622 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using System.Security.Claims; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Umbraco.Core; using Umbraco.Core.BackOffice; using Umbraco.Core.Mapping; @@ -14,14 +15,17 @@ namespace Umbraco.Extensions { public static void AddUmbracoBackOfficeIdentity(this IServiceCollection services) { + services.AddDataProtection(); + // UmbracoMapper - hack? - services.AddSingleton(); - services.AddSingleton(s => new MapDefinitionCollection(new[] {s.GetService()})); - services.AddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(s => new MapDefinitionCollection(new[] {s.GetService()})); + services.TryAddSingleton(); - services.AddScoped(); + services.TryAddScoped(); - services.AddIdentity(options => + services.AddIdentityCore(); + services.AddIdentityCore(options => { options.User.RequireUniqueEmail = true; @@ -43,9 +47,12 @@ namespace Umbraco.Extensions }) .AddDefaultTokenProviders() .AddUserStore() - .AddUserManager(); + .AddUserManager() + .AddClaimsPrincipalFactory>(); + + services.AddScoped(); + services.TryAddScoped>(); - // .AddClaimsPrincipalFactory>() // TODO: extract custom claims principal factory } } } diff --git a/src/Umbraco.Web/Compose/AuditEventsComponent.cs b/src/Umbraco.Web/Compose/AuditEventsComponent.cs index bd2520aa90..51c47233c7 100644 --- a/src/Umbraco.Web/Compose/AuditEventsComponent.cs +++ b/src/Umbraco.Web/Compose/AuditEventsComponent.cs @@ -8,10 +8,9 @@ using Umbraco.Core.Events; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Net; -using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; -using Umbraco.Web.Security; +using Umbraco.Extensions; namespace Umbraco.Core.Compose { diff --git a/src/Umbraco.Web/Composing/CompositionExtensions/WebMappingProfiles.cs b/src/Umbraco.Web/Composing/CompositionExtensions/WebMappingProfiles.cs index 21a242ee17..44d86df59c 100644 --- a/src/Umbraco.Web/Composing/CompositionExtensions/WebMappingProfiles.cs +++ b/src/Umbraco.Web/Composing/CompositionExtensions/WebMappingProfiles.cs @@ -1,7 +1,7 @@ using Umbraco.Core; +using Umbraco.Core.BackOffice; using Umbraco.Core.Composing; using Umbraco.Core.Mapping; -using Umbraco.Web.Models.Identity; using Umbraco.Web.Models.Mapping; namespace Umbraco.Web.Composing.CompositionExtensions diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index 009d970fd5..b38bbbb19d 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -19,15 +19,14 @@ using Umbraco.Web.Mvc; using Umbraco.Web.Security; using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; +using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using IUser = Umbraco.Core.Models.Membership.IUser; using Umbraco.Core.Mapping; -using Umbraco.Web.Models.Identity; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Hosting; -using Umbraco.Core.IO; using Umbraco.Web.Routing; namespace Umbraco.Web.Editors @@ -41,7 +40,7 @@ namespace Umbraco.Web.Editors [IsBackOffice] public class AuthenticationController : UmbracoApiController { - private BackOfficeUserManager _userManager; + private Security.BackOfficeUserManager _userManager; private BackOfficeSignInManager _signInManager; private readonly IUserPasswordConfiguration _passwordConfiguration; private readonly IHostingEnvironment _hostingEnvironment; @@ -72,8 +71,8 @@ namespace Umbraco.Web.Editors _requestAccessor = requestAccessor ?? throw new ArgumentNullException(nameof(securitySettings)); } - protected BackOfficeUserManager UserManager => _userManager - ?? (_userManager = TryGetOwinContext().Result.GetBackOfficeUserManager()); + protected Security.BackOfficeUserManager UserManager => _userManager + ?? (_userManager = TryGetOwinContext().Result.GetBackOfficeUserManager()); protected BackOfficeSignInManager SignInManager => _signInManager ?? (_signInManager = TryGetOwinContext().Result.GetBackOfficeSignInManager()); diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 825b67767a..782dea61ac 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -20,7 +20,6 @@ using Umbraco.Web.Models; using Umbraco.Web.Mvc; using Umbraco.Core.Services; using Umbraco.Web.Features; -using Umbraco.Web.Models.Identity; using Umbraco.Web.Security; using Constants = Umbraco.Core.Constants; using JArray = Newtonsoft.Json.Linq.JArray; @@ -32,6 +31,7 @@ using Umbraco.Core.Runtime; using Umbraco.Core.WebAssets; using Umbraco.Web.Trees; using Umbraco.Web.WebAssets; +using BackOfficeIdentityUser = Umbraco.Core.BackOffice.BackOfficeIdentityUser; namespace Umbraco.Web.Editors { diff --git a/src/Umbraco.Web/Editors/PasswordChanger.cs b/src/Umbraco.Web/Editors/PasswordChanger.cs index b6757f76cc..10a5248045 100644 --- a/src/Umbraco.Web/Editors/PasswordChanger.cs +++ b/src/Umbraco.Web/Editors/PasswordChanger.cs @@ -2,12 +2,10 @@ using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Umbraco.Core; +using Umbraco.Core.BackOffice; using Umbraco.Core.Logging; using Umbraco.Core.Models; -using Umbraco.Core.Models.Identity; -using Umbraco.Core.Security; using Umbraco.Web.Models; -using Umbraco.Web.Models.Identity; using Umbraco.Web.Security; using IUser = Umbraco.Core.Models.Membership.IUser; @@ -34,7 +32,7 @@ namespace Umbraco.Web.Editors IUser currentUser, IUser savingUser, ChangingPasswordModel passwordModel, - BackOfficeUserManager userMgr) + Security.BackOfficeUserManager userMgr) { if (passwordModel == null) throw new ArgumentNullException(nameof(passwordModel)); if (userMgr == null) throw new ArgumentNullException(nameof(userMgr)); diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index e695139b8c..8a3156dd2d 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -11,6 +11,7 @@ using System.Web; using System.Web.Http; using System.Web.Mvc; using Umbraco.Core; +using Umbraco.Core.BackOffice; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.IO; @@ -24,7 +25,6 @@ using Umbraco.Core.Strings; using Umbraco.Web.Editors.Filters; using Umbraco.Web.Models; using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Models.Identity; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; diff --git a/src/Umbraco.Web/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Web/Models/Identity/BackOfficeIdentityUser.cs deleted file mode 100644 index 21c965aa3d..0000000000 --- a/src/Umbraco.Web/Models/Identity/BackOfficeIdentityUser.cs +++ /dev/null @@ -1,434 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Collections.Specialized; -using System.ComponentModel; -using System.Linq; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Models.Entities; -using Umbraco.Core.Models.Identity; -using Umbraco.Core.Models.Membership; - -namespace Umbraco.Web.Models.Identity -{ - public class BackOfficeIdentityUser : IdentityUser, IdentityUserClaim>, IRememberBeingDirty - { - private string _email; - private string _userName; - private int _id; - private bool _hasIdentity; - private DateTime? _lastLoginDateUtc; - private bool _emailConfirmed; - private string _name; - private int _accessFailedCount; - private string _passwordHash; - private string _culture; - private ObservableCollection _logins; - private Lazy> _getLogins; - private IReadOnlyUserGroup[] _groups; - private string[] _allowedSections; - private int[] _startMediaIds; - private int[] _startContentIds; - private DateTime? _lastPasswordChangeDateUtc; - - /// - /// Used to construct a new instance without an identity - /// - /// - /// This is allowed to be null (but would need to be filled in if trying to persist this instance) - /// - /// - public static BackOfficeIdentityUser CreateNew(IGlobalSettings globalSettings, string username, string email, string culture) - { - if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); - if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture)); - - var user = new BackOfficeIdentityUser(globalSettings, Array.Empty()); - user.DisableChangeTracking(); - user._userName = username; - user._email = email; - //we are setting minvalue here because the default is "0" which is the id of the admin user - //which we cannot allow because the admin user will always exist - user._id = int.MinValue; - user._hasIdentity = false; - user._culture = culture; - user.EnableChangeTracking(); - return user; - } - - private BackOfficeIdentityUser(IGlobalSettings globalSettings, IReadOnlyUserGroup[] groups) - { - _startMediaIds = Array.Empty(); - _startContentIds = Array.Empty(); - _allowedSections = Array.Empty(); - _culture = globalSettings.DefaultUILanguage; - - // must initialize before setting groups - _roles = new ObservableCollection>(); - _roles.CollectionChanged += _roles_CollectionChanged; - - // use the property setters - they do more than just setting a field - Groups = groups; - } - - /// - /// Creates an existing user with the specified groups - /// - /// - /// - /// - public BackOfficeIdentityUser(IGlobalSettings globalSettings, int userId, IEnumerable groups) - : this(globalSettings, groups.ToArray()) - { - // use the property setters - they do more than just setting a field - Id = userId; - } - - /// - /// Returns true if an Id has been set on this object this will be false if the object is new and not persisted to the database - /// - public bool HasIdentity => _hasIdentity; - - public int[] CalculatedMediaStartNodeIds { get; internal set; } - public int[] CalculatedContentStartNodeIds { get; internal set; } - - public override int Id - { - get => _id; - set - { - _id = value; - _hasIdentity = true; - } - } - - /// - /// Override Email so we can track changes to it - /// - public override string Email - { - get => _email; - set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _email, nameof(Email)); - } - - /// - /// Override UserName so we can track changes to it - /// - public override string UserName - { - get => _userName; - set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _userName, nameof(UserName)); - } - - /// - /// LastPasswordChangeDateUtc so we can track changes to it - /// - public override DateTime? LastPasswordChangeDateUtc - { - get { return _lastPasswordChangeDateUtc; } - set { _beingDirty.SetPropertyValueAndDetectChanges(value, ref _lastPasswordChangeDateUtc, nameof(LastPasswordChangeDateUtc)); } - } - - /// - /// Override LastLoginDateUtc so we can track changes to it - /// - public override DateTime? LastLoginDateUtc - { - get => _lastLoginDateUtc; - set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _lastLoginDateUtc, nameof(LastLoginDateUtc)); - } - - /// - /// Override EmailConfirmed so we can track changes to it - /// - public override bool EmailConfirmed - { - get => _emailConfirmed; - set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _emailConfirmed, nameof(EmailConfirmed)); - } - - /// - /// Gets/sets the user's real name - /// - public string Name - { - get => _name; - set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name)); - } - - /// - /// Override AccessFailedCount so we can track changes to it - /// - public override int AccessFailedCount - { - get => _accessFailedCount; - set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _accessFailedCount, nameof(AccessFailedCount)); - } - - /// - /// Override PasswordHash so we can track changes to it - /// - public override string PasswordHash - { - get => _passwordHash; - set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordHash, nameof(PasswordHash)); - } - - - /// - /// Content start nodes assigned to the User (not ones assigned to the user's groups) - /// - public int[] StartContentIds - { - get => _startContentIds; - set - { - if (value == null) value = new int[0]; - _beingDirty.SetPropertyValueAndDetectChanges(value, ref _startContentIds, nameof(StartContentIds), StartIdsComparer); - } - } - - /// - /// Media start nodes assigned to the User (not ones assigned to the user's groups) - /// - public int[] StartMediaIds - { - get => _startMediaIds; - set - { - if (value == null) value = new int[0]; - _beingDirty.SetPropertyValueAndDetectChanges(value, ref _startMediaIds, nameof(StartMediaIds), StartIdsComparer); - } - } - - /// - /// This is a readonly list of the user's allowed sections which are based on it's user groups - /// - public string[] AllowedSections - { - get { return _allowedSections ?? (_allowedSections = _groups.SelectMany(x => x.AllowedSections).Distinct().ToArray()); } - } - - public string Culture - { - get => _culture; - set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _culture, nameof(Culture)); - } - - public IReadOnlyUserGroup[] Groups - { - get => _groups; - set - { - //so they recalculate - _allowedSections = null; - - _groups = value; - - //now clear all roles and re-add them - _roles.CollectionChanged -= _roles_CollectionChanged; - _roles.Clear(); - foreach (var identityUserRole in _groups.Select(x => new IdentityUserRole - { - RoleId = x.Alias, - UserId = Id.ToString() - })) - { - _roles.Add(identityUserRole); - } - _roles.CollectionChanged += _roles_CollectionChanged; - - _beingDirty.SetPropertyValueAndDetectChanges(value, ref _groups, nameof(Groups), GroupsComparer); - } - } - - /// - /// Lockout is always enabled - /// - public override bool LockoutEnabled - { - get { return true; } - set - { - //do nothing - } - } - - /// - /// Based on the user's lockout end date, this will determine if they are locked out - /// - internal bool IsLockedOut - { - get - { - var isLocked = LockoutEndDateUtc.HasValue && LockoutEndDateUtc.Value.ToLocalTime() >= DateTime.Now; - return isLocked; - } - } - - /// - /// This is a 1:1 mapping with IUser.IsApproved - /// - internal bool IsApproved { get; set; } - - /// - /// Overridden to make the retrieval lazy - /// - public override ICollection Logins - { - get - { - if (_getLogins != null && _getLogins.IsValueCreated == false) - { - _logins = new ObservableCollection(); - foreach (var l in _getLogins.Value) - { - _logins.Add(l); - } - //now assign events - _logins.CollectionChanged += Logins_CollectionChanged; - } - return _logins; - } - } - - void Logins_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - _beingDirty.OnPropertyChanged(nameof(Logins)); - } - - private void _roles_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - _beingDirty.OnPropertyChanged(nameof(Roles)); - } - - private readonly ObservableCollection> _roles; - - /// - /// helper method to easily add a role without having to deal with IdentityUserRole{T} - /// - /// - /// - /// Adding a role this way will not reflect on the user's group's collection or it's allowed sections until the user is persisted - /// - public void AddRole(string role) - { - Roles.Add(new IdentityUserRole - { - UserId = Id.ToString(), - RoleId = role - }); - } - - /// - /// Override Roles because the value of these are the user's group aliases - /// - public override ICollection> Roles => _roles; - - /// - /// Used to set a lazy call back to populate the user's Login list - /// - /// - public void SetLoginsCallback(Lazy> callback) - { - _getLogins = callback ?? throw new ArgumentNullException(nameof(callback)); - } - - #region BeingDirty - - private readonly BeingDirty _beingDirty = new BeingDirty(); - - /// - public bool IsDirty() - { - return _beingDirty.IsDirty(); - } - - /// - public bool IsPropertyDirty(string propName) - { - return _beingDirty.IsPropertyDirty(propName); - } - - /// - public IEnumerable GetDirtyProperties() - { - return _beingDirty.GetDirtyProperties(); - } - - /// - public void ResetDirtyProperties() - { - _beingDirty.ResetDirtyProperties(); - } - - /// - public bool WasDirty() - { - return _beingDirty.WasDirty(); - } - - /// - public bool WasPropertyDirty(string propertyName) - { - return _beingDirty.WasPropertyDirty(propertyName); - } - - /// - public void ResetWereDirtyProperties() - { - _beingDirty.ResetWereDirtyProperties(); - } - - /// - public void ResetDirtyProperties(bool rememberDirty) - { - _beingDirty.ResetDirtyProperties(rememberDirty); - } - - /// - public IEnumerable GetWereDirtyProperties() - => _beingDirty.GetWereDirtyProperties(); - - /// - /// Disables change tracking. - /// - public void DisableChangeTracking() - { - _beingDirty.DisableChangeTracking(); - } - - /// - /// Enables change tracking. - /// - public void EnableChangeTracking() - { - _beingDirty.EnableChangeTracking(); - } - - public event PropertyChangedEventHandler PropertyChanged - { - add - { - _beingDirty.PropertyChanged += value; - } - remove - { - _beingDirty.PropertyChanged -= value; - } - } - - #endregion - - //Custom comparer for enumerables - private static readonly DelegateEqualityComparer GroupsComparer = new DelegateEqualityComparer( - (groups, enumerable) => groups.Select(x => x.Alias).UnsortedSequenceEqual(enumerable.Select(x => x.Alias)), - groups => groups.GetHashCode()); - - private static readonly DelegateEqualityComparer StartIdsComparer = new DelegateEqualityComparer( - (groups, enumerable) => groups.UnsortedSequenceEqual(enumerable), - groups => groups.GetHashCode()); - - } -} diff --git a/src/Umbraco.Web/Models/Identity/IdentityMapDefinition.cs b/src/Umbraco.Web/Models/Identity/IdentityMapDefinition.cs deleted file mode 100644 index 7c20c6108a..0000000000 --- a/src/Umbraco.Web/Models/Identity/IdentityMapDefinition.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Mapping; -using Umbraco.Core.Models; -using Umbraco.Core.Models.Membership; -using Umbraco.Core.Services; - -namespace Umbraco.Web.Models.Identity -{ - public class IdentityMapDefinition : IMapDefinition - { - private readonly ILocalizedTextService _textService; - private readonly IEntityService _entityService; - private readonly IGlobalSettings _globalSettings; - - public IdentityMapDefinition(ILocalizedTextService textService, IEntityService entityService, IGlobalSettings globalSettings) - { - _textService = textService; - _entityService = entityService; - _globalSettings = globalSettings; - } - - public void DefineMaps(UmbracoMapper mapper) - { - mapper.Define( - (source, context) => - { - var target = new BackOfficeIdentityUser(_globalSettings, source.Id, source.Groups); - target.DisableChangeTracking(); - return target; - }, - (source, target, context) => - { - Map(source, target); - target.ResetDirtyProperties(true); - target.EnableChangeTracking(); - }); - } - - // Umbraco.Code.MapAll -Id -Groups -LockoutEnabled -PhoneNumber -PhoneNumberConfirmed -TwoFactorEnabled - private void Map(IUser source, BackOfficeIdentityUser target) - { - // well, the ctor has been fixed - /* - // these two are already set in ctor but BackOfficeIdentityUser ctor is CompletelyBroken - target.Id = source.Id; - target.Groups = source.Groups.ToArray(); - */ - - target.CalculatedMediaStartNodeIds = source.CalculateMediaStartNodeIds(_entityService); - target.CalculatedContentStartNodeIds = source.CalculateContentStartNodeIds(_entityService); - target.Email = source.Email; - target.UserName = source.Username; - target.LastPasswordChangeDateUtc = source.LastPasswordChangeDate.ToUniversalTime(); - target.LastLoginDateUtc = source.LastLoginDate.ToUniversalTime(); - target.EmailConfirmed = source.EmailConfirmedDate.HasValue; - target.Name = source.Name; - target.AccessFailedCount = source.FailedPasswordAttempts; - target.PasswordHash = GetPasswordHash(source.RawPasswordValue); - target.StartContentIds = source.StartContentIds; - target.StartMediaIds = source.StartMediaIds; - target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); // project CultureInfo to string - target.IsApproved = source.IsApproved; - target.SecurityStamp = source.SecurityStamp; - target.LockoutEndDateUtc = source.IsLockedOut ? DateTime.MaxValue.ToUniversalTime() : (DateTime?) null; - - // this was in AutoMapper but does not have a setter anyways - //target.AllowedSections = source.AllowedSections.ToArray(), - - // these were marked as ignored for AutoMapper but don't have a setter anyways - //target.Logins =; - //target.Claims =; - //target.Roles =; - } - - private static string GetPasswordHash(string storedPass) - { - return storedPass.StartsWith(Constants.Security.EmptyPasswordPrefix) ? null : storedPass; - } - } -} diff --git a/src/Umbraco.Web/Models/Identity/IdentityUser.cs b/src/Umbraco.Web/Models/Identity/IdentityUser.cs deleted file mode 100644 index 7bd077e879..0000000000 --- a/src/Umbraco.Web/Models/Identity/IdentityUser.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using System.Collections.Generic; -using Umbraco.Core.Models.Identity; - -namespace Umbraco.Web.Models.Identity -{ - /// - /// Default IUser implementation - /// - /// - /// - /// This class normally exists inside of the EntityFramework library, not sure why MS chose to explicitly put it there but we don't want - /// references to that so we will create our own here - /// - public class IdentityUser - where TLogin : IIdentityUserLogin - //NOTE: Making our role id a string - where TRole : IdentityUserRole - where TClaim : IdentityUserClaim - { - /// - /// Initializes a new instance of the class. - /// - public IdentityUser() - { - Claims = new List(); - Roles = new List(); - Logins = new List(); - } - - /// - /// Last login date - /// - public virtual DateTime? LastLoginDateUtc { get; set; } - - /// - /// Email - /// - public virtual string Email { get; set; } - - /// - /// True if the email is confirmed, default is false - /// - public virtual bool EmailConfirmed { get; set; } - - /// - /// The salted/hashed form of the user password - /// - public virtual string PasswordHash { get; set; } - - /// - /// A random value that should change whenever a users credentials have changed (password changed, login removed) - /// - public virtual string SecurityStamp { get; set; } - - /// - /// PhoneNumber for the user - /// - public virtual string PhoneNumber { get; set; } - - /// - /// True if the phone number is confirmed, default is false - /// - public virtual bool PhoneNumberConfirmed { get; set; } - - /// - /// Is two factor enabled for the user - /// - public virtual bool TwoFactorEnabled { get; set; } - - /// - /// DateTime in UTC when lockout ends, any time in the past is considered not locked out. - /// - public virtual DateTime? LockoutEndDateUtc { get; set; } - - /// - /// DateTime in UTC when the password was last changed. - /// - public virtual DateTime? LastPasswordChangeDateUtc { get; set; } - - /// - /// Is lockout enabled for this user - /// - public virtual bool LockoutEnabled { get; set; } - - /// - /// Used to record failures for the purposes of lockout - /// - public virtual int AccessFailedCount { get; set; } - - /// - /// Navigation property for user roles - /// - public virtual ICollection Roles { get; } - - /// - /// Navigation property for user claims - /// - public virtual ICollection Claims { get; } - - /// - /// Navigation property for user logins - /// - public virtual ICollection Logins { get; } - - /// - /// User ID (Primary Key) - /// - public virtual TKey Id { get; set; } - - /// - /// User name - /// - public virtual string UserName { get; set; } - } -} diff --git a/src/Umbraco.Web/Models/Identity/UserLoginInfoWrapper.cs b/src/Umbraco.Web/Models/Identity/UserLoginInfoWrapper.cs deleted file mode 100644 index 336b4c9e72..0000000000 --- a/src/Umbraco.Web/Models/Identity/UserLoginInfoWrapper.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using Umbraco.Core.Models.Identity; - -namespace Umbraco.Web.Models.Identity -{ - internal class UserLoginInfoWrapper : IUserLoginInfo - { - private readonly UserLoginInfo _info; - - public static IUserLoginInfo Wrap(UserLoginInfo info) => new UserLoginInfoWrapper(info); - - private UserLoginInfoWrapper(UserLoginInfo info) - { - _info = info; - } - - public string LoginProvider - { - get => _info.LoginProvider; - set => _info.LoginProvider = value; - } - - public string ProviderKey - { - get => _info.ProviderKey; - set => _info.ProviderKey = value; - } - } -} diff --git a/src/Umbraco.Web/OwinExtensions.cs b/src/Umbraco.Web/OwinExtensions.cs index 52c1187707..67e9375ab5 100644 --- a/src/Umbraco.Web/OwinExtensions.cs +++ b/src/Umbraco.Web/OwinExtensions.cs @@ -3,8 +3,8 @@ using System.Web; using Microsoft.Owin; using Microsoft.Owin.Security; using Umbraco.Core; -using Umbraco.Web.Models.Identity; using Umbraco.Web.Security; +using BackOfficeIdentityUser = Umbraco.Core.BackOffice.BackOfficeIdentityUser; namespace Umbraco.Web { diff --git a/src/Umbraco.Web/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs b/src/Umbraco.Web/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs index af7aebce3a..930cabeee3 100644 --- a/src/Umbraco.Web/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs +++ b/src/Umbraco.Web/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs @@ -1,8 +1,8 @@ using System; using System.DirectoryServices.AccountManagement; using System.Threading.Tasks; +using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration; -using Umbraco.Web.Models.Identity; namespace Umbraco.Web.Security { diff --git a/src/Umbraco.Web/Security/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/AppBuilderExtensions.cs index 3f129f941d..6254dabeb9 100644 --- a/src/Umbraco.Web/Security/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/AppBuilderExtensions.cs @@ -11,6 +11,7 @@ using Microsoft.Owin.Security.DataHandler; using Microsoft.Owin.Security.DataProtection; using Owin; using Umbraco.Core; +using Umbraco.Core.BackOffice; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; @@ -19,7 +20,6 @@ using Umbraco.Core.Mapping; using Umbraco.Net; using Umbraco.Core.Services; using Umbraco.Web.Composing; -using Umbraco.Web.Models.Identity; using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Security diff --git a/src/Umbraco.Web/Security/AuthenticationExtensions.cs b/src/Umbraco.Web/Security/AuthenticationExtensions.cs index 7d76eaa7be..ee3b69c389 100644 --- a/src/Umbraco.Web/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Web/Security/AuthenticationExtensions.cs @@ -12,8 +12,9 @@ using Microsoft.Owin; using Microsoft.Owin.Security; using Newtonsoft.Json; using Umbraco.Core; +using Umbraco.Core.BackOffice; +using Umbraco.Extensions; using Umbraco.Web.Composing; -using Umbraco.Core.Security; using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Security @@ -333,38 +334,6 @@ namespace Umbraco.Web.Security return secureDataFormat.Unprotect(formsCookie); } - /// - /// This will return the current back office identity if the IPrincipal is the correct type - /// - /// - /// - public static UmbracoBackOfficeIdentity GetUmbracoIdentity(this IPrincipal user) - { - //If it's already a UmbracoBackOfficeIdentity - if (user.Identity is UmbracoBackOfficeIdentity backOfficeIdentity) return backOfficeIdentity; - - //Check if there's more than one identity assigned and see if it's a UmbracoBackOfficeIdentity and use that - if (user is ClaimsPrincipal claimsPrincipal) - { - backOfficeIdentity = claimsPrincipal.Identities.OfType().FirstOrDefault(); - if (backOfficeIdentity != null) return backOfficeIdentity; - } - - //Otherwise convert to a UmbracoBackOfficeIdentity if it's auth'd and has the back office session - if (user.Identity is ClaimsIdentity claimsIdentity && claimsIdentity.IsAuthenticated && claimsIdentity.HasClaim(x => x.Type == Constants.Security.SessionIdClaimType)) - { - try - { - return UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity); - } - catch (InvalidOperationException) - { - } - } - - return null; - } - /// /// Ensures that the thread culture is set based on the back office user's culture /// diff --git a/src/Umbraco.Web/Security/BackOfficeCookieAuthenticationProvider.cs b/src/Umbraco.Web/Security/BackOfficeCookieAuthenticationProvider.cs index 01f8dc4e96..35edb251f9 100644 --- a/src/Umbraco.Web/Security/BackOfficeCookieAuthenticationProvider.cs +++ b/src/Umbraco.Web/Security/BackOfficeCookieAuthenticationProvider.cs @@ -4,10 +4,8 @@ using System.Threading.Tasks; using Microsoft.Owin; using Microsoft.Owin.Security.Cookies; using Umbraco.Core; -using Umbraco.Web.Composing; +using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Hosting; @@ -77,12 +75,12 @@ namespace Umbraco.Web.Security Expires = DateTime.Now.AddYears(-1), Path = "/" }); - context.Response.Cookies.Append(Core.Constants.Web.PreviewCookieName, "", new CookieOptions + context.Response.Cookies.Append(Constants.Web.PreviewCookieName, "", new CookieOptions { Expires = DateTime.Now.AddYears(-1), Path = "/" }); - context.Response.Cookies.Append(Core.Constants.Security.BackOfficeExternalCookieName, "", new CookieOptions + context.Response.Cookies.Append(Constants.Security.BackOfficeExternalCookieName, "", new CookieOptions { Expires = DateTime.Now.AddYears(-1), Path = "/" diff --git a/src/Umbraco.Web/Security/BackOfficeSignInManager.cs b/src/Umbraco.Web/Security/BackOfficeSignInManager.cs index bbb4328fc3..5e172d2d77 100644 --- a/src/Umbraco.Web/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Web/Security/BackOfficeSignInManager.cs @@ -8,9 +8,9 @@ using Microsoft.Owin; using Microsoft.Owin.Logging; using Microsoft.Owin.Security; using Umbraco.Core; +using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration; -using Umbraco.Core.Security; -using Umbraco.Web.Models.Identity; +using Umbraco.Core.BackOffice; namespace Umbraco.Web.Security { diff --git a/src/Umbraco.Web/Security/BackOfficeUserManager.cs b/src/Umbraco.Web/Security/BackOfficeUserManager.cs index a143029327..ed2df536e7 100644 --- a/src/Umbraco.Web/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Web/Security/BackOfficeUserManager.cs @@ -8,12 +8,12 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Owin.Security.DataProtection; using Umbraco.Core; +using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration; using Umbraco.Core.Mapping; using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Net; -using Umbraco.Web.Models.Identity; namespace Umbraco.Web.Security { diff --git a/src/Umbraco.Web/Security/BackOfficeUserManagerMarker.cs b/src/Umbraco.Web/Security/BackOfficeUserManagerMarker.cs index b3eb9da3d5..d9d3f0e6f6 100644 --- a/src/Umbraco.Web/Security/BackOfficeUserManagerMarker.cs +++ b/src/Umbraco.Web/Security/BackOfficeUserManagerMarker.cs @@ -1,6 +1,6 @@ using System; using Microsoft.Owin; -using Umbraco.Web.Models.Identity; +using Umbraco.Core.BackOffice; namespace Umbraco.Web.Security { diff --git a/src/Umbraco.Web/Security/BackOfficeUserStore.cs b/src/Umbraco.Web/Security/BackOfficeUserStore.cs index 6f401163aa..d9d1900951 100644 --- a/src/Umbraco.Web/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Web/Security/BackOfficeUserStore.cs @@ -6,14 +6,13 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Umbraco.Core; +using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration; using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; -using Umbraco.Core.Security; using Umbraco.Core.Services; -using Umbraco.Web.Models.Identity; namespace Umbraco.Web.Security { diff --git a/src/Umbraco.Web/Security/BackOfficeUserValidator.cs b/src/Umbraco.Web/Security/BackOfficeUserValidator.cs index 94e9c2e0bd..951bdee4ed 100644 --- a/src/Umbraco.Web/Security/BackOfficeUserValidator.cs +++ b/src/Umbraco.Web/Security/BackOfficeUserValidator.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; -using Umbraco.Web.Models.Identity; +using Umbraco.Core.BackOffice; namespace Umbraco.Web.Security { diff --git a/src/Umbraco.Web/Security/ExternalSignInAutoLinkOptions.cs b/src/Umbraco.Web/Security/ExternalSignInAutoLinkOptions.cs index abe5aeb196..fc84cd270d 100644 --- a/src/Umbraco.Web/Security/ExternalSignInAutoLinkOptions.cs +++ b/src/Umbraco.Web/Security/ExternalSignInAutoLinkOptions.cs @@ -1,7 +1,7 @@ using System; using Umbraco.Core; +using Umbraco.Core.BackOffice; using Umbraco.Web.Composing; -using Umbraco.Web.Models.Identity; namespace Umbraco.Web.Security { diff --git a/src/Umbraco.Web/Security/FixWindowsAuthMiddlware.cs b/src/Umbraco.Web/Security/FixWindowsAuthMiddlware.cs index 30038e1f31..9bad4bec17 100644 --- a/src/Umbraco.Web/Security/FixWindowsAuthMiddlware.cs +++ b/src/Umbraco.Web/Security/FixWindowsAuthMiddlware.cs @@ -4,7 +4,7 @@ using System.Security.Principal; using System.Threading.Tasks; using Microsoft.Owin; using Umbraco.Core; -using Umbraco.Core.Security; +using Umbraco.Core.BackOffice; namespace Umbraco.Web.Security { diff --git a/src/Umbraco.Web/Security/IBackOfficeUserManagerMarker.cs b/src/Umbraco.Web/Security/IBackOfficeUserManagerMarker.cs index bfe2590eb8..2b1f9927f2 100644 --- a/src/Umbraco.Web/Security/IBackOfficeUserManagerMarker.cs +++ b/src/Umbraco.Web/Security/IBackOfficeUserManagerMarker.cs @@ -1,5 +1,5 @@ using Microsoft.Owin; -using Umbraco.Web.Models.Identity; +using Umbraco.Core.BackOffice; namespace Umbraco.Web.Security { diff --git a/src/Umbraco.Web/Security/IBackOfficeUserPasswordChecker.cs b/src/Umbraco.Web/Security/IBackOfficeUserPasswordChecker.cs index 9b9e7443be..2fae308eb0 100644 --- a/src/Umbraco.Web/Security/IBackOfficeUserPasswordChecker.cs +++ b/src/Umbraco.Web/Security/IBackOfficeUserPasswordChecker.cs @@ -1,6 +1,5 @@ using System.Threading.Tasks; -using Umbraco.Core.Models.Identity; -using Umbraco.Web.Models.Identity; +using Umbraco.Core.BackOffice; namespace Umbraco.Web.Security { diff --git a/src/Umbraco.Web/Security/IUserSessionStore.cs b/src/Umbraco.Web/Security/IUserSessionStore.cs deleted file mode 100644 index 06b7c2f165..0000000000 --- a/src/Umbraco.Web/Security/IUserSessionStore.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; - -namespace Umbraco.Core.Security -{ - /// - /// An IUserStore interface part to implement if the store supports validating user session Ids - /// - /// - public interface IUserSessionStore : IUserStore - where TUser : class - { - Task ValidateSessionIdAsync(string userId, string sessionId); - } -} diff --git a/src/Umbraco.Web/Security/IdentityAuditEventArgs.cs b/src/Umbraco.Web/Security/IdentityAuditEventArgs.cs index 5847250b41..d37974276c 100644 --- a/src/Umbraco.Web/Security/IdentityAuditEventArgs.cs +++ b/src/Umbraco.Web/Security/IdentityAuditEventArgs.cs @@ -1,5 +1,6 @@ using System; using System.Threading; +using Umbraco.Extensions; namespace Umbraco.Web.Security diff --git a/src/Umbraco.Web/Security/NopLookupNormalizer.cs b/src/Umbraco.Web/Security/NopLookupNormalizer.cs deleted file mode 100644 index 08aa8d548a..0000000000 --- a/src/Umbraco.Web/Security/NopLookupNormalizer.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.AspNetCore.Identity; - -namespace Umbraco.Web.Security -{ - /// - /// No-op lookup normalizer to maintain compatibility with ASP.NET Identity 2 - /// - public class NopLookupNormalizer : ILookupNormalizer - { - public string NormalizeName(string name) => name; - - public string NormalizeEmail(string email) => email; - } -} diff --git a/src/Umbraco.Web/Security/OwinDataProtectorTokenProvider.cs b/src/Umbraco.Web/Security/OwinDataProtectorTokenProvider.cs index 15bd4dfd75..72e12b8621 100644 --- a/src/Umbraco.Web/Security/OwinDataProtectorTokenProvider.cs +++ b/src/Umbraco.Web/Security/OwinDataProtectorTokenProvider.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Owin.Security.DataProtection; -using Umbraco.Web.Models.Identity; +using Umbraco.Core.BackOffice; namespace Umbraco.Web.Security { diff --git a/src/Umbraco.Web/Security/PreviewAuthenticationMiddleware.cs b/src/Umbraco.Web/Security/PreviewAuthenticationMiddleware.cs index 2f9648d1f7..799edb5f60 100644 --- a/src/Umbraco.Web/Security/PreviewAuthenticationMiddleware.cs +++ b/src/Umbraco.Web/Security/PreviewAuthenticationMiddleware.cs @@ -1,12 +1,10 @@ using System.Security.Claims; using System.Threading.Tasks; -using System.Web; using Microsoft.Owin; using Umbraco.Core; +using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration; using Umbraco.Core.Hosting; -using Umbraco.Core.IO; -using Umbraco.Core.Security; namespace Umbraco.Web.Security { diff --git a/src/Umbraco.Web/Security/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Web/Security/UmbracoBackOfficeIdentity.cs deleted file mode 100644 index c3697d5e9e..0000000000 --- a/src/Umbraco.Web/Security/UmbracoBackOfficeIdentity.cs +++ /dev/null @@ -1,232 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using Umbraco.Web; - -namespace Umbraco.Core.Security -{ - - /// - /// A custom user identity for the Umbraco backoffice - /// - /// - /// This inherits from FormsIdentity for backwards compatibility reasons since we still support the forms auth cookie, in v8 we can - /// change over to 'pure' asp.net identity and just inherit from ClaimsIdentity. - /// - [Serializable] - public class UmbracoBackOfficeIdentity : ClaimsIdentity - { - public static UmbracoBackOfficeIdentity FromClaimsIdentity(ClaimsIdentity identity) - { - return new UmbracoBackOfficeIdentity(identity); - } - - /// - /// Creates a new UmbracoBackOfficeIdentity - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public UmbracoBackOfficeIdentity(int userId, string username, string realName, - IEnumerable startContentNodes, IEnumerable startMediaNodes, string culture, - string sessionId, string securityStamp, IEnumerable allowedApps, IEnumerable roles) - : base(Enumerable.Empty(), Constants.Security.BackOfficeAuthenticationType) //this ctor is used to ensure the IsAuthenticated property is true - { - if (allowedApps == null) throw new ArgumentNullException(nameof(allowedApps)); - if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); - if (string.IsNullOrWhiteSpace(realName)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(realName)); - if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture)); - if (string.IsNullOrWhiteSpace(sessionId)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(sessionId)); - if (string.IsNullOrWhiteSpace(securityStamp)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(securityStamp)); - AddRequiredClaims(userId, username, realName, startContentNodes, startMediaNodes, culture, sessionId, securityStamp, allowedApps, roles); - } - - /// - /// Creates a new UmbracoBackOfficeIdentity - /// - /// - /// The original identity created by the ClaimsIdentityFactory - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public UmbracoBackOfficeIdentity(ClaimsIdentity childIdentity, - int userId, string username, string realName, - IEnumerable startContentNodes, IEnumerable startMediaNodes, string culture, - string sessionId, string securityStamp, IEnumerable allowedApps, IEnumerable roles) - : base(childIdentity.Claims, Constants.Security.BackOfficeAuthenticationType) - { - if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username)); - if (string.IsNullOrWhiteSpace(realName)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(realName)); - if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture)); - if (string.IsNullOrWhiteSpace(sessionId)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(sessionId)); - if (string.IsNullOrWhiteSpace(securityStamp)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(securityStamp)); - Actor = childIdentity; - AddRequiredClaims(userId, username, realName, startContentNodes, startMediaNodes, culture, sessionId, securityStamp, allowedApps, roles); - } - - /// - /// Create a back office identity based on an existing claims identity - /// - /// - private UmbracoBackOfficeIdentity(ClaimsIdentity identity) - : base(identity.Claims, Constants.Security.BackOfficeAuthenticationType) - { - Actor = identity; - - //validate that all claims exist - foreach (var t in RequiredBackOfficeIdentityClaimTypes) - { - //if the identity doesn't have the claim, or the claim value is null - if (identity.HasClaim(x => x.Type == t) == false || identity.HasClaim(x => x.Type == t && x.Value.IsNullOrWhiteSpace())) - { - throw new InvalidOperationException("Cannot create a " + typeof(UmbracoBackOfficeIdentity) + " from " + typeof(ClaimsIdentity) + " since the required claim " + t + " is missing"); - } - } - } - - public const string Issuer = Constants.Security.BackOfficeAuthenticationType; - - /// - /// Returns the required claim types for a back office identity - /// - /// - /// This does not include the role claim type or allowed apps type since that is a collection and in theory could be empty - /// - public static IEnumerable RequiredBackOfficeIdentityClaimTypes => new[] - { - ClaimTypes.NameIdentifier, //id - ClaimTypes.Name, //username - ClaimTypes.GivenName, - Constants.Security.StartContentNodeIdClaimType, - Constants.Security.StartMediaNodeIdClaimType, - ClaimTypes.Locality, - Constants.Security.SessionIdClaimType, - Constants.Web.SecurityStampClaimType - }; - - /// - /// Adds claims based on the ctor data - /// - private void AddRequiredClaims(int userId, string username, string realName, - IEnumerable startContentNodes, IEnumerable startMediaNodes, string culture, - string sessionId, string securityStamp, IEnumerable allowedApps, IEnumerable roles) - { - //This is the id that 'identity' uses to check for the user id - if (HasClaim(x => x.Type == ClaimTypes.NameIdentifier) == false) - AddClaim(new Claim(ClaimTypes.NameIdentifier, userId.ToInvariantString(), ClaimValueTypes.Integer32, Issuer, Issuer, this)); - - if (HasClaim(x => x.Type == ClaimTypes.Name) == false) - AddClaim(new Claim(ClaimTypes.Name, username, ClaimValueTypes.String, Issuer, Issuer, this)); - - if (HasClaim(x => x.Type == ClaimTypes.GivenName) == false) - AddClaim(new Claim(ClaimTypes.GivenName, realName, ClaimValueTypes.String, Issuer, Issuer, this)); - - if (HasClaim(x => x.Type == Constants.Security.StartContentNodeIdClaimType) == false && startContentNodes != null) - { - foreach (var startContentNode in startContentNodes) - { - AddClaim(new Claim(Constants.Security.StartContentNodeIdClaimType, startContentNode.ToInvariantString(), ClaimValueTypes.Integer32, Issuer, Issuer, this)); - } - } - - if (HasClaim(x => x.Type == Constants.Security.StartMediaNodeIdClaimType) == false && startMediaNodes != null) - { - foreach (var startMediaNode in startMediaNodes) - { - AddClaim(new Claim(Constants.Security.StartMediaNodeIdClaimType, startMediaNode.ToInvariantString(), ClaimValueTypes.Integer32, Issuer, Issuer, this)); - } - } - - if (HasClaim(x => x.Type == ClaimTypes.Locality) == false) - AddClaim(new Claim(ClaimTypes.Locality, culture, ClaimValueTypes.String, Issuer, Issuer, this)); - - if (HasClaim(x => x.Type == Constants.Security.SessionIdClaimType) == false && SessionId.IsNullOrWhiteSpace() == false) - AddClaim(new Claim(Constants.Security.SessionIdClaimType, sessionId, ClaimValueTypes.String, Issuer, Issuer, this)); - - //The security stamp claim is also required... this is because this claim type is hard coded - // by the SecurityStampValidator, see: https://katanaproject.codeplex.com/workitem/444 - if (HasClaim(x => x.Type == Constants.Web.SecurityStampClaimType) == false) - AddClaim(new Claim(Constants.Web.SecurityStampClaimType, securityStamp, ClaimValueTypes.String, Issuer, Issuer, this)); - - //Add each app as a separate claim - if (HasClaim(x => x.Type == Constants.Security.AllowedApplicationsClaimType) == false && allowedApps != null) - { - foreach (var application in allowedApps) - { - AddClaim(new Claim(Constants.Security.AllowedApplicationsClaimType, application, ClaimValueTypes.String, Issuer, Issuer, this)); - } - } - - //Claims are added by the ClaimsIdentityFactory because our UserStore supports roles, however this identity might - // not be made with that factory if it was created with a different ticket so perform the check - if (HasClaim(x => x.Type == DefaultRoleClaimType) == false && roles != null) - { - //manually add them - foreach (var roleName in roles) - { - AddClaim(new Claim(RoleClaimType, roleName, ClaimValueTypes.String, Issuer, Issuer, this)); - } - } - - } - - /// - /// - /// Gets the type of authenticated identity. - /// - /// - /// The type of authenticated identity. This property always returns "UmbracoBackOffice". - /// - public override string AuthenticationType => Issuer; - - private int[] _startContentNodes; - public int[] StartContentNodes => _startContentNodes ?? (_startContentNodes = FindAll(x => x.Type == Constants.Security.StartContentNodeIdClaimType).Select(app => int.TryParse(app.Value, out var i) ? i : default).Where(x => x != default).ToArray()); - - private int[] _startMediaNodes; - public int[] StartMediaNodes => _startMediaNodes ?? (_startMediaNodes = FindAll(x => x.Type == Constants.Security.StartMediaNodeIdClaimType).Select(app => int.TryParse(app.Value, out var i) ? i : default).Where(x => x != default).ToArray()); - - private string[] _allowedApplications; - public string[] AllowedApplications => _allowedApplications ?? (_allowedApplications = FindAll(x => x.Type == Constants.Security.AllowedApplicationsClaimType).Select(app => app.Value).ToArray()); - - public int Id => int.Parse(this.FindFirstValue(ClaimTypes.NameIdentifier)); - - public string RealName => this.FindFirstValue(ClaimTypes.GivenName); - - public string Username => this.FindFirstValue(ClaimTypes.Name); - - public string Culture => this.FindFirstValue(ClaimTypes.Locality); - - public string SessionId - { - get => this.FindFirstValue(Constants.Security.SessionIdClaimType); - set - { - var existing = FindFirst(Constants.Security.SessionIdClaimType); - if (existing != null) - TryRemoveClaim(existing); - AddClaim(new Claim(Constants.Security.SessionIdClaimType, value, ClaimValueTypes.String, Issuer, Issuer, this)); - } - } - - public string SecurityStamp => this.FindFirstValue(Constants.Web.SecurityStampClaimType); - - public string[] Roles => this.FindAll(x => x.Type == DefaultRoleClaimType).Select(role => role.Value).ToArray(); - - } -} diff --git a/src/Umbraco.Web/Security/UmbracoSecureDataFormat.cs b/src/Umbraco.Web/Security/UmbracoSecureDataFormat.cs index 19cd602657..41a2ad3bba 100644 --- a/src/Umbraco.Web/Security/UmbracoSecureDataFormat.cs +++ b/src/Umbraco.Web/Security/UmbracoSecureDataFormat.cs @@ -1,6 +1,6 @@ using System; using Microsoft.Owin.Security; -using Umbraco.Core.Security; +using Umbraco.Core.BackOffice; namespace Umbraco.Web.Security { diff --git a/src/Umbraco.Web/Security/UmbracoSecurityStampValidator.cs b/src/Umbraco.Web/Security/UmbracoSecurityStampValidator.cs index a3f78f5262..18539d8fab 100644 --- a/src/Umbraco.Web/Security/UmbracoSecurityStampValidator.cs +++ b/src/Umbraco.Web/Security/UmbracoSecurityStampValidator.cs @@ -3,7 +3,7 @@ using System.Security.Claims; using System.Threading.Tasks; using Microsoft.Owin.Security.Cookies; using Umbraco.Core; -using Umbraco.Web.Models.Identity; +using Umbraco.Core.BackOffice; namespace Umbraco.Web.Security { diff --git a/src/Umbraco.Web/Security/UserAwarePasswordHasher.cs b/src/Umbraco.Web/Security/UserAwarePasswordHasher.cs index d804ef0ae4..b1d88348d0 100644 --- a/src/Umbraco.Web/Security/UserAwarePasswordHasher.cs +++ b/src/Umbraco.Web/Security/UserAwarePasswordHasher.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Identity; +using Umbraco.Core.BackOffice; using Umbraco.Core.Security; -using Umbraco.Web.Models.Identity; namespace Umbraco.Web.Security { diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index 8975f9cde0..3c17bd370a 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -5,11 +5,10 @@ using Umbraco.Core; using Umbraco.Core.Services; using Umbraco.Core.Models.Membership; using Microsoft.Owin; +using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration; using Umbraco.Core.Hosting; -using Umbraco.Core.IO; using Umbraco.Core.Models; -using Umbraco.Web.Models.Identity; namespace Umbraco.Web.Security { diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index b7c1e6e0b5..9817431f76 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -176,10 +176,6 @@ - - - - @@ -198,7 +194,6 @@ - @@ -207,14 +202,11 @@ - - - diff --git a/src/Umbraco.Web/WebApi/Filters/CheckIfUserTicketDataIsStaleAttribute.cs b/src/Umbraco.Web/WebApi/Filters/CheckIfUserTicketDataIsStaleAttribute.cs index c68b949bba..09fd5e080c 100644 --- a/src/Umbraco.Web/WebApi/Filters/CheckIfUserTicketDataIsStaleAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/CheckIfUserTicketDataIsStaleAttribute.cs @@ -6,13 +6,12 @@ using System.Web.Http.Controllers; using System.Web.Http.Filters; using Umbraco.Core; using Umbraco.Web.Composing; -using Umbraco.Core.Models.Identity; +using Umbraco.Core.BackOffice; using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; using Umbraco.Web.Security; using Umbraco.Core.Mapping; using Umbraco.Core.Models; -using Umbraco.Web.Models.Identity; namespace Umbraco.Web.WebApi.Filters { diff --git a/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs b/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs index 2691063f19..b171356e23 100644 --- a/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs +++ b/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs @@ -2,10 +2,10 @@ using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; -using Umbraco.Web.WebApi.Filters;using Umbraco.Core.Models.Identity; +using Umbraco.Web.WebApi.Filters; +using Umbraco.Core.BackOffice; using Umbraco.Core.Persistence; using Umbraco.Core.Services; -using Umbraco.Web.Models.Identity; using Umbraco.Web.Security; using Umbraco.Core.Mapping; using Umbraco.Web.Routing; @@ -30,7 +30,7 @@ namespace Umbraco.Web.WebApi [EnableDetailedErrors] public abstract class UmbracoAuthorizedApiController : UmbracoApiController { - private BackOfficeUserManager _userManager; + private Security.BackOfficeUserManager _userManager; protected UmbracoAuthorizedApiController() { @@ -44,7 +44,7 @@ namespace Umbraco.Web.WebApi /// /// Gets the user manager. /// - protected BackOfficeUserManager UserManager + protected Security.BackOfficeUserManager UserManager => _userManager ?? (_userManager = TryGetOwinContext().Result.GetBackOfficeUserManager()); } } From 7b1ce5bbe7241ca68efd32990b2c08933f170ef3 Mon Sep 17 00:00:00 2001 From: Scott Brady Date: Mon, 18 May 2020 12:06:26 +0100 Subject: [PATCH 10/13] Removed duplication with user store and manager --- .../BackOffice/BackOfficeUserManager.cs | 28 +- .../Security/BackOfficeUserManagerTests.cs | 4 +- .../UmbracoSecurityStampValidatorTests.cs | 30 +- .../Web/Controllers/UsersControllerTests.cs | 6 +- .../BackOfficeUserAuditEventsComponent.cs | 14 +- .../Editors/AuthenticationController.cs | 6 +- .../Editors/BackOfficeController.cs | 4 +- src/Umbraco.Web/Editors/PasswordChanger.cs | 3 +- src/Umbraco.Web/OwinExtensions.cs | 7 +- .../Security/AppBuilderExtensions.cs | 16 +- .../Security/BackOfficeOwinUserManager.cs | 141 +++ .../Security/BackOfficeUserManager.cs | 582 ----------- .../Security/BackOfficeUserManagerMarker.cs | 4 +- .../Security/BackOfficeUserStore.cs | 917 ------------------ .../Security/BackOfficeUserValidator.cs | 20 - .../Security/IBackOfficeUserManagerMarker.cs | 3 +- .../Security/SessionIdValidator.cs | 2 +- src/Umbraco.Web/Security/WebSecurity.cs | 4 +- src/Umbraco.Web/Umbraco.Web.csproj | 4 +- .../WebApi/UmbracoAuthorizedApiController.cs | 5 +- 20 files changed, 208 insertions(+), 1592 deletions(-) create mode 100644 src/Umbraco.Web/Security/BackOfficeOwinUserManager.cs delete mode 100644 src/Umbraco.Web/Security/BackOfficeUserManager.cs delete mode 100644 src/Umbraco.Web/Security/BackOfficeUserStore.cs delete mode 100644 src/Umbraco.Web/Security/BackOfficeUserValidator.cs diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs index 6d2ad081c3..99d0265d06 100644 --- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs +++ b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -56,7 +56,7 @@ namespace Umbraco.Core.BackOffice // TODO: Support this public override bool SupportsUserPhoneNumber => false; #endregion - + /// /// Used to validate a user's session /// @@ -86,7 +86,7 @@ namespace Umbraco.Core.BackOffice /// Gets/sets the default back office user password checker /// public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get; set; } - public IPasswordConfiguration PasswordConfiguration { get; } + public IPasswordConfiguration PasswordConfiguration { get; protected set; } public IIpResolver IpResolver { get; } /// @@ -332,57 +332,57 @@ namespace Umbraco.Core.BackOffice return result; } - internal void RaiseAccountLockedEvent(int userId) + public void RaiseAccountLockedEvent(int userId) { OnAccountLocked(new IdentityAuditEventArgs(AuditEvent.AccountLocked, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } - internal void RaiseAccountUnlockedEvent(int userId) + public void RaiseAccountUnlockedEvent(int userId) { OnAccountUnlocked(new IdentityAuditEventArgs(AuditEvent.AccountUnlocked, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } - internal void RaiseForgotPasswordRequestedEvent(int userId) + public void RaiseForgotPasswordRequestedEvent(int userId) { OnForgotPasswordRequested(new IdentityAuditEventArgs(AuditEvent.ForgotPasswordRequested, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } - internal void RaiseForgotPasswordChangedSuccessEvent(int userId) + public void RaiseForgotPasswordChangedSuccessEvent(int userId) { OnForgotPasswordChangedSuccess(new IdentityAuditEventArgs(AuditEvent.ForgotPasswordChangedSuccess, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } - internal void RaiseLoginFailedEvent(int userId) + public void RaiseLoginFailedEvent(int userId) { OnLoginFailed(new IdentityAuditEventArgs(AuditEvent.LoginFailed, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } - internal void RaiseInvalidLoginAttemptEvent(string username) + public void RaiseInvalidLoginAttemptEvent(string username) { OnLoginFailed(new IdentityAuditEventArgs(AuditEvent.LoginFailed, IpResolver.GetCurrentRequestIpAddress(), username, string.Format("Attempted login for username '{0}' failed", username))); } - internal void RaiseLoginRequiresVerificationEvent(int userId) + public void RaiseLoginRequiresVerificationEvent(int userId) { OnLoginRequiresVerification(new IdentityAuditEventArgs(AuditEvent.LoginRequiresVerification, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } - internal void RaiseLoginSuccessEvent(int userId) + public void RaiseLoginSuccessEvent(int userId) { OnLoginSuccess(new IdentityAuditEventArgs(AuditEvent.LoginSucces, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } - internal void RaiseLogoutSuccessEvent(int userId) + public void RaiseLogoutSuccessEvent(int userId) { OnLogoutSuccess(new IdentityAuditEventArgs(AuditEvent.LogoutSuccess, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } - internal void RaisePasswordChangedEvent(int userId) + public void RaisePasswordChangedEvent(int userId) { OnPasswordChanged(new IdentityAuditEventArgs(AuditEvent.PasswordChanged, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } - internal void RaiseResetAccessFailedCountEvent(int userId) + public void RaiseResetAccessFailedCountEvent(int userId) { OnResetAccessFailedCount(new IdentityAuditEventArgs(AuditEvent.ResetAccessFailedCount, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); } diff --git a/src/Umbraco.Tests/Security/BackOfficeUserManagerTests.cs b/src/Umbraco.Tests/Security/BackOfficeUserManagerTests.cs index 99a2b323dd..cf3d54a4ce 100644 --- a/src/Umbraco.Tests/Security/BackOfficeUserManagerTests.cs +++ b/src/Umbraco.Tests/Security/BackOfficeUserManagerTests.cs @@ -10,7 +10,7 @@ using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration; using Umbraco.Core.Models.Membership; using Umbraco.Net; -using BackOfficeUserManager = Umbraco.Web.Security.BackOfficeUserManager; +using Umbraco.Web.Security; namespace Umbraco.Tests.Security { @@ -32,7 +32,7 @@ namespace Umbraco.Tests.Security mockPasswordConfiguration.Setup(x => x.HashAlgorithmType) .Returns("HMACSHA256"); - var userManager = BackOfficeUserManager.Create( + var userManager = BackOfficeOwinUserManager.Create( mockPasswordConfiguration.Object, mockIpResolver.Object, mockUserStore.Object, diff --git a/src/Umbraco.Tests/Security/UmbracoSecurityStampValidatorTests.cs b/src/Umbraco.Tests/Security/UmbracoSecurityStampValidatorTests.cs index 4adfe15ad7..b80e526037 100644 --- a/src/Umbraco.Tests/Security/UmbracoSecurityStampValidatorTests.cs +++ b/src/Umbraco.Tests/Security/UmbracoSecurityStampValidatorTests.cs @@ -23,7 +23,7 @@ namespace Umbraco.Tests.Security public class UmbracoSecurityStampValidatorTests { private Mock _mockOwinContext; - private Mock> _mockUserManager; + private Mock _mockUserManager; private Mock _mockSignInManager; private AuthenticationTicket _testAuthTicket; @@ -35,7 +35,7 @@ namespace Umbraco.Tests.Security public void OnValidateIdentity_When_GetUserIdCallback_Is_Null_Expect_ArgumentNullException() { Assert.Throws(() => UmbracoSecurityStampValidator - .OnValidateIdentity, BackOfficeIdentityUser>( + .OnValidateIdentity( TimeSpan.MaxValue, null, null)); } @@ -43,7 +43,7 @@ namespace Umbraco.Tests.Security public async Task OnValidateIdentity_When_Validation_Interval_Not_Met_Expect_No_Op() { var func = UmbracoSecurityStampValidator - .OnValidateIdentity, BackOfficeIdentityUser>( + .OnValidateIdentity( TimeSpan.MaxValue, null, identity => throw new Exception()); _testAuthTicket.Properties.IssuedUtc = DateTimeOffset.UtcNow; @@ -62,11 +62,11 @@ namespace Umbraco.Tests.Security public void OnValidateIdentity_When_Time_To_Validate_But_No_UserManager_Expect_InvalidOperationException() { var func = UmbracoSecurityStampValidator - .OnValidateIdentity, BackOfficeIdentityUser>( + .OnValidateIdentity( TimeSpan.MinValue, null, identity => throw new Exception()); - _mockOwinContext.Setup(x => x.Get>(It.IsAny())) - .Returns((Umbraco.Web.Security.BackOfficeUserManager) null); + _mockOwinContext.Setup(x => x.Get(It.IsAny())) + .Returns((BackOfficeOwinUserManager) null); var context = new CookieValidateIdentityContext( _mockOwinContext.Object, @@ -80,7 +80,7 @@ namespace Umbraco.Tests.Security public void OnValidateIdentity_When_Time_To_Validate_But_No_SignInManager_Expect_InvalidOperationException() { var func = UmbracoSecurityStampValidator - .OnValidateIdentity, BackOfficeIdentityUser>( + .OnValidateIdentity( TimeSpan.MinValue, null, identity => throw new Exception()); _mockOwinContext.Setup(x => x.Get(It.IsAny())) @@ -100,7 +100,7 @@ namespace Umbraco.Tests.Security var userId = Guid.NewGuid().ToString(); var func = UmbracoSecurityStampValidator - .OnValidateIdentity, BackOfficeIdentityUser>( + .OnValidateIdentity( TimeSpan.MinValue, null, identity => userId); _mockUserManager.Setup(x => x.FindByIdAsync(userId)) @@ -123,7 +123,7 @@ namespace Umbraco.Tests.Security var userId = Guid.NewGuid().ToString(); var func = UmbracoSecurityStampValidator - .OnValidateIdentity, BackOfficeIdentityUser>( + .OnValidateIdentity( TimeSpan.MinValue, null, identity => userId); _mockUserManager.Setup(x => x.FindByIdAsync(userId)).ReturnsAsync(_testUser); @@ -146,7 +146,7 @@ namespace Umbraco.Tests.Security var userId = Guid.NewGuid().ToString(); var func = UmbracoSecurityStampValidator - .OnValidateIdentity, BackOfficeIdentityUser>( + .OnValidateIdentity( TimeSpan.MinValue, null, identity => userId); _mockUserManager.Setup(x => x.FindByIdAsync(userId)).ReturnsAsync(_testUser); @@ -170,7 +170,7 @@ namespace Umbraco.Tests.Security var userId = Guid.NewGuid().ToString(); var func = UmbracoSecurityStampValidator - .OnValidateIdentity, BackOfficeIdentityUser>( + .OnValidateIdentity( TimeSpan.MinValue, null, identity => userId); _mockUserManager.Setup(x => x.FindByIdAsync(userId)).ReturnsAsync(_testUser); @@ -194,7 +194,7 @@ namespace Umbraco.Tests.Security var expectedIdentity = new ClaimsIdentity(new List {new Claim("sub", "bob")}); var regenFuncCalled = false; - Func, BackOfficeIdentityUser, Task> regenFunc = + Func> regenFunc = (signInManager, userManager, user) => { regenFuncCalled = true; @@ -202,7 +202,7 @@ namespace Umbraco.Tests.Security }; var func = UmbracoSecurityStampValidator - .OnValidateIdentity, BackOfficeIdentityUser>( + .OnValidateIdentity( TimeSpan.MinValue, regenFunc, identity => userId); _mockUserManager.Setup(x => x.FindByIdAsync(userId)).ReturnsAsync(_testUser); @@ -250,7 +250,7 @@ namespace Umbraco.Tests.Security new AuthenticationProperties()); _testOptions = new CookieAuthenticationOptions { AuthenticationType = _testAuthType }; - _mockUserManager = new Mock>( + _mockUserManager = new Mock( new Mock().Object, new Mock().Object, new Mock>().Object, @@ -267,7 +267,7 @@ namespace Umbraco.Tests.Security new Mock().Object); _mockOwinContext = new Mock(); - _mockOwinContext.Setup(x => x.Get>(It.IsAny())) + _mockOwinContext.Setup(x => x.Get(It.IsAny())) .Returns(_mockUserManager.Object); _mockOwinContext.Setup(x => x.Get(It.IsAny())) .Returns(_mockSignInManager.Object); diff --git a/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs index 80deb5a800..6905c749e6 100644 --- a/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs @@ -469,7 +469,7 @@ namespace Umbraco.Tests.Web.Controllers mockUserManager.Verify(); } - private UsersController CreateSut(IMock> mockUserManager = null) + private UsersController CreateSut(IMock mockUserManager = null) { var mockLocalizedTextService = new Mock(); mockLocalizedTextService.Setup(x => x.Localize(It.IsAny(), It.IsAny(), It.IsAny>())) @@ -509,9 +509,9 @@ namespace Umbraco.Tests.Web.Controllers return usersController; } - private static Mock> CreateMockUserManager() + private static Mock CreateMockUserManager() { - return new Mock>( + return new Mock( new Mock().Object, new Mock().Object, new Mock>().Object, diff --git a/src/Umbraco.Web/Compose/BackOfficeUserAuditEventsComponent.cs b/src/Umbraco.Web/Compose/BackOfficeUserAuditEventsComponent.cs index 84fb0e6bb8..dcb5fac32d 100644 --- a/src/Umbraco.Web/Compose/BackOfficeUserAuditEventsComponent.cs +++ b/src/Umbraco.Web/Compose/BackOfficeUserAuditEventsComponent.cs @@ -26,14 +26,14 @@ namespace Umbraco.Web.Compose { //BackOfficeUserManager.AccountLocked += ; //BackOfficeUserManager.AccountUnlocked += ; - BackOfficeUserManager.ForgotPasswordRequested += OnForgotPasswordRequest; - BackOfficeUserManager.ForgotPasswordChangedSuccess += OnForgotPasswordChange; - BackOfficeUserManager.LoginFailed += OnLoginFailed; + BackOfficeOwinUserManager.ForgotPasswordRequested += OnForgotPasswordRequest; + BackOfficeOwinUserManager.ForgotPasswordChangedSuccess += OnForgotPasswordChange; + BackOfficeOwinUserManager.LoginFailed += OnLoginFailed; //BackOfficeUserManager.LoginRequiresVerification += ; - BackOfficeUserManager.LoginSuccess += OnLoginSuccess; - BackOfficeUserManager.LogoutSuccess += OnLogoutSuccess; - BackOfficeUserManager.PasswordChanged += OnPasswordChanged; - BackOfficeUserManager.PasswordReset += OnPasswordReset; + BackOfficeOwinUserManager.LoginSuccess += OnLoginSuccess; + BackOfficeOwinUserManager.LogoutSuccess += OnLogoutSuccess; + BackOfficeOwinUserManager.PasswordChanged += OnPasswordChanged; + BackOfficeOwinUserManager.PasswordReset += OnPasswordReset; //BackOfficeUserManager.ResetAccessFailedCount += ; } diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index b38bbbb19d..d65bb043c5 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -40,7 +40,7 @@ namespace Umbraco.Web.Editors [IsBackOffice] public class AuthenticationController : UmbracoApiController { - private Security.BackOfficeUserManager _userManager; + private BackOfficeOwinUserManager _userManager; private BackOfficeSignInManager _signInManager; private readonly IUserPasswordConfiguration _passwordConfiguration; private readonly IHostingEnvironment _hostingEnvironment; @@ -71,8 +71,8 @@ namespace Umbraco.Web.Editors _requestAccessor = requestAccessor ?? throw new ArgumentNullException(nameof(securitySettings)); } - protected Security.BackOfficeUserManager UserManager => _userManager - ?? (_userManager = TryGetOwinContext().Result.GetBackOfficeUserManager()); + protected BackOfficeOwinUserManager UserManager => _userManager + ?? (_userManager = TryGetOwinContext().Result.GetBackOfficeUserManager()); protected BackOfficeSignInManager SignInManager => _signInManager ?? (_signInManager = TryGetOwinContext().Result.GetBackOfficeSignInManager()); diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 782dea61ac..db7505aa3c 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -45,7 +45,7 @@ namespace Umbraco.Web.Editors { private readonly UmbracoFeatures _features; private readonly IRuntimeState _runtimeState; - private BackOfficeUserManager _userManager; + private BackOfficeOwinUserManager _userManager; private BackOfficeSignInManager _signInManager; private readonly IUmbracoVersion _umbracoVersion; private readonly IGridConfig _gridConfig; @@ -92,7 +92,7 @@ namespace Umbraco.Web.Editors protected BackOfficeSignInManager SignInManager => _signInManager ?? (_signInManager = OwinContext.GetBackOfficeSignInManager()); - protected BackOfficeUserManager UserManager => _userManager ?? (_userManager = OwinContext.GetBackOfficeUserManager()); + protected BackOfficeOwinUserManager UserManager => _userManager ?? (_userManager = OwinContext.GetBackOfficeUserManager()); protected IAuthenticationManager AuthenticationManager => OwinContext.Authentication; diff --git a/src/Umbraco.Web/Editors/PasswordChanger.cs b/src/Umbraco.Web/Editors/PasswordChanger.cs index 10a5248045..256da61478 100644 --- a/src/Umbraco.Web/Editors/PasswordChanger.cs +++ b/src/Umbraco.Web/Editors/PasswordChanger.cs @@ -2,7 +2,6 @@ using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Umbraco.Core; -using Umbraco.Core.BackOffice; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Web.Models; @@ -32,7 +31,7 @@ namespace Umbraco.Web.Editors IUser currentUser, IUser savingUser, ChangingPasswordModel passwordModel, - Security.BackOfficeUserManager userMgr) + BackOfficeOwinUserManager userMgr) { if (passwordModel == null) throw new ArgumentNullException(nameof(passwordModel)); if (userMgr == null) throw new ArgumentNullException(nameof(userMgr)); diff --git a/src/Umbraco.Web/OwinExtensions.cs b/src/Umbraco.Web/OwinExtensions.cs index 67e9375ab5..4ea4040ec6 100644 --- a/src/Umbraco.Web/OwinExtensions.cs +++ b/src/Umbraco.Web/OwinExtensions.cs @@ -4,7 +4,6 @@ using Microsoft.Owin; using Microsoft.Owin.Security; using Umbraco.Core; using Umbraco.Web.Security; -using BackOfficeIdentityUser = Umbraco.Core.BackOffice.BackOfficeIdentityUser; namespace Umbraco.Web { @@ -72,13 +71,13 @@ namespace Umbraco.Web /// This is required because to extract the user manager we need to user a custom service since owin only deals in generics and /// developers could register their own user manager types /// - public static BackOfficeUserManager GetBackOfficeUserManager(this IOwinContext owinContext) + public static BackOfficeOwinUserManager GetBackOfficeUserManager(this IOwinContext owinContext) { - var marker = owinContext.Get(BackOfficeUserManager.OwinMarkerKey) + var marker = owinContext.Get(BackOfficeOwinUserManager.OwinMarkerKey) ?? throw new NullReferenceException($"No {typeof (IBackOfficeUserManagerMarker)}, i.e. no Umbraco back-office, has been registered with Owin."); return marker.GetManager(owinContext) - ?? throw new NullReferenceException($"Could not resolve an instance of {typeof (BackOfficeUserManager)} from the {typeof (IOwinContext)}."); + ?? throw new NullReferenceException($"Could not resolve an instance of {typeof (BackOfficeOwinUserManager)} from the {typeof (IOwinContext)}."); } /// diff --git a/src/Umbraco.Web/Security/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/AppBuilderExtensions.cs index 6254dabeb9..81c5408e98 100644 --- a/src/Umbraco.Web/Security/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/AppBuilderExtensions.cs @@ -43,8 +43,8 @@ namespace Umbraco.Web.Security if (services == null) throw new ArgumentNullException(nameof(services)); //Configure Umbraco user manager to be created per request - app.CreatePerOwinContext( - (options, owinContext) => BackOfficeUserManager.Create( + app.CreatePerOwinContext( + (options, owinContext) => BackOfficeOwinUserManager.Create( services.UserService, services.EntityService, services.ExternalLoginService, @@ -56,7 +56,7 @@ namespace Umbraco.Web.Security app.GetDataProtectionProvider(), new NullLogger>())); - app.SetBackOfficeUserManagerType(); + app.SetBackOfficeUserManagerType(); //Create a sign in manager per request app.CreatePerOwinContext((options, context) => BackOfficeSignInManager.Create(context, globalSettings, app.CreateLogger())); @@ -77,8 +77,8 @@ namespace Umbraco.Web.Security if (customUserStore == null) throw new ArgumentNullException(nameof(customUserStore)); //Configure Umbraco user manager to be created per request - app.CreatePerOwinContext( - (options, owinContext) => BackOfficeUserManager.Create( + app.CreatePerOwinContext( + (options, owinContext) => BackOfficeOwinUserManager.Create( passwordConfiguration, ipResolver, customUserStore, @@ -86,7 +86,7 @@ namespace Umbraco.Web.Security app.GetDataProtectionProvider(), new NullLogger>())); - app.SetBackOfficeUserManagerType(); + app.SetBackOfficeUserManagerType(); //Create a sign in manager per request app.CreatePerOwinContext((options, context) => BackOfficeSignInManager.Create(context, globalSettings, app.CreateLogger(typeof(BackOfficeSignInManager).FullName))); @@ -153,7 +153,7 @@ namespace Umbraco.Web.Security // logs in. This is a security feature which is used when you // change a password or add an external login to your account. OnValidateIdentity = UmbracoSecurityStampValidator - .OnValidateIdentity( + .OnValidateIdentity( TimeSpan.FromMinutes(30), (signInManager, manager, user) => signInManager.CreateUserIdentityAsync(user), identity => identity.GetUserId()), @@ -240,7 +240,7 @@ namespace Umbraco.Web.Security // a generic strongly typed instance app.Use((context, func) => { - context.Set(BackOfficeUserManager.OwinMarkerKey, new BackOfficeUserManagerMarker()); + context.Set(BackOfficeOwinUserManager.OwinMarkerKey, new BackOfficeUserManagerMarker()); return func(); }); } diff --git a/src/Umbraco.Web/Security/BackOfficeOwinUserManager.cs b/src/Umbraco.Web/Security/BackOfficeOwinUserManager.cs new file mode 100644 index 0000000000..3a5ba8f706 --- /dev/null +++ b/src/Umbraco.Web/Security/BackOfficeOwinUserManager.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Security.Claims; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Owin.Security.DataProtection; +using Umbraco.Core; +using Umbraco.Core.BackOffice; +using Umbraco.Core.Configuration; +using Umbraco.Core.Mapping; +using Umbraco.Core.Services; +using Umbraco.Net; + +namespace Umbraco.Web.Security +{ + public class BackOfficeOwinUserManager : BackOfficeUserManager + { + public const string OwinMarkerKey = "Umbraco.Web.Security.Identity.BackOfficeUserManagerMarker"; + + public BackOfficeOwinUserManager( + IPasswordConfiguration passwordConfiguration, + IIpResolver ipResolver, + IUserStore store, + IOptions optionsAccessor, + IEnumerable> userValidators, + IEnumerable> passwordValidators, + ILookupNormalizer keyNormalizer, + IdentityErrorDescriber errors, + IDataProtectionProvider dataProtectionProvider, + ILogger> logger) + : base(ipResolver, store, optionsAccessor, null, userValidators, passwordValidators, keyNormalizer, errors, null, logger) + { + PasswordConfiguration = passwordConfiguration; + InitUserManager(this, dataProtectionProvider); + } + + #region Static Create methods + + /// + /// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager + /// + public static BackOfficeOwinUserManager Create( + IUserService userService, + IEntityService entityService, + IExternalLoginService externalLoginService, + IGlobalSettings globalSettings, + UmbracoMapper mapper, + IPasswordConfiguration passwordConfiguration, + IIpResolver ipResolver, + IdentityErrorDescriber errors, + IDataProtectionProvider dataProtectionProvider, + ILogger> logger) + { + var store = new BackOfficeUserStore(userService, entityService, externalLoginService, globalSettings, mapper); + + return Create( + passwordConfiguration, + ipResolver, + store, + errors, + dataProtectionProvider, + logger); + } + + /// + /// Creates a BackOfficeUserManager instance with all default options and a custom BackOfficeUserManager instance + /// + public static BackOfficeOwinUserManager Create( + IPasswordConfiguration passwordConfiguration, + IIpResolver ipResolver, + IUserStore customUserStore, + IdentityErrorDescriber errors, + IDataProtectionProvider dataProtectionProvider, + ILogger> logger) + { + var options = new IdentityOptions(); + + // Configure validation logic for usernames + var userValidators = new List> { new BackOfficeUserValidator() }; + options.User.RequireUniqueEmail = true; + + // Configure validation logic for passwords + var passwordValidators = new List> { new PasswordValidator() }; + options.Password.RequiredLength = passwordConfiguration.RequiredLength; + options.Password.RequireNonAlphanumeric = passwordConfiguration.RequireNonLetterOrDigit; + options.Password.RequireDigit = passwordConfiguration.RequireDigit; + options.Password.RequireLowercase = passwordConfiguration.RequireLowercase; + options.Password.RequireUppercase = passwordConfiguration.RequireUppercase; + + // Ensure Umbraco security stamp claim type is used + options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier; + options.ClaimsIdentity.UserNameClaimType = ClaimTypes.Name; + options.ClaimsIdentity.RoleClaimType = ClaimTypes.Role; + options.ClaimsIdentity.SecurityStampClaimType = Constants.Web.SecurityStampClaimType; + + options.Lockout.AllowedForNewUsers = true; + options.Lockout.MaxFailedAccessAttempts = passwordConfiguration.MaxFailedAccessAttemptsBeforeLockout; + //NOTE: This just needs to be in the future, we currently don't support a lockout timespan, it's either they are locked + // or they are not locked, but this determines what is set on the account lockout date which corresponds to whether they are + // locked out or not. + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromDays(30); + + return new BackOfficeOwinUserManager( + passwordConfiguration, + ipResolver, + customUserStore, + new OptionsWrapper(options), + userValidators, + passwordValidators, + new NopLookupNormalizer(), + errors, + dataProtectionProvider, + logger); + } + + #endregion + + protected void InitUserManager(BackOfficeOwinUserManager manager, IDataProtectionProvider dataProtectionProvider) + { + // use a custom hasher based on our membership provider + PasswordHasher = GetDefaultPasswordHasher(PasswordConfiguration); + + // set OWIN data protection token provider as default + if (dataProtectionProvider != null) + { + manager.RegisterTokenProvider( + TokenOptions.DefaultProvider, + new OwinDataProtectorTokenProvider(dataProtectionProvider.Create("ASP.NET Identity")) + { + TokenLifespan = TimeSpan.FromDays(3) + }); + } + + // register ASP.NET Core Identity token providers + manager.RegisterTokenProvider(TokenOptions.DefaultEmailProvider, new EmailTokenProvider()); + manager.RegisterTokenProvider(TokenOptions.DefaultPhoneProvider, new PhoneNumberTokenProvider()); + manager.RegisterTokenProvider(TokenOptions.DefaultAuthenticatorProvider, new AuthenticatorTokenProvider()); + } + } +} diff --git a/src/Umbraco.Web/Security/BackOfficeUserManager.cs b/src/Umbraco.Web/Security/BackOfficeUserManager.cs deleted file mode 100644 index ed2df536e7..0000000000 --- a/src/Umbraco.Web/Security/BackOfficeUserManager.cs +++ /dev/null @@ -1,582 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Security.Claims; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Owin.Security.DataProtection; -using Umbraco.Core; -using Umbraco.Core.BackOffice; -using Umbraco.Core.Configuration; -using Umbraco.Core.Mapping; -using Umbraco.Core.Security; -using Umbraco.Core.Services; -using Umbraco.Net; - -namespace Umbraco.Web.Security -{ - public class BackOfficeUserManager : BackOfficeUserManager - { - public const string OwinMarkerKey = "Umbraco.Web.Security.Identity.BackOfficeUserManagerMarker"; - - public BackOfficeUserManager( - IPasswordConfiguration passwordConfiguration, - IIpResolver ipResolver, - IUserStore store, - IOptions optionsAccessor, - IEnumerable> userValidators, - IEnumerable> passwordValidators, - ILookupNormalizer keyNormalizer, - IdentityErrorDescriber errors, - IDataProtectionProvider dataProtectionProvider, - ILogger> logger) - : base(passwordConfiguration, ipResolver, store, optionsAccessor, userValidators, passwordValidators, keyNormalizer, errors, null, logger) - { - InitUserManager(this, dataProtectionProvider); - } - - #region Static Create methods - - /// - /// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager - /// - public static BackOfficeUserManager Create( - IUserService userService, - IEntityService entityService, - IExternalLoginService externalLoginService, - IGlobalSettings globalSettings, - UmbracoMapper mapper, - IPasswordConfiguration passwordConfiguration, - IIpResolver ipResolver, - IdentityErrorDescriber errors, - IDataProtectionProvider dataProtectionProvider, - ILogger> logger) - { - var store = new BackOfficeUserStore(userService, entityService, externalLoginService, globalSettings, mapper); - - return Create( - passwordConfiguration, - ipResolver, - store, - errors, - dataProtectionProvider, - logger); - } - - /// - /// Creates a BackOfficeUserManager instance with all default options and a custom BackOfficeUserManager instance - /// - public static BackOfficeUserManager Create( - IPasswordConfiguration passwordConfiguration, - IIpResolver ipResolver, - IUserStore customUserStore, - IdentityErrorDescriber errors, - IDataProtectionProvider dataProtectionProvider, - ILogger> logger) - { - var options = new IdentityOptions(); - - // Configure validation logic for usernames - var userValidators = new List> { new BackOfficeUserValidator() }; - options.User.RequireUniqueEmail = true; - - // Configure validation logic for passwords - var passwordValidators = new List> { new PasswordValidator() }; - options.Password.RequiredLength = passwordConfiguration.RequiredLength; - options.Password.RequireNonAlphanumeric = passwordConfiguration.RequireNonLetterOrDigit; - options.Password.RequireDigit = passwordConfiguration.RequireDigit; - options.Password.RequireLowercase = passwordConfiguration.RequireLowercase; - options.Password.RequireUppercase = passwordConfiguration.RequireUppercase; - - // Ensure Umbraco security stamp claim type is used - options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier; - options.ClaimsIdentity.UserNameClaimType = ClaimTypes.Name; - options.ClaimsIdentity.RoleClaimType = ClaimTypes.Role; - options.ClaimsIdentity.SecurityStampClaimType = Constants.Web.SecurityStampClaimType; - - options.Lockout.AllowedForNewUsers = true; - options.Lockout.MaxFailedAccessAttempts = passwordConfiguration.MaxFailedAccessAttemptsBeforeLockout; - //NOTE: This just needs to be in the future, we currently don't support a lockout timespan, it's either they are locked - // or they are not locked, but this determines what is set on the account lockout date which corresponds to whether they are - // locked out or not. - options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromDays(30); - - return new BackOfficeUserManager( - passwordConfiguration, - ipResolver, - customUserStore, - new OptionsWrapper(options), - userValidators, - passwordValidators, - new NopLookupNormalizer(), - errors, - dataProtectionProvider, - logger); - } - - #endregion - } - - public class BackOfficeUserManager : UserManager - where T : BackOfficeIdentityUser - { - private PasswordGenerator _passwordGenerator; - - public BackOfficeUserManager( - IPasswordConfiguration passwordConfiguration, - IIpResolver ipResolver, - IUserStore store, - IOptions optionsAccessor, - IEnumerable> userValidators, - IEnumerable> passwordValidators, - ILookupNormalizer keyNormalizer, - IdentityErrorDescriber errors, - IServiceProvider services, - ILogger> logger) - : base(store, optionsAccessor, null, userValidators, passwordValidators, keyNormalizer, errors, services, logger) - { - PasswordConfiguration = passwordConfiguration ?? throw new ArgumentNullException(nameof(passwordConfiguration)); - IpResolver = ipResolver ?? throw new ArgumentNullException(nameof(ipResolver)); - } - - #region What we do not currently support - // TODO: We could support this - but a user claims will mostly just be what is in the auth cookie - public override bool SupportsUserClaim => false; - - // TODO: Support this - public override bool SupportsQueryableUsers => false; - - /// - /// Developers will need to override this to support custom 2 factor auth - /// - public override bool SupportsUserTwoFactor => false; - - // TODO: Support this - public override bool SupportsUserPhoneNumber => false; - #endregion - - /// - /// Initializes the user manager with the correct options - /// - protected void InitUserManager( - BackOfficeUserManager manager, - IDataProtectionProvider dataProtectionProvider) - { - // use a custom hasher based on our membership provider - PasswordHasher = GetDefaultPasswordHasher(PasswordConfiguration); - - // set OWIN data protection token provider as default - if (dataProtectionProvider != null) - { - manager.RegisterTokenProvider( - TokenOptions.DefaultProvider, - new OwinDataProtectorTokenProvider(dataProtectionProvider.Create("ASP.NET Identity")) - { - TokenLifespan = TimeSpan.FromDays(3) - }); - } - - // register ASP.NET Core Identity token providers - manager.RegisterTokenProvider(TokenOptions.DefaultEmailProvider, new EmailTokenProvider()); - manager.RegisterTokenProvider(TokenOptions.DefaultPhoneProvider, new PhoneNumberTokenProvider()); - manager.RegisterTokenProvider(TokenOptions.DefaultAuthenticatorProvider, new AuthenticatorTokenProvider()); - } - - /// - /// Used to validate a user's session - /// - /// - /// - /// - public virtual async Task ValidateSessionIdAsync(string userId, string sessionId) - { - var userSessionStore = Store as IUserSessionStore; - //if this is not set, for backwards compat (which would be super rare), we'll just approve it - if (userSessionStore == null) return true; - - return await userSessionStore.ValidateSessionIdAsync(userId, sessionId); - } - - /// - /// This will determine which password hasher to use based on what is defined in config - /// - /// - protected virtual IPasswordHasher GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration) - { - //we can use the user aware password hasher (which will be the default and preferred way) - return new UserAwarePasswordHasher(new PasswordSecurity(passwordConfiguration)); - } - - /// - /// Gets/sets the default back office user password checker - /// - public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get; set; } - public IPasswordConfiguration PasswordConfiguration { get; } - public IIpResolver IpResolver { get; } - - /// - /// Helper method to generate a password for a user based on the current password validator - /// - /// - public string GeneratePassword() - { - if (_passwordGenerator == null) _passwordGenerator = new PasswordGenerator(PasswordConfiguration); - var password = _passwordGenerator.GeneratePassword(); - return password; - } - - /// - /// Override to check the user approval value as well as the user lock out date, by default this only checks the user's locked out date - /// - /// - /// - /// - /// In the ASP.NET Identity world, there is only one value for being locked out, in Umbraco we have 2 so when checking this for Umbraco we need to check both values - /// - public override async Task IsLockedOutAsync(T user) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - - if (user.IsApproved == false) return true; - - return await base.IsLockedOutAsync(user); - } - - #region Overrides for password logic - - /// - /// Logic used to validate a username and password - /// - /// - /// - /// - /// - /// By default this uses the standard ASP.Net Identity approach which is: - /// * Get password store - /// * Call VerifyPasswordAsync with the password store + user + password - /// * Uses the PasswordHasher.VerifyHashedPassword to compare the stored password - /// - /// In some cases people want simple custom control over the username/password check, for simplicity - /// sake, developers would like the users to simply validate against an LDAP directory but the user - /// data remains stored inside of Umbraco. - /// See: http://issues.umbraco.org/issue/U4-7032 for the use cases. - /// - /// We've allowed this check to be overridden with a simple callback so that developers don't actually - /// have to implement/override this class. - /// - public override async Task CheckPasswordAsync(T user, string password) - { - if (BackOfficeUserPasswordChecker != null) - { - var result = await BackOfficeUserPasswordChecker.CheckPasswordAsync(user, password); - - if (user.HasIdentity == false) - { - return false; - } - - //if the result indicates to not fallback to the default, then return true if the credentials are valid - if (result != BackOfficeUserPasswordCheckerResult.FallbackToDefaultChecker) - { - return result == BackOfficeUserPasswordCheckerResult.ValidCredentials; - } - } - - //we cannot proceed if the user passed in does not have an identity - if (user.HasIdentity == false) - return false; - - //use the default behavior - return await base.CheckPasswordAsync(user, password); - } - - /// - /// This is a special method that will reset the password but will raise the Password Changed event instead of the reset event - /// - /// - /// - /// - /// - /// - /// We use this because in the back office the only way an admin can change another user's password without first knowing their password - /// is to generate a token and reset it, however, when we do this we want to track a password change, not a password reset - /// - public async Task ChangePasswordWithResetAsync(int userId, string token, string newPassword) - { - var user = await base.FindByIdAsync(userId.ToString()); - if (user == null) throw new InvalidOperationException("Could not find user"); - - var result = await base.ResetPasswordAsync(user, token, newPassword); - if (result.Succeeded) RaisePasswordChangedEvent(userId); - return result; - } - - public override async Task ChangePasswordAsync(T user, string currentPassword, string newPassword) - { - var result = await base.ChangePasswordAsync(user, currentPassword, newPassword); - if (result.Succeeded) RaisePasswordChangedEvent(user.Id); - return result; - } - - /// - /// Override to determine how to hash the password - /// - /// - /// - /// - /// - /// - /// This method is called anytime the password needs to be hashed for storage (i.e. including when reset password is used) - /// - protected override async Task UpdatePasswordHash(T user, string newPassword, bool validatePassword) - { - user.LastPasswordChangeDateUtc = DateTime.UtcNow; - - if (validatePassword) - { - var validate = await ValidatePasswordAsync(user, newPassword); - if (!validate.Succeeded) - { - return validate; - } - } - - var passwordStore = Store as IUserPasswordStore; - if (passwordStore == null) throw new NotSupportedException("The current user store does not implement " + typeof(IUserPasswordStore<>)); - - var hash = newPassword != null ? PasswordHasher.HashPassword(user, newPassword) : null; - await passwordStore.SetPasswordHashAsync(user, hash, CancellationToken); - await UpdateSecurityStampInternal(user); - return IdentityResult.Success; - } - - /// - /// This is copied from the underlying .NET base class since they decided to not expose it - /// - /// - /// - private async Task UpdateSecurityStampInternal(T user) - { - if (SupportsUserSecurityStamp == false) return; - await GetSecurityStore().SetSecurityStampAsync(user, NewSecurityStamp(), CancellationToken.None); - } - - /// - /// This is copied from the underlying .NET base class since they decided to not expose it - /// - /// - private IUserSecurityStampStore GetSecurityStore() - { - var store = Store as IUserSecurityStampStore; - if (store == null) throw new NotSupportedException("The current user store does not implement " + typeof(IUserSecurityStampStore<>)); - return store; - } - - /// - /// This is copied from the underlying .NET base class since they decided to not expose it - /// - /// - private static string NewSecurityStamp() - { - return Guid.NewGuid().ToString(); - } - - #endregion - - public override async Task SetLockoutEndDateAsync(T user, DateTimeOffset? lockoutEnd) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - - var result = await base.SetLockoutEndDateAsync(user, lockoutEnd); - - // The way we unlock is by setting the lockoutEnd date to the current datetime - if (result.Succeeded && lockoutEnd >= DateTimeOffset.UtcNow) - { - RaiseAccountLockedEvent(user.Id); - } - else - { - RaiseAccountUnlockedEvent(user.Id); - //Resets the login attempt fails back to 0 when unlock is clicked - await ResetAccessFailedCountAsync(user); - } - - return result; - } - - public override async Task ResetAccessFailedCountAsync(T user) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - - var lockoutStore = (IUserLockoutStore)Store; - var accessFailedCount = await GetAccessFailedCountAsync(user); - - if (accessFailedCount == 0) - return IdentityResult.Success; - - await lockoutStore.ResetAccessFailedCountAsync(user, CancellationToken.None); - //raise the event now that it's reset - RaiseResetAccessFailedCountEvent(user.Id); - return await UpdateAsync(user); - } - - /// - /// Overrides the Microsoft ASP.NET user management method - /// - /// - /// - /// returns a Async Task - /// - /// - /// Doesn't set fail attempts back to 0 - /// - public override async Task AccessFailedAsync(T user) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - - var lockoutStore = Store as IUserLockoutStore; - if (lockoutStore == null) throw new NotSupportedException("The current user store does not implement " + typeof(IUserLockoutStore<>)); - - var count = await lockoutStore.IncrementAccessFailedCountAsync(user, CancellationToken.None); - - if (count >= Options.Lockout.MaxFailedAccessAttempts) - { - await lockoutStore.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.Add(Options.Lockout.DefaultLockoutTimeSpan), - CancellationToken.None); - //NOTE: in normal aspnet identity this would do set the number of failed attempts back to 0 - //here we are persisting the value for the back office - } - - var result = await UpdateAsync(user); - - //Slightly confusing: this will return a Success if we successfully update the AccessFailed count - if (result.Succeeded) RaiseLoginFailedEvent(user.Id); - - return result; - } - - internal void RaiseAccountLockedEvent(int userId) - { - OnAccountLocked(new IdentityAuditEventArgs(AuditEvent.AccountLocked, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); - } - - internal void RaiseAccountUnlockedEvent(int userId) - { - OnAccountUnlocked(new IdentityAuditEventArgs(AuditEvent.AccountUnlocked, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); - } - - internal void RaiseForgotPasswordRequestedEvent(int userId) - { - OnForgotPasswordRequested(new IdentityAuditEventArgs(AuditEvent.ForgotPasswordRequested, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); - } - - internal void RaiseForgotPasswordChangedSuccessEvent(int userId) - { - OnForgotPasswordChangedSuccess(new IdentityAuditEventArgs(AuditEvent.ForgotPasswordChangedSuccess, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); - } - - internal void RaiseLoginFailedEvent(int userId) - { - OnLoginFailed(new IdentityAuditEventArgs(AuditEvent.LoginFailed, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); - } - - internal void RaiseInvalidLoginAttemptEvent(string username) - { - OnLoginFailed(new IdentityAuditEventArgs(AuditEvent.LoginFailed, IpResolver.GetCurrentRequestIpAddress(), username, string.Format("Attempted login for username '{0}' failed", username))); - } - - internal void RaiseLoginRequiresVerificationEvent(int userId) - { - OnLoginRequiresVerification(new IdentityAuditEventArgs(AuditEvent.LoginRequiresVerification, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); - } - - internal void RaiseLoginSuccessEvent(int userId) - { - OnLoginSuccess(new IdentityAuditEventArgs(AuditEvent.LoginSucces, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); - } - - internal void RaiseLogoutSuccessEvent(int userId) - { - OnLogoutSuccess(new IdentityAuditEventArgs(AuditEvent.LogoutSuccess, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); - } - - internal void RaisePasswordChangedEvent(int userId) - { - OnPasswordChanged(new IdentityAuditEventArgs(AuditEvent.PasswordChanged, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); - } - - internal void RaiseResetAccessFailedCountEvent(int userId) - { - OnResetAccessFailedCount(new IdentityAuditEventArgs(AuditEvent.ResetAccessFailedCount, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId)); - } - - public static event EventHandler AccountLocked; - public static event EventHandler AccountUnlocked; - public static event EventHandler ForgotPasswordRequested; - public static event EventHandler ForgotPasswordChangedSuccess; - public static event EventHandler LoginFailed; - public static event EventHandler LoginRequiresVerification; - public static event EventHandler LoginSuccess; - public static event EventHandler LogoutSuccess; - public static event EventHandler PasswordChanged; - public static event EventHandler PasswordReset; - public static event EventHandler ResetAccessFailedCount; - - protected virtual void OnAccountLocked(IdentityAuditEventArgs e) - { - if (AccountLocked != null) AccountLocked(this, e); - } - - protected virtual void OnAccountUnlocked(IdentityAuditEventArgs e) - { - if (AccountUnlocked != null) AccountUnlocked(this, e); - } - - protected virtual void OnForgotPasswordRequested(IdentityAuditEventArgs e) - { - if (ForgotPasswordRequested != null) ForgotPasswordRequested(this, e); - } - - protected virtual void OnForgotPasswordChangedSuccess(IdentityAuditEventArgs e) - { - if (ForgotPasswordChangedSuccess != null) ForgotPasswordChangedSuccess(this, e); - } - - protected virtual void OnLoginFailed(IdentityAuditEventArgs e) - { - if (LoginFailed != null) LoginFailed(this, e); - } - - protected virtual void OnLoginRequiresVerification(IdentityAuditEventArgs e) - { - if (LoginRequiresVerification != null) LoginRequiresVerification(this, e); - } - - protected virtual void OnLoginSuccess(IdentityAuditEventArgs e) - { - if (LoginSuccess != null) LoginSuccess(this, e); - } - - protected virtual void OnLogoutSuccess(IdentityAuditEventArgs e) - { - if (LogoutSuccess != null) LogoutSuccess(this, e); - } - - protected virtual void OnPasswordChanged(IdentityAuditEventArgs e) - { - if (PasswordChanged != null) PasswordChanged(this, e); - } - - protected virtual void OnPasswordReset(IdentityAuditEventArgs e) - { - if (PasswordReset != null) PasswordReset(this, e); - } - - protected virtual void OnResetAccessFailedCount(IdentityAuditEventArgs e) - { - if (ResetAccessFailedCount != null) ResetAccessFailedCount(this, e); - } - } -} diff --git a/src/Umbraco.Web/Security/BackOfficeUserManagerMarker.cs b/src/Umbraco.Web/Security/BackOfficeUserManagerMarker.cs index d9d3f0e6f6..dd657b48bf 100644 --- a/src/Umbraco.Web/Security/BackOfficeUserManagerMarker.cs +++ b/src/Umbraco.Web/Security/BackOfficeUserManagerMarker.cs @@ -15,9 +15,9 @@ namespace Umbraco.Web.Security where TManager : BackOfficeUserManager where TUser : BackOfficeIdentityUser { - public BackOfficeUserManager GetManager(IOwinContext owin) + public BackOfficeOwinUserManager GetManager(IOwinContext owin) { - var mgr = owin.Get() as BackOfficeUserManager; + var mgr = owin.Get() as BackOfficeOwinUserManager; if (mgr == null) throw new InvalidOperationException("Could not cast the registered back office user of type " + typeof(TManager) + " to " + typeof(BackOfficeUserManager)); return mgr; } diff --git a/src/Umbraco.Web/Security/BackOfficeUserStore.cs b/src/Umbraco.Web/Security/BackOfficeUserStore.cs deleted file mode 100644 index d9d1900951..0000000000 --- a/src/Umbraco.Web/Security/BackOfficeUserStore.cs +++ /dev/null @@ -1,917 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using Umbraco.Core; -using Umbraco.Core.BackOffice; -using Umbraco.Core.Configuration; -using Umbraco.Core.Mapping; -using Umbraco.Core.Models; -using Umbraco.Core.Models.Identity; -using Umbraco.Core.Models.Membership; -using Umbraco.Core.Services; - -namespace Umbraco.Web.Security -{ - public class BackOfficeUserStore : DisposableObjectSlim, - IUserPasswordStore, - IUserEmailStore, - IUserLoginStore, - IUserRoleStore, - IUserSecurityStampStore, - IUserLockoutStore, - IUserTwoFactorStore, - IUserSessionStore - - // 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 IEntityService _entityService; - private readonly IExternalLoginService _externalLoginService; - private readonly IGlobalSettings _globalSettings; - private readonly UmbracoMapper _mapper; - private bool _disposed = false; - - public BackOfficeUserStore(IUserService userService, IEntityService entityService, IExternalLoginService externalLoginService, IGlobalSettings globalSettings, UmbracoMapper mapper) - { - _userService = userService; - _entityService = entityService; - _externalLoginService = externalLoginService; - _globalSettings = globalSettings; - if (userService == null) throw new ArgumentNullException("userService"); - if (externalLoginService == null) throw new ArgumentNullException("externalLoginService"); - _mapper = mapper; - - _userService = userService; - _externalLoginService = externalLoginService; - } - - /// - /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. - /// - protected override void DisposeResources() - { - _disposed = true; - } - - public Task GetUserIdAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - return Task.FromResult(user.Id.ToString()); - } - - public Task GetUserNameAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - return Task.FromResult(user.UserName); - } - - public Task SetUserNameAsync(BackOfficeIdentityUser user, string userName, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - user.UserName = userName; - return Task.CompletedTask; - } - - public Task GetNormalizedUserNameAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) - { - return GetUserNameAsync(user, cancellationToken); - } - - public Task SetNormalizedUserNameAsync(BackOfficeIdentityUser user, string normalizedName, CancellationToken cancellationToken) - { - return SetUserNameAsync(user, normalizedName, cancellationToken); - } - - /// - /// Insert a new user - /// - /// - /// - /// - public Task CreateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(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. - //this will hash the guid with a salt so should be nicely random - var aspHasher = new PasswordHasher(); - var emptyPasswordValue = Constants.Security.EmptyPasswordPrefix + - aspHasher.HashPassword(user, Guid.NewGuid().ToString("N")); - - var userEntity = new User(_globalSettings, user.Name, user.Email, user.UserName, emptyPasswordValue) - { - Language = user.Culture ?? _globalSettings.DefaultUILanguage, - StartContentIds = user.StartContentIds ?? new int[] { }, - StartMediaIds = user.StartMediaIds ?? new int[] { }, - IsLockedOut = user.IsLockedOut, - }; - - UpdateMemberProperties(userEntity, user); - - // TODO: We should deal with Roles --> User Groups here which we currently are not doing - - _userService.Save(userEntity); - - if (!userEntity.HasIdentity) throw new DataException("Could not create the user, check logs for details"); - - //re-assign id - user.Id = userEntity.Id; - - return Task.FromResult(IdentityResult.Success); - } - - /// - /// Update a user - /// - /// - /// - /// - public async Task UpdateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - var found = _userService.GetUserById(user.Id); - if (found != null) - { - // we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. - var isLoginsPropertyDirty = user.IsPropertyDirty("Logins"); - - if (UpdateMemberProperties(found, user)) - { - _userService.Save(found); - } - - if (isLoginsPropertyDirty) - { - var logins = await GetLoginsAsync(user); - _externalLoginService.SaveUserLogins(found.Id, logins.Select(UserLoginInfoWrapper.Wrap)); - } - } - - return IdentityResult.Success; - } - - /// - /// Delete a user - /// - /// - /// - public Task DeleteAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - var found = _userService.GetUserById(user.Id); - if (found != null) - { - _userService.Delete(found); - } - _externalLoginService.DeleteUserLogins(user.Id); - - return Task.FromResult(IdentityResult.Success); - } - - /// - /// Finds a user - /// - /// - /// - /// - public async Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - - var user = _userService.GetUserById(UserIdToInt(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, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - 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, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - if (passwordHash == null) throw new ArgumentNullException(nameof(passwordHash)); - if (string.IsNullOrEmpty(passwordHash)) throw new ArgumentException("Value can't be empty.", nameof(passwordHash)); - - user.PasswordHash = passwordHash; - - return Task.CompletedTask; - } - - /// - /// Get the user password hash - /// - /// - /// - /// - public Task GetPasswordHashAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - return Task.FromResult(user.PasswordHash); - } - - /// - /// Returns true if a user has a password set - /// - /// - /// - /// - public Task HasPasswordAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - return Task.FromResult(string.IsNullOrEmpty(user.PasswordHash) == false); - } - - /// - /// Set the user email - /// - /// - /// - /// - public Task SetEmailAsync(BackOfficeIdentityUser user, string email, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - if (email.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(email)); - - user.Email = email; - - return Task.CompletedTask; - } - - /// - /// Get the user email - /// - /// - /// - /// - public Task GetEmailAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - return Task.FromResult(user.Email); - } - - /// - /// Returns true if the user email is confirmed - /// - /// - /// - /// - public Task GetEmailConfirmedAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - return Task.FromResult(user.EmailConfirmed); - } - - /// - /// Sets whether the user email is confirmed - /// - /// - /// - /// - public Task SetEmailConfirmedAsync(BackOfficeIdentityUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - user.EmailConfirmed = confirmed; - return Task.CompletedTask; - } - - /// - /// Returns the user associated with this email - /// - /// - /// - /// - public Task FindByEmailAsync(string email, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - var user = _userService.GetByEmail(email); - var result = user == null - ? null - : _mapper.Map(user); - - return Task.FromResult(AssignLoginsCallback(result)); - } - - public Task GetNormalizedEmailAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken) - { - return GetEmailAsync(user, cancellationToken); - } - - public Task SetNormalizedEmailAsync(BackOfficeIdentityUser user, string normalizedEmail, CancellationToken cancellationToken) - { - return SetEmailAsync(user, normalizedEmail, cancellationToken); - } - - /// - /// Adds a user login with the specified provider and key - /// - /// - /// - /// - /// - public Task AddLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - if (login == null) throw new ArgumentNullException(nameof(login)); - - var logins = user.Logins; - var instance = new IdentityUserLogin(login.LoginProvider, login.ProviderKey, user.Id); - var userLogin = instance; - logins.Add(userLogin); - - return Task.CompletedTask; - } - - /// - /// Removes the user login with the specified combination if it exists - /// - /// - /// - /// - /// - /// - public Task RemoveLoginAsync(BackOfficeIdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - var userLogin = user.Logins.SingleOrDefault(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey); - if (userLogin != null) user.Logins.Remove(userLogin); - - return Task.CompletedTask; - } - - /// - /// Returns the linked accounts for this user - /// - /// - /// - /// - public Task> GetLoginsAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - 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()); - } - - /// - /// Returns the user associated with this login - /// - /// - public Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - - //get all logins associated with the login id - var result = _externalLoginService.Find(UserLoginInfoWrapper.Wrap(new UserLoginInfo(loginProvider, providerKey, loginProvider))).ToArray(); - if (result.Any()) - { - //return the first user that matches the result - BackOfficeIdentityUser output = null; - foreach (var l in result) - { - var user = _userService.GetUserById(l.UserId); - if (user != null) - { - output = _mapper.Map(user); - break; - } - } - - return Task.FromResult(AssignLoginsCallback(output)); - } - - return Task.FromResult(null); - } - - - /// - /// Adds a user to a role (user group) - /// - /// - /// - /// - /// - public Task AddToRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) - { - 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)); - - var userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName); - - if (userRole == null) - { - user.AddRole(normalizedRoleName); - } - - return Task.CompletedTask; - } - - /// - /// Removes the role (user group) for the user - /// - /// - /// - /// - /// - public Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) - { - 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)); - - var userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName); - - if (userRole != null) - { - user.Roles.Remove(userRole); - } - - return Task.CompletedTask; - } - - /// - /// Returns the roles (user groups) for this user - /// - /// - /// - /// - public Task> GetRolesAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - return Task.FromResult((IList)user.Roles.Select(x => x.RoleId).ToList()); - } - - /// - /// Returns true if a user is in the role - /// - /// - /// - /// - /// - public Task IsInRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - return Task.FromResult(user.Roles.Select(x => x.RoleId).InvariantContains(normalizedRoleName)); - } - - /// - /// Lists all users of a given role. - /// - /// - /// Identity Role names are equal to Umbraco UserGroup alias. - /// - public Task> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (normalizedRoleName == null) throw new ArgumentNullException(nameof(normalizedRoleName)); - - var userGroup = _userService.GetUserGroupByAlias(normalizedRoleName); - - var users = _userService.GetAllInGroup(userGroup.Id); - IList backOfficeIdentityUsers = users.Select(x => _mapper.Map(x)).ToList(); - - return Task.FromResult(backOfficeIdentityUsers); - } - - /// - /// Set the security stamp for the user - /// - /// - /// - /// - /// - public Task SetSecurityStampAsync(BackOfficeIdentityUser user, string stamp, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - user.SecurityStamp = stamp; - return Task.CompletedTask; - } - - /// - /// Get the user security stamp - /// - /// - /// - /// - public Task GetSecurityStampAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - 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) - { - 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, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - - user.TwoFactorEnabled = false; - return Task.CompletedTask; - } - - /// - /// Returns whether two factor authentication is enabled for the user - /// - /// - /// - public virtual Task GetTwoFactorEnabledAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - - 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 support a timed lock out, when they are locked out, an admin will have to reset the status - /// - public Task GetLockoutEndDateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(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) - /// - /// - /// - /// - /// - /// Currently we do not support a timed lock out, when they are locked out, an admin will have to reset the status - /// - public Task SetLockoutEndDateAsync(BackOfficeIdentityUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - user.LockoutEndDateUtc = lockoutEnd.Value.UtcDateTime; - return Task.CompletedTask; - } - - /// - /// Used to record when an attempt to access the user has failed - /// - /// - /// - /// - public Task IncrementAccessFailedCountAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(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, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - user.AccessFailedCount = 0; - return Task.CompletedTask; - } - - /// - /// 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, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - return Task.FromResult(user.AccessFailedCount); - } - - /// - /// Returns true - /// - /// - /// - /// - public Task GetLockoutEnabledAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(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, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) throw new ArgumentNullException(nameof(user)); - - user.LockoutEnabled = enabled; - return Task.CompletedTask; - } - #endregion - - private bool UpdateMemberProperties(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 (identityUser.IsPropertyDirty("LastLoginDateUtc") - || (user.LastLoginDate != default(DateTime) && identityUser.LastLoginDateUtc.HasValue == false) - || identityUser.LastLoginDateUtc.HasValue && user.LastLoginDate.ToUniversalTime() != identityUser.LastLoginDateUtc.Value) - { - anythingChanged = true; - //if the LastLoginDate is being set to MinValue, don't convert it ToLocalTime - var dt = identityUser.LastLoginDateUtc == DateTime.MinValue ? DateTime.MinValue : identityUser.LastLoginDateUtc.Value.ToLocalTime(); - user.LastLoginDate = dt; - } - if (identityUser.IsPropertyDirty("LastPasswordChangeDateUtc") - || (user.LastPasswordChangeDate != default(DateTime) && identityUser.LastPasswordChangeDateUtc.HasValue == false) - || identityUser.LastPasswordChangeDateUtc.HasValue && user.LastPasswordChangeDate.ToUniversalTime() != identityUser.LastPasswordChangeDateUtc.Value) - { - anythingChanged = true; - user.LastPasswordChangeDate = identityUser.LastPasswordChangeDateUtc.Value.ToLocalTime(); - } - if (identityUser.IsPropertyDirty("EmailConfirmed") - || (user.EmailConfirmedDate.HasValue && user.EmailConfirmedDate.Value != default(DateTime) && identityUser.EmailConfirmed == false) - || ((user.EmailConfirmedDate.HasValue == false || user.EmailConfirmedDate.Value == default(DateTime)) && identityUser.EmailConfirmed)) - { - anythingChanged = true; - user.EmailConfirmedDate = identityUser.EmailConfirmed ? (DateTime?)DateTime.Now : null; - } - if (identityUser.IsPropertyDirty("Name") - && user.Name != identityUser.Name && identityUser.Name.IsNullOrWhiteSpace() == false) - { - anythingChanged = true; - user.Name = identityUser.Name; - } - if (identityUser.IsPropertyDirty("Email") - && user.Email != identityUser.Email && identityUser.Email.IsNullOrWhiteSpace() == false) - { - anythingChanged = true; - user.Email = identityUser.Email; - } - if (identityUser.IsPropertyDirty("AccessFailedCount") - && 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 (identityUser.IsPropertyDirty("UserName") - && user.Username != identityUser.UserName && identityUser.UserName.IsNullOrWhiteSpace() == false) - { - anythingChanged = true; - user.Username = identityUser.UserName; - } - if (identityUser.IsPropertyDirty("PasswordHash") - && user.RawPasswordValue != identityUser.PasswordHash && identityUser.PasswordHash.IsNullOrWhiteSpace() == false) - { - anythingChanged = true; - user.RawPasswordValue = identityUser.PasswordHash; - } - - if (identityUser.IsPropertyDirty("Culture") - && user.Language != identityUser.Culture && identityUser.Culture.IsNullOrWhiteSpace() == false) - { - anythingChanged = true; - user.Language = identityUser.Culture; - } - if (identityUser.IsPropertyDirty("StartMediaIds") - && user.StartMediaIds.UnsortedSequenceEqual(identityUser.StartMediaIds) == false) - { - anythingChanged = true; - user.StartMediaIds = identityUser.StartMediaIds; - } - if (identityUser.IsPropertyDirty("StartContentIds") - && user.StartContentIds.UnsortedSequenceEqual(identityUser.StartContentIds) == false) - { - anythingChanged = true; - user.StartContentIds = identityUser.StartContentIds; - } - if (user.SecurityStamp != identityUser.SecurityStamp) - { - anythingChanged = true; - user.SecurityStamp = identityUser.SecurityStamp; - } - - // TODO: Fix this for Groups too - if (identityUser.IsPropertyDirty("Roles") || identityUser.IsPropertyDirty("Groups")) - { - 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(); - - if (userGroupAliases.ContainsAll(combinedAliases) == false - || combinedAliases.ContainsAll(userGroupAliases) == false) - { - 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; - } - } - - //we should re-set the calculated start nodes - identityUser.CalculatedMediaStartNodeIds = user.CalculateMediaStartNodeIds(_entityService); - identityUser.CalculatedContentStartNodeIds = user.CalculateContentStartNodeIds(_entityService); - - //reset all changes - identityUser.ResetDirtyProperties(false); - - return anythingChanged; - } - - private void ThrowIfDisposed() - { - if (_disposed) throw new ObjectDisposedException(GetType().Name); - } - - public Task ValidateSessionIdAsync(string userId, string sessionId) - { - Guid guidSessionId; - if (Guid.TryParse(sessionId, out guidSessionId)) - { - return Task.FromResult(_userService.ValidateLoginSession(UserIdToInt(userId), guidSessionId)); - } - - return Task.FromResult(false); - } - - private static int UserIdToInt(string userId) - { - var attempt = userId.TryConvertTo(); - if (attempt.Success) return attempt.Result; - - throw new InvalidOperationException("Unable to convert user ID to int", attempt.Exception); - } - } -} diff --git a/src/Umbraco.Web/Security/BackOfficeUserValidator.cs b/src/Umbraco.Web/Security/BackOfficeUserValidator.cs deleted file mode 100644 index 951bdee4ed..0000000000 --- a/src/Umbraco.Web/Security/BackOfficeUserValidator.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using Umbraco.Core.BackOffice; - -namespace Umbraco.Web.Security -{ - public class BackOfficeUserValidator : UserValidator - where T : BackOfficeIdentityUser - { - public override async Task ValidateAsync(UserManager manager, T user) - { - // Don't validate if the user's email or username hasn't changed otherwise it's just wasting SQL queries. - if (user.IsPropertyDirty("Email") || user.IsPropertyDirty("UserName")) - { - return await base.ValidateAsync(manager, user); - } - return IdentityResult.Success; - } - } -} diff --git a/src/Umbraco.Web/Security/IBackOfficeUserManagerMarker.cs b/src/Umbraco.Web/Security/IBackOfficeUserManagerMarker.cs index 2b1f9927f2..16c0666c9c 100644 --- a/src/Umbraco.Web/Security/IBackOfficeUserManagerMarker.cs +++ b/src/Umbraco.Web/Security/IBackOfficeUserManagerMarker.cs @@ -1,5 +1,4 @@ using Microsoft.Owin; -using Umbraco.Core.BackOffice; namespace Umbraco.Web.Security { @@ -10,6 +9,6 @@ namespace Umbraco.Web.Security /// internal interface IBackOfficeUserManagerMarker { - BackOfficeUserManager GetManager(IOwinContext owin); + BackOfficeOwinUserManager GetManager(IOwinContext owin); } } diff --git a/src/Umbraco.Web/Security/SessionIdValidator.cs b/src/Umbraco.Web/Security/SessionIdValidator.cs index 9edae8da10..090b6c6dac 100644 --- a/src/Umbraco.Web/Security/SessionIdValidator.cs +++ b/src/Umbraco.Web/Security/SessionIdValidator.cs @@ -86,7 +86,7 @@ namespace Umbraco.Web.Security if (validate == false) return true; - var manager = owinCtx.Get(); + var manager = owinCtx.Get(); if (manager == null) return false; diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index 3c17bd370a..6d3de9be1d 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -70,8 +70,8 @@ namespace Umbraco.Web.Security } } - private BackOfficeUserManager _userManager; - protected BackOfficeUserManager UserManager + private BackOfficeOwinUserManager _userManager; + protected BackOfficeOwinUserManager UserManager => _userManager ?? (_userManager = _httpContextAccessor.GetRequiredHttpContext().GetOwinContext().GetBackOfficeUserManager()); [Obsolete("This needs to be removed, ASP.NET Identity should always be used for this operation, this is currently only used in the installer which needs to be updated")] diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 9817431f76..3bc0b68906 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -195,12 +195,10 @@ - + - - diff --git a/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs b/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs index b171356e23..f0c79b24f2 100644 --- a/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs +++ b/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs @@ -3,7 +3,6 @@ using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Web.WebApi.Filters; -using Umbraco.Core.BackOffice; using Umbraco.Core.Persistence; using Umbraco.Core.Services; using Umbraco.Web.Security; @@ -30,7 +29,7 @@ namespace Umbraco.Web.WebApi [EnableDetailedErrors] public abstract class UmbracoAuthorizedApiController : UmbracoApiController { - private Security.BackOfficeUserManager _userManager; + private BackOfficeOwinUserManager _userManager; protected UmbracoAuthorizedApiController() { @@ -44,7 +43,7 @@ namespace Umbraco.Web.WebApi /// /// Gets the user manager. /// - protected Security.BackOfficeUserManager UserManager + protected BackOfficeOwinUserManager UserManager => _userManager ?? (_userManager = TryGetOwinContext().Result.GetBackOfficeUserManager()); } } From 6f75fe6cc48f5e21d74bd5405d5132540fecae1d Mon Sep 17 00:00:00 2001 From: Scott Brady Date: Mon, 18 May 2020 12:51:24 +0100 Subject: [PATCH 11/13] Reintroduced missing OWIN user manager functionality --- .../BackOffice/BackOfficeUserManager.cs | 3 --- ...serManagerTests.cs => BackOfficeOwinUserManagerTests.cs} | 2 +- src/Umbraco.Tests/Umbraco.Tests.csproj | 2 +- src/Umbraco.Web/Security/BackOfficeOwinUserManager.cs | 6 ++++++ 4 files changed, 8 insertions(+), 5 deletions(-) rename src/Umbraco.Tests/Security/{BackOfficeUserManagerTests.cs => BackOfficeOwinUserManagerTests.cs} (98%) diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs index 99d0265d06..b98a9bb68c 100644 --- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs +++ b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs @@ -37,7 +37,6 @@ namespace Umbraco.Core.BackOffice ILogger> logger) : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) { - // PasswordConfiguration = passwordConfiguration ?? throw new ArgumentNullException(nameof(passwordConfiguration)); IpResolver = ipResolver ?? throw new ArgumentNullException(nameof(ipResolver)); } @@ -95,8 +94,6 @@ namespace Umbraco.Core.BackOffice /// public string GeneratePassword() { - throw new NotImplementedException(); - if (_passwordGenerator == null) _passwordGenerator = new PasswordGenerator(PasswordConfiguration); var password = _passwordGenerator.GeneratePassword(); return password; diff --git a/src/Umbraco.Tests/Security/BackOfficeUserManagerTests.cs b/src/Umbraco.Tests/Security/BackOfficeOwinUserManagerTests.cs similarity index 98% rename from src/Umbraco.Tests/Security/BackOfficeUserManagerTests.cs rename to src/Umbraco.Tests/Security/BackOfficeOwinUserManagerTests.cs index cf3d54a4ce..705387b3ae 100644 --- a/src/Umbraco.Tests/Security/BackOfficeUserManagerTests.cs +++ b/src/Umbraco.Tests/Security/BackOfficeOwinUserManagerTests.cs @@ -14,7 +14,7 @@ using Umbraco.Web.Security; namespace Umbraco.Tests.Security { - public class BackOfficeUserManagerTests + public class BackOfficeOwinUserManagerTests { [Test] public async Task CheckPasswordAsync_When_Default_Password_Hasher_Validates_Umbraco7_Hash_Expect_Valid_Password() diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 22f5348501..56af9c6053 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -148,7 +148,7 @@ - + diff --git a/src/Umbraco.Web/Security/BackOfficeOwinUserManager.cs b/src/Umbraco.Web/Security/BackOfficeOwinUserManager.cs index 3a5ba8f706..005ae972c3 100644 --- a/src/Umbraco.Web/Security/BackOfficeOwinUserManager.cs +++ b/src/Umbraco.Web/Security/BackOfficeOwinUserManager.cs @@ -9,6 +9,7 @@ using Umbraco.Core; using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration; using Umbraco.Core.Mapping; +using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Net; @@ -116,6 +117,11 @@ namespace Umbraco.Web.Security #endregion + protected override IPasswordHasher GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration) + { + return new UserAwarePasswordHasher(new PasswordSecurity(passwordConfiguration)); + } + protected void InitUserManager(BackOfficeOwinUserManager manager, IDataProtectionProvider dataProtectionProvider) { // use a custom hasher based on our membership provider From a44dde9432670cb15c74329112fe6518e1cc2eb1 Mon Sep 17 00:00:00 2001 From: Scott Brady Date: Mon, 18 May 2020 13:00:32 +0100 Subject: [PATCH 12/13] Added IdentityExtensions --- .../BackOffice/Extensions/IdentityExtensions.cs | 16 ++++++++++++++++ .../Install/InstallSteps/NewInstallStep.cs | 3 ++- .../BackOffice/IdentityExtensionsTests.cs} | 4 ++-- src/Umbraco.Tests/Umbraco.Tests.csproj | 1 - ...bracoBackOfficeServiceCollectionExtensions.cs | 2 +- .../Editors/AuthenticationController.cs | 2 +- src/Umbraco.Web/Editors/BackOfficeController.cs | 1 + src/Umbraco.Web/Editors/CurrentUserController.cs | 1 + src/Umbraco.Web/Editors/PasswordChanger.cs | 1 + src/Umbraco.Web/Editors/UsersController.cs | 2 +- .../Security/AuthenticationExtensions.cs | 7 ------- 11 files changed, 26 insertions(+), 14 deletions(-) create mode 100644 src/Umbraco.Infrastructure/BackOffice/Extensions/IdentityExtensions.cs rename src/{Umbraco.Tests/Security/AuthenticationExtensionsTests.cs => Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/IdentityExtensionsTests.cs} (94%) diff --git a/src/Umbraco.Infrastructure/BackOffice/Extensions/IdentityExtensions.cs b/src/Umbraco.Infrastructure/BackOffice/Extensions/IdentityExtensions.cs new file mode 100644 index 0000000000..95a63c6001 --- /dev/null +++ b/src/Umbraco.Infrastructure/BackOffice/Extensions/IdentityExtensions.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Identity; + +namespace Umbraco.Extensions +{ + public static class IdentityExtensions + { + public static string ToErrorMessage(this IEnumerable errors) + { + if (errors == null) throw new ArgumentNullException(nameof(errors)); + return string.Join(", ", errors.Select(x => x.Description).ToList()); + } + } +} diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs index 133272e8de..fdc38d8836 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Services; using Umbraco.Web.Install.Models; using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Extensions; namespace Umbraco.Web.Install.InstallSteps { @@ -75,7 +76,7 @@ namespace Umbraco.Web.Install.InstallSteps var resetResult = await _userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, user.Password.Trim()); if (!resetResult.Succeeded) - throw new InvalidOperationException("Could not reset password: " + string.Join(", ", "error" /*resetResult.Errors.ToErrorMessage()*/)); + throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors.ToErrorMessage())); if (user.SubscribeToNewsLetter) { diff --git a/src/Umbraco.Tests/Security/AuthenticationExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/IdentityExtensionsTests.cs similarity index 94% rename from src/Umbraco.Tests/Security/AuthenticationExtensionsTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/IdentityExtensionsTests.cs index e622458c39..baf4a6f062 100644 --- a/src/Umbraco.Tests/Security/AuthenticationExtensionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/IdentityExtensionsTests.cs @@ -2,11 +2,11 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Identity; using NUnit.Framework; -using Umbraco.Web.Security; +using Umbraco.Extensions; namespace Umbraco.Tests.Security { - public class AuthenticationExtensionsTests + public class IdentityExtensionsTests { [Test] public void ToErrorMessage_When_Errors_Are_Null_Expect_ArgumentNullException() diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 56af9c6053..3ca52b73f1 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -146,7 +146,6 @@ - diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs index 4325e10622..d31b0fb5b0 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs @@ -22,7 +22,7 @@ namespace Umbraco.Extensions services.TryAddSingleton(s => new MapDefinitionCollection(new[] {s.GetService()})); services.TryAddSingleton(); - services.TryAddScoped(); + services.TryAddScoped(); services.AddIdentityCore(); services.AddIdentityCore(options => diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index d65bb043c5..e2dff57cca 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -19,7 +19,6 @@ using Umbraco.Web.Mvc; using Umbraco.Web.Security; using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; -using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; @@ -27,6 +26,7 @@ using IUser = Umbraco.Core.Models.Membership.IUser; using Umbraco.Core.Mapping; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Hosting; +using Umbraco.Extensions; using Umbraco.Web.Routing; namespace Umbraco.Web.Editors diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 2a5bebae61..a8bdba8b40 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -24,6 +24,7 @@ using Umbraco.Core.Configuration.Grid; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Hosting; using Umbraco.Core.WebAssets; +using Umbraco.Extensions; using Umbraco.Web.Trees; using Umbraco.Web.WebAssets; using BackOfficeIdentityUser = Umbraco.Core.BackOffice.BackOfficeIdentityUser; diff --git a/src/Umbraco.Web/Editors/CurrentUserController.cs b/src/Umbraco.Web/Editors/CurrentUserController.cs index 2ad09dc895..2bfbcb0c22 100644 --- a/src/Umbraco.Web/Editors/CurrentUserController.cs +++ b/src/Umbraco.Web/Editors/CurrentUserController.cs @@ -25,6 +25,7 @@ using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Hosting; using Umbraco.Web.Routing; using Umbraco.Core.Media; +using Umbraco.Extensions; namespace Umbraco.Web.Editors { diff --git a/src/Umbraco.Web/Editors/PasswordChanger.cs b/src/Umbraco.Web/Editors/PasswordChanger.cs index 256da61478..4b3995ef06 100644 --- a/src/Umbraco.Web/Editors/PasswordChanger.cs +++ b/src/Umbraco.Web/Editors/PasswordChanger.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Extensions; using Umbraco.Web.Models; using Umbraco.Web.Security; using IUser = Umbraco.Core.Models.Membership.IUser; diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 8a3156dd2d..4cf19af968 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -36,7 +36,7 @@ using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Hosting; using Umbraco.Web.Routing; using Umbraco.Core.Media; -using Umbraco.Web.Security; +using Umbraco.Extensions; namespace Umbraco.Web.Editors { diff --git a/src/Umbraco.Web/Security/AuthenticationExtensions.cs b/src/Umbraco.Web/Security/AuthenticationExtensions.cs index ee3b69c389..deb735ca56 100644 --- a/src/Umbraco.Web/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Web/Security/AuthenticationExtensions.cs @@ -352,12 +352,5 @@ namespace Umbraco.Web.Security /// Used so that we aren't creating a new CultureInfo object for every single request /// private static readonly ConcurrentDictionary UserCultures = new ConcurrentDictionary(); - - public static string ToErrorMessage(this IEnumerable errors) - { - if (errors == null) throw new ArgumentNullException(nameof(errors)); - return string.Join(", ", errors.Select(x => x.Description).ToList()); - } - } } From 6dcaa986cda8c8bbca06df8030299d5901d31ceb Mon Sep 17 00:00:00 2001 From: Scott Brady Date: Tue, 19 May 2020 12:18:47 +0100 Subject: [PATCH 13/13] Removed unnecessary TODO and code duplication --- .../Install/InstallSteps/NewInstallStep.cs | 1 - .../Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs index fdc38d8836..a240eaf104 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs @@ -61,7 +61,6 @@ namespace Umbraco.Web.Install.InstallSteps _userService.Save(admin); - //TODO: This needs to be reintroduced, when users are compatible with ASP.NET Core Identity. var membershipUser = await _userManager.FindByIdAsync(Constants.Security.SuperUserId.ToString()); if (membershipUser == null) { diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs index d31b0fb5b0..f3a9a528ae 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs @@ -24,7 +24,6 @@ namespace Umbraco.Extensions services.TryAddScoped(); - services.AddIdentityCore(); services.AddIdentityCore(options => { options.User.RequireUniqueEmail = true;