diff --git a/src/Umbraco.Core/BackOffice/IdentityAuditEventArgs.cs b/src/Umbraco.Core/BackOffice/IdentityAuditEventArgs.cs index a188eda8dd..1d51c45074 100644 --- a/src/Umbraco.Core/BackOffice/IdentityAuditEventArgs.cs +++ b/src/Umbraco.Core/BackOffice/IdentityAuditEventArgs.cs @@ -3,6 +3,7 @@ namespace Umbraco.Core.BackOffice { + /// /// This class is used by events raised from the BackofficeUserManager /// @@ -82,6 +83,7 @@ namespace Umbraco.Core.BackOffice LogoutSuccess, PasswordChanged, PasswordReset, - ResetAccessFailedCount + ResetAccessFailedCount, + SendingUserInvite } } diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs index 84956c7636..090d512e87 100644 --- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs @@ -32,6 +32,9 @@ public string UmbracoPath { get; set; } = "~/umbraco"; + // TODO: Umbraco cannot be hard coded here that is what UmbracoPath is for + // so this should not be a normal get set it has to have dynamic ability to return the correct + // path given UmbracoPath if this hasn't been explicitly set. public string IconsPath { get; set; } = $"~/umbraco/assets/icons"; public string UmbracoCssPath { get; set; } = "~/css"; diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs index 7589d506c7..24b8b20731 100644 --- a/src/Umbraco.Core/Constants-Security.cs +++ b/src/Umbraco.Core/Constants-Security.cs @@ -55,8 +55,6 @@ public const string SessionIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/sessionid"; public const string TicketExpiresClaimType = "http://umbraco.org/2020/06/identity/claims/backoffice/ticketexpires"; - public const string BackOfficeExternalLoginOptionsProperty = "UmbracoBackOfficeExternalLoginOptions"; - /// /// The claim type for the ASP.NET Identity security stamp /// diff --git a/src/Umbraco.Core/IUmbracoContext.cs b/src/Umbraco.Core/IUmbracoContext.cs index e65b7b9d80..312135169a 100644 --- a/src/Umbraco.Core/IUmbracoContext.cs +++ b/src/Umbraco.Core/IUmbracoContext.cs @@ -1,8 +1,8 @@ using System; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Security; using Umbraco.Web.PublishedCache; using Umbraco.Web.Routing; -using Umbraco.Web.Security; namespace Umbraco.Web { diff --git a/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs b/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs index feb8af24f3..cbe5b47b38 100644 --- a/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs +++ b/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs @@ -2,15 +2,6 @@ namespace Umbraco.Core.Models.Identity { - // TODO: Merge these in v8! This is here purely for backward compat - - public interface IIdentityUserLoginExtended : IIdentityUserLogin - { - /// - /// Used to store any arbitrary data for the user and external provider - like user tokens returned from the provider - /// - string UserData { get; set; } - } public interface IIdentityUserLogin : IEntity, IRememberBeingDirty { @@ -28,5 +19,10 @@ namespace Umbraco.Core.Models.Identity /// User Id for the user who owns this login /// int UserId { get; set; } + + /// + /// Used to store any arbitrary data for the user and external provider - like user tokens returned from the provider + /// + string UserData { get; set; } } } diff --git a/src/Umbraco.Core/Models/Identity/IUserLoginInfo.cs b/src/Umbraco.Core/Models/Identity/IUserLoginInfo.cs deleted file mode 100644 index 84dc1da7e0..0000000000 --- a/src/Umbraco.Core/Models/Identity/IUserLoginInfo.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Umbraco.Core.Models.Identity -{ - public interface IUserLoginInfo - { - /// - /// Provider for the linked login, i.e. Facebook, Google, etc. - /// - string LoginProvider { get; set; } - - /// User specific key for the login provider - string ProviderKey { get; set; } - } -} diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs b/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs index 66911b08ac..c13b28461d 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs @@ -7,7 +7,7 @@ namespace Umbraco.Core.Models.Identity /// /// Entity type for a user's login (i.e. Facebook, Google) /// - public class IdentityUserLogin : EntityBase, IIdentityUserLoginExtended + public class IdentityUserLogin : EntityBase, IIdentityUserLogin { public IdentityUserLogin(string loginProvider, string providerKey, int userId) { diff --git a/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs index 590a3003a7..a3455249fe 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs @@ -6,8 +6,6 @@ namespace Umbraco.Core.Persistence.Repositories { public interface IExternalLoginRepository : IReadWriteQueryRepository { - [Obsolete("Use the overload specifying IIdentityUserLoginExtended instead")] - void SaveUserLogins(int memberId, IEnumerable logins); void Save(int userId, IEnumerable logins); void DeleteUserLogins(int memberId); } diff --git a/src/Umbraco.Web/Security/BackOfficeExternalLoginProviderErrors.cs b/src/Umbraco.Core/Security/BackOfficeExternalLoginProviderErrors.cs similarity index 94% rename from src/Umbraco.Web/Security/BackOfficeExternalLoginProviderErrors.cs rename to src/Umbraco.Core/Security/BackOfficeExternalLoginProviderErrors.cs index 39b967fa96..d7a2fed46a 100644 --- a/src/Umbraco.Web/Security/BackOfficeExternalLoginProviderErrors.cs +++ b/src/Umbraco.Core/Security/BackOfficeExternalLoginProviderErrors.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; -namespace Umbraco.Web.Security +namespace Umbraco.Core.Security { public class BackOfficeExternalLoginProviderErrors { diff --git a/src/Umbraco.Core/Security/IBackofficeSecurity.cs b/src/Umbraco.Core/Security/IBackofficeSecurity.cs index 187d5d172d..4ba20f7bfa 100644 --- a/src/Umbraco.Core/Security/IBackofficeSecurity.cs +++ b/src/Umbraco.Core/Security/IBackofficeSecurity.cs @@ -2,7 +2,7 @@ using System; using Umbraco.Core; using Umbraco.Core.Models.Membership; -namespace Umbraco.Web.Security +namespace Umbraco.Core.Security { public interface IBackOfficeSecurity { diff --git a/src/Umbraco.Core/Security/IBackofficeSecurityAccessor.cs b/src/Umbraco.Core/Security/IBackofficeSecurityAccessor.cs index 03c1035cb9..1695ecf46e 100644 --- a/src/Umbraco.Core/Security/IBackofficeSecurityAccessor.cs +++ b/src/Umbraco.Core/Security/IBackofficeSecurityAccessor.cs @@ -1,6 +1,4 @@ -using Umbraco.Web.Security; - -namespace Umbraco.Core.Security +namespace Umbraco.Core.Security { public interface IBackOfficeSecurityAccessor { diff --git a/src/Umbraco.Core/Security/ValidateRequestAttempt.cs b/src/Umbraco.Core/Security/ValidateRequestAttempt.cs index 8fab0b2533..a88e18d463 100644 --- a/src/Umbraco.Core/Security/ValidateRequestAttempt.cs +++ b/src/Umbraco.Core/Security/ValidateRequestAttempt.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Web.Security +namespace Umbraco.Core.Security { public enum ValidateRequestAttempt { diff --git a/src/Umbraco.Core/Services/IExternalLoginService.cs b/src/Umbraco.Core/Services/IExternalLoginService.cs index 5f0a69eb7e..d0c2a74192 100644 --- a/src/Umbraco.Core/Services/IExternalLoginService.cs +++ b/src/Umbraco.Core/Services/IExternalLoginService.cs @@ -16,9 +16,6 @@ namespace Umbraco.Core.Services /// IEnumerable GetAll(int userId); - [Obsolete("Use the overload specifying loginProvider and providerKey instead")] - IEnumerable Find(IUserLoginInfo login); - /// /// Returns all logins matching the login info - generally there should only be one but in some cases /// there might be more than one depending on if an administrator has been editing/removing members @@ -28,9 +25,6 @@ namespace Umbraco.Core.Services /// IEnumerable Find(string loginProvider, string providerKey); - [Obsolete("Use the Save method instead")] - void SaveUserLogins(int userId, IEnumerable logins); - /// /// Saves the external logins associated with the user /// @@ -47,7 +41,7 @@ namespace Umbraco.Core.Services /// Save a single external login record /// /// - void Save(IIdentityUserLoginExtended login); + void Save(IIdentityUserLogin login); /// /// Deletes all user logins - normally used when a member is deleted diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs index 3c604a77c8..1e5cd8436e 100644 --- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs +++ b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs @@ -8,9 +8,11 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; using Umbraco.Extensions; using Umbraco.Net; +using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Core.BackOffice { @@ -362,10 +364,15 @@ namespace Umbraco.Core.BackOffice return result; } - private IdentityAuditEventArgs CreateArgs(AuditEvent auditEvent, IPrincipal currentUser, int affectedUserId, string affectedUsername) + private int GetCurrentUserId(IPrincipal currentUser) { var umbIdentity = currentUser?.GetUmbracoIdentity(); var currentUserId = umbIdentity?.GetUserId() ?? Constants.Security.SuperUserId; + return currentUserId; + } + private IdentityAuditEventArgs CreateArgs(AuditEvent auditEvent, IPrincipal currentUser, int affectedUserId, string affectedUsername) + { + var currentUserId = GetCurrentUserId(currentUser); var ip = IpResolver.GetCurrentRequestIpAddress(); return new IdentityAuditEventArgs(auditEvent, ip, currentUserId, string.Empty, affectedUserId, affectedUsername); } @@ -391,44 +398,53 @@ namespace Umbraco.Core.BackOffice public void RaiseInvalidLoginAttemptEvent(IPrincipal currentUser, string username) => OnLoginFailed(CreateArgs(AuditEvent.LoginFailed, currentUser, Constants.Security.SuperUserId, username)); public void RaiseLoginRequiresVerificationEvent(IPrincipal currentUser, int userId) => OnLoginRequiresVerification(CreateArgs(AuditEvent.LoginRequiresVerification, currentUser, userId, string.Empty)); - internal SignOutAuditEventArgs RaiseLogoutSuccessEvent(int userId) - { - var args = new SignOutAuditEventArgs(AuditEvent.LogoutSuccess, IpResolver.GetCurrentRequestIpAddress(), affectedUser: userId); - OnLogoutSuccess(args); - return args; - } public void RaiseLoginSuccessEvent(BackOfficeIdentityUser currentUser, int userId) => OnLoginSuccess(CreateArgs(AuditEvent.LoginSucces, currentUser, userId, string.Empty)); - public void RaiseLogoutSuccessEvent(IPrincipal currentUser, int userId) => OnLogoutSuccess(CreateArgs(AuditEvent.LogoutSuccess, currentUser, userId, string.Empty)); - + public SignOutAuditEventArgs RaiseLogoutSuccessEvent(IPrincipal currentUser, int userId) + { + var currentUserId = GetCurrentUserId(currentUser); + var args = new SignOutAuditEventArgs(AuditEvent.LogoutSuccess, IpResolver.GetCurrentRequestIpAddress(), performingUser: currentUserId, affectedUser: userId); + OnLogoutSuccess(args); + return args; + } + public void RaisePasswordChangedEvent(IPrincipal currentUser, int userId) => OnPasswordChanged(CreateArgs(AuditEvent.LogoutSuccess, currentUser, userId, string.Empty)); public void RaiseResetAccessFailedCountEvent(IPrincipal currentUser, int userId) => OnResetAccessFailedCount(CreateArgs(AuditEvent.ResetAccessFailedCount, currentUser, userId, string.Empty)); - internal void RaiseSendingUserInvite(UserInviteEventArgs args) => OnSendingUserInvite(args); - internal bool HasSendingUserInviteEventHandler => SendingUserInvite != null; + public UserInviteEventArgs RaiseSendingUserInvite(IPrincipal currentUser, UserInvite invite, IUser createdUser) + { + var currentUserId = GetCurrentUserId(currentUser); + var ip = IpResolver.GetCurrentRequestIpAddress(); + var args = new UserInviteEventArgs(ip, currentUserId, invite, createdUser); + OnSendingUserInvite(args); + return args; + } - // TODO: Not sure why these are not strongly typed events?? They should be in netcore! - 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; + public bool HasSendingUserInviteEventHandler => SendingUserInvite != null; + + 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; /// /// Raised when a user is invited /// - public static event EventHandler SendingUserInvite; // this event really has nothing to do with the user manager but was the most convenient place to put it + public static event EventHandler SendingUserInvite; // this event really has nothing to do with the user manager but was the most convenient place to put it + protected virtual void OnAccountLocked(IdentityAuditEventArgs e) => AccountLocked?.Invoke(this, e); protected virtual void OnSendingUserInvite(UserInviteEventArgs e) => SendingUserInvite?.Invoke(this, e); + protected virtual void OnAccountUnlocked(IdentityAuditEventArgs e) => AccountUnlocked?.Invoke(this, e); protected virtual void OnForgotPasswordRequested(IdentityAuditEventArgs e) => ForgotPasswordRequested?.Invoke(this, e); @@ -441,7 +457,7 @@ namespace Umbraco.Core.BackOffice protected virtual void OnLoginSuccess(IdentityAuditEventArgs e) => LoginSuccess?.Invoke(this, e); - protected virtual void OnLogoutSuccess(IdentityAuditEventArgs e) => LogoutSuccess?.Invoke(this, e); + protected virtual void OnLogoutSuccess(SignOutAuditEventArgs e) => LogoutSuccess?.Invoke(this, e); protected virtual void OnPasswordChanged(IdentityAuditEventArgs e) => PasswordChanged?.Invoke(this, e); diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs index b24ec73332..7ac3701c5c 100644 --- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs @@ -144,7 +144,7 @@ namespace Umbraco.Core.BackOffice user.Logins.Select(x => new ExternalLogin( x.LoginProvider, x.ProviderKey, - (x is IIdentityUserLoginExtended extended) ? extended.UserData : null))); + x.UserData))); } return Task.FromResult(IdentityResult.Success); @@ -188,7 +188,7 @@ namespace Umbraco.Core.BackOffice user.Logins.Select(x => new ExternalLogin( x.LoginProvider, x.ProviderKey, - (x is IIdentityUserLoginExtended extended) ? extended.UserData : null))); + x.UserData))); } } diff --git a/src/Umbraco.Infrastructure/BackOffice/IBackOfficeUserManager.cs b/src/Umbraco.Infrastructure/BackOffice/IBackOfficeUserManager.cs index cc169f31f9..ca22567418 100644 --- a/src/Umbraco.Infrastructure/BackOffice/IBackOfficeUserManager.cs +++ b/src/Umbraco.Infrastructure/BackOffice/IBackOfficeUserManager.cs @@ -3,7 +3,8 @@ using System.Collections.Generic; using System.Security.Principal; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; - +using Umbraco.Core.Models.Membership; +using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Core.BackOffice { @@ -14,6 +15,8 @@ namespace Umbraco.Core.BackOffice public interface IBackOfficeUserManager: IDisposable where TUser : BackOfficeIdentityUser { + Task> GetLoginsAsync(TUser user); + Task DeleteAsync(TUser user); Task FindByLoginAsync(string loginProvider, string providerKey); @@ -303,8 +306,11 @@ namespace Umbraco.Core.BackOffice void RaiseForgotPasswordRequestedEvent(IPrincipal currentUser, int userId); void RaiseForgotPasswordChangedSuccessEvent(IPrincipal currentUser, int userId); - void RaiseLogoutSuccessEvent(IPrincipal currentUser, int userId); + SignOutAuditEventArgs RaiseLogoutSuccessEvent(IPrincipal currentUser, int userId); + UserInviteEventArgs RaiseSendingUserInvite(IPrincipal currentUser, UserInvite invite, IUser createdUser); void RaiseLoginSuccessEvent(TUser currentUser, int userId); + + bool HasSendingUserInviteEventHandler { get; } } } diff --git a/src/Umbraco.Infrastructure/BackOffice/UserLoginInfoWrapper.cs b/src/Umbraco.Infrastructure/BackOffice/UserLoginInfoWrapper.cs deleted file mode 100644 index ab6af35519..0000000000 --- a/src/Umbraco.Infrastructure/BackOffice/UserLoginInfoWrapper.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using Umbraco.Core.Models.Identity; - -namespace Umbraco.Core.BackOffice -{ - 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.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs index 6c1af68acd..74d2fe7ff0 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs @@ -6,7 +6,7 @@ namespace Umbraco.Core.Persistence.Factories { internal static class ExternalLoginFactory { - public static IIdentityUserLoginExtended BuildEntity(ExternalLoginDto dto) + public static IIdentityUserLogin BuildEntity(ExternalLoginDto dto) { var entity = new IdentityUserLogin(dto.Id, dto.LoginProvider, dto.ProviderKey, dto.UserId, dto.CreateDate) { @@ -20,7 +20,6 @@ namespace Umbraco.Core.Persistence.Factories public static ExternalLoginDto BuildDto(IIdentityUserLogin entity) { - var asExtended = entity as IIdentityUserLoginExtended; var dto = new ExternalLoginDto { Id = entity.Id, @@ -28,7 +27,7 @@ namespace Umbraco.Core.Persistence.Factories LoginProvider = entity.LoginProvider, ProviderKey = entity.ProviderKey, UserId = entity.UserId, - UserData = asExtended?.UserData + UserData = entity.UserData }; return dto; diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs index 017dbfd60b..33fd3af7fc 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs @@ -77,11 +77,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.InsertBulk(toInsert.Select(i => ExternalLoginFactory.BuildDto(userId, i))); } - public void SaveUserLogins(int memberId, IEnumerable logins) - { - Save(memberId, logins.Select(x => new ExternalLogin(x.LoginProvider, x.ProviderKey))); - } - protected override IIdentityUserLogin PerformGet(int id) { var sql = GetBaseQuery(false); diff --git a/src/Umbraco.Web/Security/SignOutAuditEventArgs.cs b/src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs similarity index 83% rename from src/Umbraco.Web/Security/SignOutAuditEventArgs.cs rename to src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs index e7943f70b6..961c2e6137 100644 --- a/src/Umbraco.Web/Security/SignOutAuditEventArgs.cs +++ b/src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs @@ -1,12 +1,13 @@ -namespace Umbraco.Web.Security +namespace Umbraco.Core.BackOffice { + /// /// Event args used when signing out /// public class SignOutAuditEventArgs : IdentityAuditEventArgs { public SignOutAuditEventArgs(AuditEvent action, string ipAddress, string comment = null, int performingUser = -1, int affectedUser = -1) - : base(action, ipAddress, comment, performingUser, affectedUser) + : base(action, ipAddress, performingUser, comment, affectedUser, null) { } diff --git a/src/Umbraco.Web/Security/UserInviteEventArgs.cs b/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs similarity index 88% rename from src/Umbraco.Web/Security/UserInviteEventArgs.cs rename to src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs index 9fb53a44c0..4e980b7bb1 100644 --- a/src/Umbraco.Web/Security/UserInviteEventArgs.cs +++ b/src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs @@ -1,12 +1,12 @@ using Umbraco.Core.Models.Membership; using Umbraco.Web.Models.ContentEditing; -namespace Umbraco.Web.Security +namespace Umbraco.Core.BackOffice { public class UserInviteEventArgs : IdentityAuditEventArgs { public UserInviteEventArgs(string ipAddress, int performingUser, UserInvite invitedUser, IUser localUser, string comment = null) - : base(AuditEvent.SendingUserInvite, ipAddress, comment, performingUser) + : base(AuditEvent.SendingUserInvite, ipAddress, performingUser, comment, localUser.Id, localUser.Name) { InvitedUser = invitedUser ?? throw new System.ArgumentNullException(nameof(invitedUser)); User = localUser; diff --git a/src/Umbraco.Infrastructure/Services/Implement/ExternalLoginService.cs b/src/Umbraco.Infrastructure/Services/Implement/ExternalLoginService.cs index b6254e5049..fabbfea1d4 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ExternalLoginService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/ExternalLoginService.cs @@ -30,12 +30,6 @@ namespace Umbraco.Core.Services.Implement } } - [Obsolete("Use the overload specifying loginProvider and providerKey instead")] - public IEnumerable Find(IUserLoginInfo login) - { - return Find(login.LoginProvider, login.ProviderKey); - } - /// public IEnumerable Find(string loginProvider, string providerKey) { @@ -47,12 +41,6 @@ namespace Umbraco.Core.Services.Implement } } - [Obsolete("Use the Save method instead")] - public void SaveUserLogins(int userId, IEnumerable logins) - { - Save(userId, logins.Select(x => new ExternalLogin(x.LoginProvider, x.ProviderKey))); - } - /// public void Save(int userId, IEnumerable logins) { @@ -64,7 +52,7 @@ namespace Umbraco.Core.Services.Implement } /// - public void Save(IIdentityUserLoginExtended login) + public void Save(IIdentityUserLogin login) { using (var scope = ScopeProvider.CreateScope()) { diff --git a/src/Umbraco.Tests/Services/ExternalLoginServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ExternalLoginServiceTests.cs similarity index 70% rename from src/Umbraco.Tests/Services/ExternalLoginServiceTests.cs rename to src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ExternalLoginServiceTests.cs index c9ad924099..429e1953f7 100644 --- a/src/Umbraco.Tests/Services/ExternalLoginServiceTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ExternalLoginServiceTests.cs @@ -1,13 +1,12 @@ using System; using System.Linq; using System.Threading; -using Microsoft.AspNet.Identity; using NUnit.Framework; -using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.Legacy; using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.Services; +using Umbraco.Tests.Integration.Testing; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing; @@ -16,15 +15,16 @@ namespace Umbraco.Tests.Services [TestFixture] [Apartment(ApartmentState.STA)] [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)] - public class ExternalLoginServiceTests : TestWithDatabaseBase + public class ExternalLoginServiceTests : UmbracoIntegrationTest { - private IGlobalSettings GlobalSettings => SettingsForTests.DefaultGlobalSettings; + private IUserService UserService => GetRequiredService(); + private IExternalLoginService ExternalLoginService => GetRequiredService(); [Test] public void Removes_Existing_Duplicates_On_Save() { var user = new User(GlobalSettings, "Test", "test@test.com", "test", "helloworldtest"); - ServiceContext.UserService.Save(user); + UserService.Save(user); var providerKey = Guid.NewGuid().ToString("N"); var latest = DateTime.Now.AddDays(-1); @@ -57,9 +57,9 @@ namespace Umbraco.Tests.Services new ExternalLogin("test1", providerKey) }; - ServiceContext.ExternalLoginService.Save(user.Id, externalLogins); + ExternalLoginService.Save(user.Id, externalLogins); - var logins = ServiceContext.ExternalLoginService.GetAll(user.Id).ToList(); + var logins = ExternalLoginService.GetAll(user.Id).ToList(); // duplicates will be removed, keeping the latest entries Assert.AreEqual(2, logins.Count); @@ -72,7 +72,7 @@ namespace Umbraco.Tests.Services public void Does_Not_Persist_Duplicates() { var user = new User(GlobalSettings, "Test", "test@test.com", "test", "helloworldtest"); - ServiceContext.UserService.Save(user); + UserService.Save(user); var providerKey = Guid.NewGuid().ToString("N"); var externalLogins = new[] @@ -81,9 +81,9 @@ namespace Umbraco.Tests.Services new ExternalLogin("test1", providerKey) }; - ServiceContext.ExternalLoginService.Save(user.Id, externalLogins); + ExternalLoginService.Save(user.Id, externalLogins); - var logins = ServiceContext.ExternalLoginService.GetAll(user.Id).ToList(); + var logins = ExternalLoginService.GetAll(user.Id).ToList(); Assert.AreEqual(1, logins.Count); } @@ -91,15 +91,15 @@ namespace Umbraco.Tests.Services public void Single_Create() { var user = new User(GlobalSettings, "Test", "test@test.com", "test", "helloworldtest"); - ServiceContext.UserService.Save(user); + UserService.Save(user); var extLogin = new IdentityUserLogin("test1", Guid.NewGuid().ToString("N"), user.Id) { UserData = "hello" }; - ServiceContext.ExternalLoginService.Save(extLogin); + ExternalLoginService.Save(extLogin); - var found = ServiceContext.ExternalLoginService.GetAll(user.Id); + var found = ExternalLoginService.GetAll(user.Id); Assert.AreEqual(1, found.Count()); Assert.IsTrue(extLogin.HasIdentity); @@ -110,18 +110,18 @@ namespace Umbraco.Tests.Services public void Single_Update() { var user = new User(GlobalSettings, "Test", "test@test.com", "test", "helloworldtest"); - ServiceContext.UserService.Save(user); + UserService.Save(user); var extLogin = new IdentityUserLogin("test1", Guid.NewGuid().ToString("N"), user.Id) { UserData = "hello" }; - ServiceContext.ExternalLoginService.Save(extLogin); + ExternalLoginService.Save(extLogin); extLogin.UserData = "world"; - ServiceContext.ExternalLoginService.Save(extLogin); + ExternalLoginService.Save(extLogin); - var found = ServiceContext.ExternalLoginService.GetAll(user.Id).Cast().ToList(); + var found = ExternalLoginService.GetAll(user.Id).ToList(); Assert.AreEqual(1, found.Count); Assert.AreEqual("world", found[0].UserData); } @@ -130,7 +130,7 @@ namespace Umbraco.Tests.Services public void Multiple_Update() { var user = new User(GlobalSettings, "Test", "test@test.com", "test", "helloworldtest"); - ServiceContext.UserService.Save(user); + UserService.Save(user); var providerKey1 = Guid.NewGuid().ToString("N"); var providerKey2 = Guid.NewGuid().ToString("N"); @@ -139,16 +139,16 @@ namespace Umbraco.Tests.Services new ExternalLogin("test1", providerKey1, "hello"), new ExternalLogin("test2", providerKey2, "world") }; - ServiceContext.ExternalLoginService.Save(user.Id, extLogins); + ExternalLoginService.Save(user.Id, extLogins); extLogins = new[] { new ExternalLogin("test1", providerKey1, "123456"), new ExternalLogin("test2", providerKey2, "987654") }; - ServiceContext.ExternalLoginService.Save(user.Id, extLogins); + ExternalLoginService.Save(user.Id, extLogins); - var found = ServiceContext.ExternalLoginService.GetAll(user.Id).Cast().OrderBy(x => x.LoginProvider).ToList(); + var found = ExternalLoginService.GetAll(user.Id).OrderBy(x => x.LoginProvider).ToList(); Assert.AreEqual(2, found.Count); Assert.AreEqual("123456", found[0].UserData); Assert.AreEqual("987654", found[1].UserData); @@ -158,7 +158,7 @@ namespace Umbraco.Tests.Services public void Can_Find_As_Extended_Type() { var user = new User(GlobalSettings, "Test", "test@test.com", "test", "helloworldtest"); - ServiceContext.UserService.Save(user); + UserService.Save(user); var providerKey1 = Guid.NewGuid().ToString("N"); var providerKey2 = Guid.NewGuid().ToString("N"); @@ -167,11 +167,11 @@ namespace Umbraco.Tests.Services new ExternalLogin("test1", providerKey1, "hello"), new ExternalLogin("test2", providerKey2, "world") }; - ServiceContext.ExternalLoginService.Save(user.Id, extLogins); + ExternalLoginService.Save(user.Id, extLogins); - var found = ServiceContext.ExternalLoginService.Find("test2", providerKey2).ToList(); + var found = ExternalLoginService.Find("test2", providerKey2).ToList(); Assert.AreEqual(1, found.Count); - var asExtended = found.Cast().ToList(); + var asExtended = found.ToList(); Assert.AreEqual(1, found.Count); } @@ -180,7 +180,7 @@ namespace Umbraco.Tests.Services public void Add_Logins() { var user = new User(GlobalSettings, "Test", "test@test.com", "test", "helloworldtest"); - ServiceContext.UserService.Save(user); + UserService.Save(user); var externalLogins = new[] { @@ -188,9 +188,9 @@ namespace Umbraco.Tests.Services new ExternalLogin("test2", Guid.NewGuid().ToString("N")) }; - ServiceContext.ExternalLoginService.Save(user.Id, externalLogins); + ExternalLoginService.Save(user.Id, externalLogins); - var logins = ServiceContext.ExternalLoginService.GetAll(user.Id).OrderBy(x => x.LoginProvider).ToList(); + var logins = ExternalLoginService.GetAll(user.Id).OrderBy(x => x.LoginProvider).ToList(); Assert.AreEqual(2, logins.Count); for (int i = 0; i < logins.Count; i++) { @@ -203,7 +203,7 @@ namespace Umbraco.Tests.Services public void Add_Update_Delete_Logins() { var user = new User(GlobalSettings, "Test", "test@test.com", "test", "helloworldtest"); - ServiceContext.UserService.Save(user); + UserService.Save(user); var externalLogins = new[] { @@ -213,17 +213,17 @@ namespace Umbraco.Tests.Services new ExternalLogin("test4", Guid.NewGuid().ToString("N")) }; - ServiceContext.ExternalLoginService.Save(user.Id, externalLogins); + ExternalLoginService.Save(user.Id, externalLogins); - var logins = ServiceContext.ExternalLoginService.GetAll(user.Id).OrderBy(x => x.LoginProvider).ToList(); + var logins = ExternalLoginService.GetAll(user.Id).OrderBy(x => x.LoginProvider).ToList(); logins.RemoveAt(0); // remove the first one logins.Add(new IdentityUserLogin("test5", Guid.NewGuid().ToString("N"), user.Id)); // add a new one // save new list - ServiceContext.ExternalLoginService.Save(user.Id, logins.Select(x => new ExternalLogin(x.LoginProvider, x.ProviderKey))); + ExternalLoginService.Save(user.Id, logins.Select(x => new ExternalLogin(x.LoginProvider, x.ProviderKey))); - var updatedLogins = ServiceContext.ExternalLoginService.GetAll(user.Id).OrderBy(x => x.LoginProvider).ToList(); + var updatedLogins = ExternalLoginService.GetAll(user.Id).OrderBy(x => x.LoginProvider).ToList(); Assert.AreEqual(4, updatedLogins.Count); for (int i = 0; i < updatedLogins.Count; i++) { @@ -235,16 +235,16 @@ namespace Umbraco.Tests.Services public void Add_Retrieve_User_Data() { var user = new User(GlobalSettings, "Test", "test@test.com", "test", "helloworldtest"); - ServiceContext.UserService.Save(user); + UserService.Save(user); var externalLogins = new[] { new ExternalLogin("test1", Guid.NewGuid().ToString("N"), "hello world") }; - ServiceContext.ExternalLoginService.Save(user.Id, externalLogins); + ExternalLoginService.Save(user.Id, externalLogins); - var logins = ServiceContext.ExternalLoginService.GetAll(user.Id).Cast().ToList(); + var logins = ExternalLoginService.GetAll(user.Id).ToList(); Assert.AreEqual("hello world", logins[0].UserData); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Filters/AppendUserModifiedHeaderAttributeTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Filters/AppendUserModifiedHeaderAttributeTests.cs index 8222347664..e18f5cb3f4 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Filters/AppendUserModifiedHeaderAttributeTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Filters/AppendUserModifiedHeaderAttributeTests.cs @@ -8,9 +8,8 @@ using Microsoft.AspNetCore.Routing; using Moq; using NUnit.Framework; using Umbraco.Core.Models.Membership; -using Umbraco.Web; +using Umbraco.Core.Security; using Umbraco.Web.BackOffice.Filters; -using Umbraco.Web.Security; namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Filters { diff --git a/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs b/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs index 2f93fa2739..c5333ea524 100644 --- a/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs +++ b/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs @@ -4,20 +4,15 @@ using Moq; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; -using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Tests.Common; -using Umbraco.Tests.Common.Builders; using Umbraco.Tests.LegacyXmlPublishedCache; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing; -using Umbraco.Tests.Testing.Objects.Accessors; using Umbraco.Web; using Umbraco.Web.PublishedCache; -using Umbraco.Web.Routing; -using Umbraco.Web.Security; namespace Umbraco.Tests.Cache.PublishedCache { diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs index e1b96b9a73..52378535a0 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs @@ -9,8 +9,6 @@ using Umbraco.Core; using Umbraco.Core.Models.PublishedContent; using Umbraco.Web; using Umbraco.Web.PublishedCache; -using Umbraco.Web.Routing; -using Umbraco.Web.Security; using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Hosting; @@ -21,9 +19,9 @@ using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Tests.TestHelpers; -using Umbraco.Tests.Testing.Objects.Accessors; using Current = Umbraco.Web.Composing.Current; using Umbraco.Tests.Common; +using Umbraco.Core.Security; namespace Umbraco.Tests.PublishedContent { diff --git a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs index cf0285f907..ad476d49ca 100644 --- a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Events; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; using Umbraco.Core.Strings; @@ -23,7 +24,6 @@ using Umbraco.Web.Cache; using Umbraco.Web.PublishedCache; using Umbraco.Web.PublishedCache.NuCache; using Umbraco.Web.PublishedCache.NuCache.DataSource; -using Umbraco.Web.Security; using Current = Umbraco.Web.Composing.Current; namespace Umbraco.Tests.Scoping diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs index 8eb98ac655..664b00b513 100644 --- a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs @@ -14,10 +14,10 @@ using Umbraco.Core.Services; using Umbraco.Web; using Umbraco.Web.PublishedCache; using Umbraco.Web.Routing; -using Umbraco.Web.Security; using Umbraco.Web.WebApi; using Umbraco.Tests.Common; using Umbraco.Tests.TestHelpers.Entities; +using Umbraco.Core.Security; namespace Umbraco.Tests.TestHelpers.ControllerTesting { diff --git a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs index fd832f94fa..51d323d0ff 100644 --- a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs +++ b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs @@ -5,7 +5,6 @@ using System.Threading; using System.Web.Routing; using System.Xml; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using Moq; using NUnit.Framework; using Umbraco.Core; @@ -18,7 +17,6 @@ using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Web; using Umbraco.Web.PublishedCache; -using Umbraco.Web.Security; using Umbraco.Web.Routing; using File = System.IO.File; using Umbraco.Web.Composing; @@ -32,7 +30,7 @@ using Umbraco.Persistance.SqlCe; using Umbraco.Tests.LegacyXmlPublishedCache; using Umbraco.Web.WebApi; using Umbraco.Tests.Common; -using Umbraco.Tests.Common.Builders; +using Umbraco.Core.Security; namespace Umbraco.Tests.TestHelpers { diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 59b390c37d..84404ddae9 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -167,19 +167,6 @@ - - - - - - - - - - - - - @@ -469,4 +456,4 @@ - + \ No newline at end of file diff --git a/src/Umbraco.Tests/Web/WebExtensionMethodTests.cs b/src/Umbraco.Tests/Web/WebExtensionMethodTests.cs index a588e09ea6..4e52617e6c 100644 --- a/src/Umbraco.Tests/Web/WebExtensionMethodTests.cs +++ b/src/Umbraco.Tests/Web/WebExtensionMethodTests.cs @@ -1,21 +1,16 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; +using System.IO; using System.Web; using System.Web.Mvc; using System.Web.Routing; using Moq; using NUnit.Framework; -using Umbraco.Core.Services; +using Umbraco.Core.Security; using Umbraco.Tests.Common; using Umbraco.Tests.TestHelpers; -using Umbraco.Tests.TestHelpers.Stubs; using Umbraco.Tests.Testing; using Umbraco.Web; using Umbraco.Web.Mvc; using Umbraco.Web.PublishedCache; -using Umbraco.Web.Routing; -using Umbraco.Web.Security; using Current = Umbraco.Web.Composing.Current; namespace Umbraco.Tests.Web diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index 43dfad102d..151e6b8b42 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -31,6 +31,7 @@ using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Security; using Constants = Umbraco.Core.Constants; using Microsoft.AspNetCore.Identity; +using Umbraco.Web.Editors.Filters; namespace Umbraco.Web.BackOffice.Controllers { @@ -63,6 +64,7 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly Core.Hosting.IHostingEnvironment _hostingEnvironment; private readonly IRequestAccessor _requestAccessor; private readonly LinkGenerator _linkGenerator; + private readonly IBackOfficeExternalLoginProviders _externalAuthenticationOptions; // TODO: We need to import the logic from Umbraco.Web.Editors.AuthenticationController // TODO: We need to review all _userManager.Raise calls since many/most should be on the usermanager or signinmanager, very few should be here @@ -83,7 +85,8 @@ namespace Umbraco.Web.BackOffice.Controllers ISmsSender smsSender, Core.Hosting.IHostingEnvironment hostingEnvironment, IRequestAccessor requestAccessor, - LinkGenerator linkGenerator) + LinkGenerator linkGenerator, + IBackOfficeExternalLoginProviders externalAuthenticationOptions) { _backofficeSecurityAccessor = backofficeSecurityAccessor; _userManager = backOfficeUserManager; @@ -101,6 +104,7 @@ namespace Umbraco.Web.BackOffice.Controllers _hostingEnvironment = hostingEnvironment; _requestAccessor = requestAccessor; _linkGenerator = linkGenerator; + _externalAuthenticationOptions = externalAuthenticationOptions; } /// @@ -123,6 +127,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// This will also update the security stamp for the user so it can only be used once /// [ValidateAngularAntiForgeryToken] + [DenyLocalLoginAuthorization] public async Task> PostVerifyInvite([FromQuery] int id, [FromQuery] string token) { if (string.IsNullOrWhiteSpace(token)) @@ -154,11 +159,29 @@ namespace Umbraco.Web.BackOffice.Controllers [UmbracoAuthorize] [ValidateAngularAntiForgeryToken] - public async Task PostUnLinkLogin(UnLinkLoginModel unlinkLoginModel) + public async Task PostUnLinkLogin(UnLinkLoginModel unlinkLoginModel) { var user = await _userManager.FindByIdAsync(User.Identity.GetUserId()); if (user == null) throw new InvalidOperationException("Could not find user"); + ExternalSignInAutoLinkOptions autoLinkOptions = null; + var authType = (await _signInManager.GetExternalAuthenticationSchemesAsync()) + .FirstOrDefault(x => x.Name == unlinkLoginModel.LoginProvider); + + if (authType == null) + { + _logger.LogWarning("Could not find external authentication provider registered: {LoginProvider}", unlinkLoginModel.LoginProvider); + } + else + { + autoLinkOptions = _externalAuthenticationOptions.Get(authType.Name); + if (!autoLinkOptions.AllowManualLinking) + { + // If AllowManualLinking is disabled for this provider we cannot unlink + return BadRequest(); + } + } + var result = await _userManager.RemoveLoginAsync( user, unlinkLoginModel.LoginProvider, @@ -243,6 +266,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// [UmbracoAuthorize(redirectToUmbracoLogin: false, requireApproval: false)] [SetAngularAntiForgeryTokens] + [DenyLocalLoginAuthorization] public ActionResult GetCurrentInvitedUser() { var user = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser; @@ -266,6 +290,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// [SetAngularAntiForgeryTokens] + [DenyLocalLoginAuthorization] public async Task PostLogin(LoginModel loginModel) { // Sign the user in with username/password, this also gives a chance for developers to @@ -332,6 +357,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// [SetAngularAntiForgeryTokens] + [DenyLocalLoginAuthorization] public async Task PostRequestPasswordReset(RequestPasswordResetModel model) { // If this feature is switched off in configuration the UI will be amended to not make the request to reset password available. @@ -546,11 +572,19 @@ namespace Umbraco.Web.BackOffice.Controllers [ValidateAngularAntiForgeryToken] public IActionResult PostLogout() { - HttpContext.SignOutAsync(Core.Constants.Security.BackOfficeAuthenticationType); + HttpContext.SignOutAsync(Constants.Security.BackOfficeAuthenticationType); _logger.LogInformation("User {UserName} from IP address {RemoteIpAddress} has logged out", User.Identity == null ? "UNKNOWN" : User.Identity.Name, HttpContext.Connection.RemoteIpAddress); - _userManager.RaiseLogoutSuccessEvent(User, int.Parse(User.Identity.GetUserId())); + var userId = int.Parse(User.Identity.GetUserId()); + var args = _userManager.RaiseLogoutSuccessEvent(User, userId); + if (!args.SignOutRedirectUrl.IsNullOrWhiteSpace()) + { + return new ObjectResult(new + { + signOutRedirectUrl = args.SignOutRedirectUrl + }); + } return Ok(); } diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs index 46c5625ee1..749c1c6f07 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs @@ -31,6 +31,7 @@ using Umbraco.Web.WebAssets; using Constants = Umbraco.Core.Constants; using Microsoft.AspNetCore.Identity; using System.Security.Claims; +using Umbraco.Web.Security; namespace Umbraco.Web.BackOffice.Controllers { @@ -50,7 +51,7 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; private readonly ILogger _logger; private readonly IJsonSerializer _jsonSerializer; - private readonly IExternalAuthenticationOptions _externalAuthenticationOptions; + private readonly IBackOfficeExternalLoginProviders _externalLogins; public BackOfficeController( IBackOfficeUserManager userManager, @@ -65,7 +66,7 @@ namespace Umbraco.Web.BackOffice.Controllers IBackOfficeSecurityAccessor backofficeSecurityAccessor, ILogger logger, IJsonSerializer jsonSerializer, - IExternalAuthenticationOptions externalAuthenticationOptions) + IBackOfficeExternalLoginProviders externalLogins) { _userManager = userManager; _runtimeMinifier = runtimeMinifier; @@ -79,7 +80,7 @@ namespace Umbraco.Web.BackOffice.Controllers _backofficeSecurityAccessor = backofficeSecurityAccessor; _logger = logger; _jsonSerializer = jsonSerializer; - _externalAuthenticationOptions = externalAuthenticationOptions; + _externalLogins = externalLogins; } [HttpGet] @@ -260,6 +261,9 @@ namespace Umbraco.Web.BackOffice.Controllers } var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); + // TODO: I believe we will have to fill in our own XsrfKey like we use to do since I think + // we validate against that key? + // see https://github.com/umbraco/Umbraco-CMS/blob/v8/contrib/src/Umbraco.Web/Editors/ChallengeResult.cs#L48 return Challenge(properties, provider); } @@ -275,6 +279,9 @@ namespace Umbraco.Web.BackOffice.Controllers // Request a redirect to the external login provider to link a login for the current user var redirectUrl = Url.Action(nameof(ExternalLinkLoginCallback), this.GetControllerName()); var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, User.Identity.GetUserId()); + // TODO: I believe we will have to fill in our own XsrfKey like we use to do since I think + // we validate against that key? + // see https://github.com/umbraco/Umbraco-CMS/blob/v8/contrib/src/Umbraco.Web/Editors/ChallengeResult.cs#L48 return Challenge(properties, provider); } @@ -305,6 +312,11 @@ namespace Umbraco.Web.BackOffice.Controllers [HttpGet] public async Task ExternalLinkLoginCallback() { + // TODO: Do we need/want to tell it an expected xsrf. + // In v8 the xsrf used to be set to the user id which was verified manually, in this case I think we don't specify + // the key and that is up to the underlying sign in manager to set so we'd just tell it to expect the user id, + // the XSRF value used to be set in our ChallengeResult but now we don't have that so this needs to be set in the + // BackOfficeController when we issue a Challenge, see TODO notes there. var loginInfo = await _signInManager.GetExternalLoginInfoAsync(); if (loginInfo == null) @@ -322,8 +334,8 @@ namespace Umbraco.Web.BackOffice.Controllers return RedirectToLocal(Url.Action(nameof(Default), this.GetControllerName())); } - var result2 = await _userManager.AddLoginAsync(user, loginInfo); - if (result2.Succeeded) + var addLoginResult = await _userManager.AddLoginAsync(user, loginInfo); + if (addLoginResult.Succeeded) { // Update any authentication tokens if login succeeded // TODO: This is a new thing that we need to implement and because we can store data with the external login now, this is exactly @@ -334,7 +346,7 @@ namespace Umbraco.Web.BackOffice.Controllers } //Add errors and redirect for it to be displayed - TempData[ViewDataExtensions.TokenExternalSignInError] = result2.Errors; + TempData[ViewDataExtensions.TokenExternalSignInError] = addLoginResult.Errors; return RedirectToLocal(Url.Action(nameof(Default), this.GetControllerName())); } @@ -352,15 +364,27 @@ namespace Umbraco.Web.BackOffice.Controllers ViewData.SetUmbracoPath(_globalSettings.GetUmbracoMvcArea(_hostingEnvironment)); - //check if there is the TempData with the any token name specified, if so, assign to view bag and render the view - if (ViewData.FromTempData(TempData, ViewDataExtensions.TokenExternalSignInError) || + //check if there is the TempData or cookies with the any token name specified, if so, assign to view bag and render the view + if (ViewData.FromBase64CookieData(HttpContext, ViewDataExtensions.TokenExternalSignInError, _jsonSerializer) || + ViewData.FromTempData(TempData, ViewDataExtensions.TokenExternalSignInError) || ViewData.FromTempData(TempData, ViewDataExtensions.TokenPasswordResetCode)) return defaultResponse(); //First check if there's external login info, if there's not proceed as normal var loginInfo = await _signInManager.GetExternalLoginInfoAsync(); + if (loginInfo == null || loginInfo.Principal == null) { + // if the user is not logged in, check if there's any auto login redirects specified + if (!_backofficeSecurityAccessor.BackOfficeSecurity.ValidateCurrentUser()) + { + var oauthRedirectAuthProvider = _externalLogins.GetAutoLoginProvider(); + if (!oauthRedirectAuthProvider.IsNullOrWhiteSpace()) + { + return ExternalLogin(oauthRedirectAuthProvider); + } + } + return defaultResponse(); } @@ -383,7 +407,7 @@ namespace Umbraco.Web.BackOffice.Controllers } else { - autoLinkOptions = _externalAuthenticationOptions.Get(authType.Name); + autoLinkOptions = _externalLogins.Get(authType.Name); } // Sign in the user with this external login provider if the user already has a login @@ -391,12 +415,6 @@ namespace Umbraco.Web.BackOffice.Controllers var user = await _userManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey); if (user != null) { - // TODO: It might be worth keeping some of the claims associated with the ExternalLoginInfo, in which case we - // wouldn't necessarily sign the user in here with the standard login, instead we'd update the - // UseUmbracoBackOfficeExternalCookieAuthentication extension method to have the correct provider and claims factory, - // ticket format, etc.. to create our back office user including the claims assigned and in this method we'd just ensure - // that the ticket is created and stored and that the user is logged in. - var shouldSignIn = true; if (autoLinkOptions != null && autoLinkOptions.OnExternalLogin != null) { @@ -417,7 +435,10 @@ namespace Umbraco.Web.BackOffice.Controllers { if (await AutoLinkAndSignInExternalAccount(loginInfo, autoLinkOptions) == false) { - ViewData.SetExternalSignInError(new[] { "The requested provider (" + loginInfo.LoginProvider + ") has not been linked to an account" }); + ViewData.SetExternalSignInProviderErrors( + new BackOfficeExternalLoginProviderErrors( + loginInfo.LoginProvider, + new[] { "The requested provider (" + loginInfo.LoginProvider + ") has not been linked to an account" })); } //Remove the cookie otherwise this message will keep appearing @@ -440,7 +461,10 @@ namespace Umbraco.Web.BackOffice.Controllers //we are allowing auto-linking/creating of local accounts if (email.IsNullOrWhiteSpace()) { - ViewData.SetExternalSignInError(new[] { $"The requested provider ({loginInfo.LoginProvider}) has not provided the email claim {ClaimTypes.Email}, the account cannot be linked." }); + ViewData.SetExternalSignInProviderErrors( + new BackOfficeExternalLoginProviderErrors( + loginInfo.LoginProvider, + new[] { $"The requested provider ({loginInfo.LoginProvider}) has not provided the email claim {ClaimTypes.Email}, the account cannot be linked." })); } else { @@ -448,13 +472,26 @@ namespace Umbraco.Web.BackOffice.Controllers var autoLinkUser = await _userManager.FindByEmailAsync(email); if (autoLinkUser != null) { - // TODO This will be filled out with 8.9 changes - throw new NotImplementedException("Merge 8.9 changes in!"); + try + { + //call the callback if one is assigned + autoLinkOptions.OnAutoLinking?.Invoke(autoLinkUser, loginInfo); + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not link login provider {LoginProvider}.", loginInfo.LoginProvider); + ViewData.SetExternalSignInProviderErrors( + new BackOfficeExternalLoginProviderErrors( + loginInfo.LoginProvider, + new[] { "Could not link login provider " + loginInfo.LoginProvider + ". " + ex.Message })); + return true; + } + + await LinkUser(autoLinkUser, loginInfo); } else { var name = loginInfo.Principal?.Identity?.Name; - if (name.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Name value cannot be null"); autoLinkUser = BackOfficeIdentityUser.CreateNew(_globalSettings, email, email, autoLinkOptions.GetUserAutoLinkCulture(_globalSettings), name); @@ -465,40 +502,76 @@ namespace Umbraco.Web.BackOffice.Controllers } //call the callback if one is assigned - autoLinkOptions.OnAutoLinking?.Invoke(autoLinkUser, loginInfo); + try + { + autoLinkOptions.OnAutoLinking?.Invoke(autoLinkUser, loginInfo); + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not link login provider {LoginProvider}.", loginInfo.LoginProvider); + ViewData.SetExternalSignInProviderErrors( + new BackOfficeExternalLoginProviderErrors( + loginInfo.LoginProvider, + new[] { "Could not link login provider " + loginInfo.LoginProvider + ". " + ex.Message })); + return true; + } var userCreationResult = await _userManager.CreateAsync(autoLinkUser); if (userCreationResult.Succeeded == false) { - ViewData.SetExternalSignInError(userCreationResult.Errors.Select(x => x.Description).ToList()); + ViewData.SetExternalSignInProviderErrors( + new BackOfficeExternalLoginProviderErrors( + loginInfo.LoginProvider, + userCreationResult.Errors.Select(x => x.Description).ToList())); } else { - var linkResult = await _userManager.AddLoginAsync(autoLinkUser, loginInfo); - if (linkResult.Succeeded == false) - { - ViewData.SetExternalSignInError(linkResult.Errors.Select(x => x.Description).ToList()); - - //If this fails, we should really delete the user since it will be in an inconsistent state! - var deleteResult = await _userManager.DeleteAsync(autoLinkUser); - if (deleteResult.Succeeded == false) - { - //DOH! ... this isn't good, combine all errors to be shown - ViewData.SetExternalSignInError(linkResult.Errors.Concat(deleteResult.Errors).Select(x => x.Description).ToList()); - } - } - else - { - //sign in - await _signInManager.SignInAsync(autoLinkUser, isPersistent: false); - } + await LinkUser(autoLinkUser, loginInfo); } } } return true; } + private async Task LinkUser(BackOfficeIdentityUser autoLinkUser, ExternalLoginInfo loginInfo) + { + var existingLogins = await _userManager.GetLoginsAsync(autoLinkUser); + var exists = existingLogins.FirstOrDefault(x => x.LoginProvider == loginInfo.LoginProvider && x.ProviderKey == loginInfo.ProviderKey); + + // if it already exists (perhaps it was added in the AutoLink callbak) then we just continue + if (exists != null) + { + //sign in + await _signInManager.SignInAsync(autoLinkUser, isPersistent: false); + return; + } + + var linkResult = await _userManager.AddLoginAsync(autoLinkUser, loginInfo); + if (linkResult.Succeeded) + { + //we're good! sign in + await _signInManager.SignInAsync(autoLinkUser, isPersistent: false); + return; + } + + ViewData.SetExternalSignInProviderErrors( + new BackOfficeExternalLoginProviderErrors( + loginInfo.LoginProvider, + linkResult.Errors.Select(x => x.Description).ToList())); + + //If this fails, we should really delete the user since it will be in an inconsistent state! + var deleteResult = await _userManager.DeleteAsync(autoLinkUser); + if (!deleteResult.Succeeded) + { + //DOH! ... this isn't good, combine all errors to be shown + ViewData.SetExternalSignInProviderErrors( + new BackOfficeExternalLoginProviderErrors( + loginInfo.LoginProvider, + linkResult.Errors.Concat(deleteResult.Errors).Select(x => x.Description).ToList())); + } + } + // Used for XSRF protection when adding external logins // TODO: This is duplicated in BackOfficeSignInManager private const string XsrfKey = "XsrfId"; diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index 23c4dd68d8..b5d152edfb 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs @@ -19,6 +19,7 @@ using Umbraco.Web.BackOffice.Profiling; using Umbraco.Web.BackOffice.PropertyEditors; using Umbraco.Web.BackOffice.Routing; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Security; using Umbraco.Web.Editors; using Umbraco.Web.Features; using Umbraco.Web.Models.ContentEditing; @@ -44,7 +45,7 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly RuntimeSettings _runtimeSettings; private readonly SecuritySettings _securitySettings; private readonly IRuntimeMinifier _runtimeMinifier; - private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider; + private readonly IBackOfficeExternalLoginProviders _externalLogins; private readonly IImageUrlGenerator _imageUrlGenerator; private readonly PreviewRoutes _previewRoutes; @@ -61,7 +62,7 @@ namespace Umbraco.Web.BackOffice.Controllers IOptions runtimeSettings, IOptions securitySettings, IRuntimeMinifier runtimeMinifier, - IAuthenticationSchemeProvider authenticationSchemeProvider, + IBackOfficeExternalLoginProviders externalLogins, IImageUrlGenerator imageUrlGenerator, PreviewRoutes previewRoutes) { @@ -77,7 +78,7 @@ namespace Umbraco.Web.BackOffice.Controllers _runtimeSettings = runtimeSettings.Value; _securitySettings = securitySettings.Value; _runtimeMinifier = runtimeMinifier; - _authenticationSchemeProvider = authenticationSchemeProvider; + _externalLogins = externalLogins; _imageUrlGenerator = imageUrlGenerator; _previewRoutes = previewRoutes; } @@ -419,19 +420,11 @@ namespace Umbraco.Web.BackOffice.Controllers "externalLogins", new Dictionary { { - "providers", (await _authenticationSchemeProvider.GetAllSchemesAsync()) - // Filter only external providers - .Where(x => !x.DisplayName.IsNullOrWhiteSpace()) - // TODO: We need to filter only back office enabled schemes. - // Before we used to have a property bag to check, now we don't so need to investigate the easiest/best - // way to do this. We have the type so maybe we check for a marker interface, but maybe there's another way, - // just need to investigate. - //.Where(p => p.Properties.ContainsKey("UmbracoBackOffice")) + "providers", _externalLogins.GetBackOfficeProviders() .Select(p => new { - authType = p.Name, caption = p.DisplayName, - // TODO: See above, if we need this property bag in the vars then we'll need to figure something out - // properties = p.Properties + authType = p.AuthenticationType, caption = p.Name, + properties = p.Properties }) .ToArray() } diff --git a/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs index 7248c62c96..3086e02a31 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs @@ -201,10 +201,10 @@ namespace Umbraco.Web.BackOffice.Controllers } [AppendUserModifiedHeader] - public async Task PostSetAvatar(IList files) + public IActionResult PostSetAvatar(IList files) { //borrow the logic from the user controller - return await UsersController.PostSetAvatarInternal(files, _userService, _appCaches.RuntimeCache, _mediaFileSystem, _shortStringHelper, _contentSettings, _hostingEnvironment, _imageUrlGenerator, _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0)); + return UsersController.PostSetAvatarInternal(files, _userService, _appCaches.RuntimeCache, _mediaFileSystem, _shortStringHelper, _contentSettings, _hostingEnvironment, _imageUrlGenerator, _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0)); } /// @@ -216,6 +216,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// public async Task> PostChangePassword(ChangingPasswordModel data) { + // TODO: Why don't we inject this? Then we can just inject a logger var passwordChanger = new PasswordChanger(_loggerFactory.CreateLogger()); var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, data, _backOfficeUserManager); @@ -240,7 +241,15 @@ namespace Umbraco.Web.BackOffice.Controllers public async Task> GetCurrentUserLinkedLogins() { var identityUser = await _backOfficeUserManager.FindByIdAsync(_backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0).ToString()); - return identityUser.Logins.ToDictionary(x => x.LoginProvider, x => x.ProviderKey); + + // deduplicate in case there are duplicates (there shouldn't be now since we have a unique constraint on the external logins + // but there didn't used to be) + var result = new Dictionary(); + foreach (var l in identityUser.Logins) + { + result[l.LoginProvider] = l.ProviderKey; + } + return result; } } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/DenyLocalLoginAuthorizationAttribute.cs b/src/Umbraco.Web.BackOffice/Controllers/DenyLocalLoginAuthorizationAttribute.cs deleted file mode 100644 index 89a67d8f78..0000000000 --- a/src/Umbraco.Web.BackOffice/Controllers/DenyLocalLoginAuthorizationAttribute.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Web.Http; -using System.Web.Http.Controllers; -using Umbraco.Web.WebApi; -using Umbraco.Web.Security; - -namespace Umbraco.Web.Editors.Filters -{ - internal class DenyLocalLoginAuthorizationAttribute : AuthorizeAttribute - { - protected override bool IsAuthorized(HttpActionContext actionContext) - { - var owinContext = actionContext.Request.TryGetOwinContext().Result; - - // no authorization if any external logins deny local login - return !owinContext.Authentication.HasDenyLocalLogin(); - } - } -} diff --git a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs index 41b6c0a889..8bc99979ff 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs @@ -38,13 +38,12 @@ using Umbraco.Web.Common.ActionResults; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Editors; -using Umbraco.Web.Models; -using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Security; -using Umbraco.Web.WebApi.Filters; using Constants = Umbraco.Core.Constants; using IUser = Umbraco.Core.Models.Membership.IUser; using Task = System.Threading.Tasks.Task; +using Umbraco.Net; +using Umbraco.Web.Common.ActionsResults; +using Umbraco.Web.Common.Security; namespace Umbraco.Web.BackOffice.Controllers { @@ -72,9 +71,11 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly IMediaService _mediaService; private readonly IContentService _contentService; private readonly GlobalSettings _globalSettings; - private readonly IBackOfficeUserManager _backOfficeUserManager; + private readonly IBackOfficeUserManager _userManager; private readonly ILoggerFactory _loggerFactory; private readonly LinkGenerator _linkGenerator; + private readonly IBackOfficeExternalLoginProviders _externalLogins; + private readonly ILogger _logger; public UsersController( IMediaFileSystem mediaFileSystem, @@ -97,7 +98,8 @@ namespace Umbraco.Web.BackOffice.Controllers IOptions globalSettings, IBackOfficeUserManager backOfficeUserManager, ILoggerFactory loggerFactory, - LinkGenerator linkGenerator) + LinkGenerator linkGenerator, + IBackOfficeExternalLoginProviders externalLogins) { _mediaFileSystem = mediaFileSystem; _contentSettings = contentSettings.Value; @@ -117,9 +119,11 @@ namespace Umbraco.Web.BackOffice.Controllers _mediaService = mediaService; _contentService = contentService; _globalSettings = globalSettings.Value; - _backOfficeUserManager = backOfficeUserManager; + _userManager = backOfficeUserManager; _loggerFactory = loggerFactory; _linkGenerator = linkGenerator; + _externalLogins = externalLogins; + _logger = _loggerFactory.CreateLogger(); } /// @@ -137,12 +141,12 @@ namespace Umbraco.Web.BackOffice.Controllers [AppendUserModifiedHeader("id")] [AdminUsersAuthorize] - public async Task PostSetAvatar(int id, IList files) + public IActionResult PostSetAvatar(int id, IList files) { - return await PostSetAvatarInternal(files, _userService, _appCaches.RuntimeCache, _mediaFileSystem, _shortStringHelper, _contentSettings, _hostingEnvironment, _imageUrlGenerator, id); + return PostSetAvatarInternal(files, _userService, _appCaches.RuntimeCache, _mediaFileSystem, _shortStringHelper, _contentSettings, _hostingEnvironment, _imageUrlGenerator, id); } - internal static async Task PostSetAvatarInternal(IList files, IUserService userService, IAppCache cache, IMediaFileSystem mediaFileSystem, IShortStringHelper shortStringHelper, ContentSettings contentSettings, IHostingEnvironment hostingEnvironment, IImageUrlGenerator imageUrlGenerator, int id) + internal static IActionResult PostSetAvatarInternal(IList files, IUserService userService, IAppCache cache, IMediaFileSystem mediaFileSystem, IShortStringHelper shortStringHelper, ContentSettings contentSettings, IHostingEnvironment hostingEnvironment, IImageUrlGenerator imageUrlGenerator, int id) { if (files is null) { @@ -375,16 +379,16 @@ namespace Umbraco.Web.BackOffice.Controllers var identityUser = BackOfficeIdentityUser.CreateNew(_globalSettings, userSave.Username, userSave.Email, _globalSettings.DefaultUILanguage); identityUser.Name = userSave.Name; - var created = await _backOfficeUserManager.CreateAsync(identityUser); + var created = await _userManager.CreateAsync(identityUser); if (created.Succeeded == false) { throw HttpResponseException.CreateNotificationValidationErrorResponse(created.Errors.ToErrorMessage()); } string resetPassword; - var password = _backOfficeUserManager.GeneratePassword(); + var password = _userManager.GeneratePassword(); - var result = await _backOfficeUserManager.AddPasswordAsync(identityUser, password); + var result = await _userManager.AddPasswordAsync(identityUser, password); if (result.Succeeded == false) { throw HttpResponseException.CreateNotificationValidationErrorResponse(created.Errors.ToErrorMessage()); @@ -416,7 +420,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// This will email the user an invite and generate a token that will be validated in the email /// - public async Task PostInviteUser(UserInvite userSave) + public async Task> PostInviteUser(UserInvite userSave) { if (userSave == null) throw new ArgumentNullException("userSave"); @@ -425,7 +429,7 @@ namespace Umbraco.Web.BackOffice.Controllers if (ModelState.IsValid == false) { - throw new HttpResponseException(HttpStatusCode.BadRequest, ModelState); + return new ValidationErrorResult(ModelState); } IUser user; @@ -441,12 +445,9 @@ namespace Umbraco.Web.BackOffice.Controllers } user = CheckUniqueEmail(userSave.Email, u => u.LastLoginDate != default || u.EmailConfirmedDate.HasValue); - var userMgr = TryGetOwinContext().Result.GetBackOfficeUserManager(); - - if (!EmailSender.CanSendRequiredEmail(GlobalSettings) && !userMgr.HasSendingUserInviteEventHandler) + if (!EmailSender.CanSendRequiredEmail(_globalSettings) && !_userManager.HasSendingUserInviteEventHandler) { - throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse("No Email server is configured")); + return new ValidationErrorResult("No Email server is configured"); } //Perform authorization here to see if the current user can actually save this user with the info being requested @@ -454,7 +455,7 @@ namespace Umbraco.Web.BackOffice.Controllers var canSaveUser = authHelper.IsAuthorized(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, user, null, null, userSave.UserGroups); if (canSaveUser == false) { - throw new HttpResponseException(HttpStatusCode.Unauthorized, canSaveUser.Result); + return new ValidationErrorResult(canSaveUser.Result, StatusCodes.Status401Unauthorized); } if (user == null) @@ -464,10 +465,10 @@ namespace Umbraco.Web.BackOffice.Controllers var identityUser = BackOfficeIdentityUser.CreateNew(_globalSettings, userSave.Username, userSave.Email, _globalSettings.DefaultUILanguage); identityUser.Name = userSave.Name; - var created = await _backOfficeUserManager.CreateAsync(identityUser); + var created = await _userManager.CreateAsync(identityUser); if (created.Succeeded == false) { - throw HttpResponseException.CreateNotificationValidationErrorResponse(created.Errors.ToErrorMessage()); + return ValidationErrorResult.CreateNotificationValidationErrorResult(created.Errors.ToErrorMessage()); } //now re-look the user back up @@ -484,21 +485,16 @@ namespace Umbraco.Web.BackOffice.Controllers _userService.Save(user); var display = _umbracoMapper.Map(user); - var inviteArgs = new UserInviteEventArgs( - Request.TryGetHttpContext().Result.GetCurrentRequestIpAddress(), - performingUser: Security.GetUserId().Result, - userSave, - user); + UserInviteEventArgs inviteArgs; try { - userMgr.RaiseSendingUserInvite(inviteArgs); + inviteArgs = _userManager.RaiseSendingUserInvite(User, userSave, user); } catch (Exception ex) { - Logger.Error(ex, "An error occured in a custom event handler while inviting the user"); - throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse($"An error occured inviting the user (check logs for more info): {ex.Message}")); + _logger.LogError(ex, "An error occured in a custom event handler while inviting the user"); + return ValidationErrorResult.CreateNotificationValidationErrorResult($"An error occured inviting the user (check logs for more info): {ex.Message}"); } // If the event is handled then no need to send the email @@ -553,8 +549,8 @@ namespace Umbraco.Web.BackOffice.Controllers private async Task SendUserInviteEmailAsync(UserBasic userDisplay, string from, string fromEmail, IUser to, string message) { - var user = await _backOfficeUserManager.FindByIdAsync(((int) userDisplay.Id).ToString()); - var token = await _backOfficeUserManager.GenerateEmailConfirmationTokenAsync(user); + var user = await _userManager.FindByIdAsync(((int) userDisplay.Id).ToString()); + var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); var inviteToken = string.Format("{0}{1}{2}", (int)userDisplay.Id, @@ -625,8 +621,7 @@ namespace Umbraco.Web.BackOffice.Controllers var hasErrors = false; // we need to check if there's any Deny Local login providers present, if so we need to ensure that the user's email address cannot be changed - var owinContext = Request.TryGetOwinContext().Result; - var hasDenyLocalLogin = owinContext.Authentication.HasDenyLocalLogin(); + var hasDenyLocalLogin = _externalLogins.HasDenyLocalLogin(); if (hasDenyLocalLogin) { userSave.Email = found.Email; // it cannot change, this would only happen if people are mucking around with the request @@ -706,8 +701,9 @@ namespace Umbraco.Web.BackOffice.Controllers throw new HttpResponseException(HttpStatusCode.NotFound); } + // TODO: Why don't we inject this? Then we can just inject a logger var passwordChanger = new PasswordChanger(_loggerFactory.CreateLogger()); - var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, found, changingPasswordModel, _backOfficeUserManager); + var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, found, changingPasswordModel, _userManager); if (passwordChangeResult.Success) { @@ -792,14 +788,14 @@ namespace Umbraco.Web.BackOffice.Controllers foreach (var u in userIds) { - var user = await _backOfficeUserManager.FindByIdAsync(u.ToString()); + var user = await _userManager.FindByIdAsync(u.ToString()); if (user == null) { notFound.Add(u); continue; } - var unlockResult = await _backOfficeUserManager.SetLockoutEndDateAsync(user, DateTimeOffset.Now); + var unlockResult = await _userManager.SetLockoutEndDateAsync(user, DateTimeOffset.Now); if (unlockResult.Succeeded == false) { throw HttpResponseException.CreateValidationErrorResponse( diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs index 3c6b538506..7002203d33 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs @@ -2,8 +2,8 @@ using System; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using SixLabors.ImageSharp.Web.DependencyInjection; +using Umbraco.Web.BackOffice.Middleware; using Umbraco.Web.BackOffice.Routing; -using Umbraco.Web.BackOffice.Security; namespace Umbraco.Extensions { @@ -49,6 +49,7 @@ namespace Umbraco.Extensions app.UseUmbracoRuntimeMinification(); app.UseMiddleware(); + app.UseMiddleware(); return app; } diff --git a/src/Umbraco.Web.BackOffice/Extensions/HtmlHelperBackOfficeExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/HtmlHelperBackOfficeExtensions.cs index 153e309992..9828931198 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/HtmlHelperBackOfficeExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/HtmlHelperBackOfficeExtensions.cs @@ -19,6 +19,7 @@ using Umbraco.Web.Models; using Umbraco.Web.WebApi; using Umbraco.Web.WebAssets; using Umbraco.Core; +using Umbraco.Core.Security; namespace Umbraco.Extensions { @@ -61,27 +62,20 @@ namespace Umbraco.Extensions /// Used to render the script that will pass in the angular "externalLoginInfo" service/value on page load /// /// - /// + /// /// - public static async Task AngularValueExternalLoginInfoScriptAsync(this IHtmlHelper html, - BackOfficeExternalLoginProviderErrors externalLoginErrors, - BackOfficeSignInManager signInManager, - IEnumerable externalLoginErrors) + public static Task AngularValueExternalLoginInfoScriptAsync(this IHtmlHelper html, + IBackOfficeExternalLoginProviders externalLogins, + BackOfficeExternalLoginProviderErrors externalLoginErrors) { - var providers = await signInManager.GetExternalAuthenticationSchemesAsync(); + var providers = externalLogins.GetBackOfficeProviders(); var loginProviders = providers - // TODO: We need to filter only back office enabled schemes. - // Before we used to have a property bag to check, now we don't so need to investigate the easiest/best - // way to do this. We have the type so maybe we check for a marker interface, but maybe there's another way, - // just need to investigate. - //.Where(p => p.Properties.ContainsKey("UmbracoBackOffice")) .Select(p => new { - authType = p.Name, - caption = p.DisplayName, - // TODO: See above, if we need this property bag in the vars then we'll need to figure something out - //properties = p.Properties + authType = p.AuthenticationType, + caption = p.Name, + properties = p.Properties }) .ToArray(); @@ -105,13 +99,7 @@ namespace Umbraco.Extensions sb.AppendLine(JsonConvert.SerializeObject(loginProviders)); sb.AppendLine(@"});"); - return html.Raw(sb.ToString()); - } - - [Obsolete("Use the other overload instead")] - public static IHtmlString AngularValueExternalLoginInfoScript(this HtmlHelper html, IEnumerable externalLoginErrors) - { - return html.AngularValueExternalLoginInfoScript(new BackOfficeExternalLoginProviderErrors(string.Empty, externalLoginErrors)); + return Task.FromResult(html.Raw(sb.ToString())); } /// diff --git a/src/Umbraco.Web.BackOffice/Extensions/HttpContextExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/HttpContextExtensions.cs new file mode 100644 index 0000000000..dd41de67bd --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Extensions/HttpContextExtensions.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Http; +using Umbraco.Core.Security; + +namespace Umbraco.Extensions +{ + public static class HttpContextExtensions + { + public static void SetExternalLoginProviderErrors(this HttpContext httpContext, BackOfficeExternalLoginProviderErrors errors) + => httpContext.Items[nameof(BackOfficeExternalLoginProviderErrors)] = errors; + + public static BackOfficeExternalLoginProviderErrors GetExternalLoginProviderErrors(this HttpContext httpContext) + => httpContext.Items[nameof(BackOfficeExternalLoginProviderErrors)] as BackOfficeExternalLoginProviderErrors; + } +} diff --git a/src/Umbraco.Web.BackOffice/Filters/AppendUserModifiedHeaderAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/AppendUserModifiedHeaderAttribute.cs index 3c2efec3a3..04adab4a27 100644 --- a/src/Umbraco.Web.BackOffice/Filters/AppendUserModifiedHeaderAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/AppendUserModifiedHeaderAttribute.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; using Umbraco.Core; +using Umbraco.Core.Security; using Umbraco.Web.Security; namespace Umbraco.Web.BackOffice.Filters diff --git a/src/Umbraco.Web.BackOffice/Filters/ContentModelValidator.cs b/src/Umbraco.Web.BackOffice/Filters/ContentModelValidator.cs index 1ce72d8723..0d7a3a14aa 100644 --- a/src/Umbraco.Web.BackOffice/Filters/ContentModelValidator.cs +++ b/src/Umbraco.Web.BackOffice/Filters/ContentModelValidator.cs @@ -1,5 +1,4 @@ -using Newtonsoft.Json; -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -12,7 +11,7 @@ using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Extensions; using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Security; +using Umbraco.Core.Security; namespace Umbraco.Web.BackOffice.Filters { diff --git a/src/Umbraco.Web.BackOffice/Filters/ContentSaveModelValidator.cs b/src/Umbraco.Web.BackOffice/Filters/ContentSaveModelValidator.cs index ef50f4fa74..caaee1d9e0 100644 --- a/src/Umbraco.Web.BackOffice/Filters/ContentSaveModelValidator.cs +++ b/src/Umbraco.Web.BackOffice/Filters/ContentSaveModelValidator.cs @@ -1,8 +1,8 @@ using Microsoft.Extensions.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Security; namespace Umbraco.Web.BackOffice.Filters { diff --git a/src/Umbraco.Web.BackOffice/Filters/DenyLocalLoginAuthorizationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/DenyLocalLoginAuthorizationAttribute.cs new file mode 100644 index 0000000000..a5d22d702d --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Filters/DenyLocalLoginAuthorizationAttribute.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using System; +using Umbraco.Web.Common.Security; + +namespace Umbraco.Web.Editors.Filters +{ + public sealed class DenyLocalLoginAuthorizationAttribute : TypeFilterAttribute + { + public DenyLocalLoginAuthorizationAttribute() : base(typeof(DenyLocalLoginFilter)) + { + } + + private class DenyLocalLoginFilter : IAuthorizationFilter + { + private readonly IBackOfficeExternalLoginProviders _externalLogins; + + public DenyLocalLoginFilter(IBackOfficeExternalLoginProviders externalLogins) + { + _externalLogins = externalLogins; + } + + public void OnAuthorization(AuthorizationFilterContext context) + { + if (_externalLogins.HasDenyLocalLogin()) + { + // if there is a deny local login provider then we cannot authorize + context.Result = new ForbidResult(); + } + } + } + + } +} diff --git a/src/Umbraco.Web.BackOffice/Filters/MediaSaveModelValidator.cs b/src/Umbraco.Web.BackOffice/Filters/MediaSaveModelValidator.cs index 925d52e948..b398a4e401 100644 --- a/src/Umbraco.Web.BackOffice/Filters/MediaSaveModelValidator.cs +++ b/src/Umbraco.Web.BackOffice/Filters/MediaSaveModelValidator.cs @@ -1,8 +1,8 @@ using Microsoft.Extensions.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Security; namespace Umbraco.Web.BackOffice.Filters { diff --git a/src/Umbraco.Web.BackOffice/Filters/MemberSaveModelValidator.cs b/src/Umbraco.Web.BackOffice/Filters/MemberSaveModelValidator.cs index a84b4f1c9e..275220c8b4 100644 --- a/src/Umbraco.Web.BackOffice/Filters/MemberSaveModelValidator.cs +++ b/src/Umbraco.Web.BackOffice/Filters/MemberSaveModelValidator.cs @@ -7,11 +7,11 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Logging; using Umbraco.Core; using Umbraco.Core.Models; +using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Extensions; using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Security; namespace Umbraco.Web.BackOffice.Filters { diff --git a/src/Umbraco.Web/Security/BackOfficeExternalLoginProviderErrorMiddlware.cs b/src/Umbraco.Web.BackOffice/Middleware/BackOfficeExternalLoginProviderErrorMiddlware.cs similarity index 62% rename from src/Umbraco.Web/Security/BackOfficeExternalLoginProviderErrorMiddlware.cs rename to src/Umbraco.Web.BackOffice/Middleware/BackOfficeExternalLoginProviderErrorMiddlware.cs index 6e6477443b..0a58f0018b 100644 --- a/src/Umbraco.Web/Security/BackOfficeExternalLoginProviderErrorMiddlware.cs +++ b/src/Umbraco.Web.BackOffice/Middleware/BackOfficeExternalLoginProviderErrorMiddlware.cs @@ -1,31 +1,27 @@ using System; -using System.Collections.Generic; using System.Text; using System.Threading.Tasks; -using System.Web.Mvc; -using Microsoft.Owin; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Newtonsoft.Json; using Umbraco.Core; +using Umbraco.Extensions; -namespace Umbraco.Web.Security +namespace Umbraco.Web.BackOffice.Middleware { /// /// Used to handle errors registered by external login providers /// /// - /// When an external login provider registers an error with during the OAuth process, + /// When an external login provider registers an error with during the OAuth process, /// this middleware will detect that, store the errors into cookie data and redirect to the back office login so we can read the errors back out. /// - internal class BackOfficeExternalLoginProviderErrorMiddlware : OwinMiddleware + public class BackOfficeExternalLoginProviderErrorMiddlware : IMiddleware { - public BackOfficeExternalLoginProviderErrorMiddlware(OwinMiddleware next) : base(next) - { - } - - public override async Task Invoke(IOwinContext context) + public async Task InvokeAsync(HttpContext context, RequestDelegate next) { var shortCircuit = false; - if (!context.Request.Uri.IsClientSideRequest()) + if (!context.Request.IsClientSideRequest()) { // check if we have any errors registered var errors = context.GetExternalLoginProviderErrors(); @@ -39,16 +35,16 @@ namespace Umbraco.Web.Security { Expires = DateTime.Now.AddMinutes(5), HttpOnly = true, - Secure = context.Request.IsSecure + Secure = context.Request.IsHttps }); - context.Response.Redirect(context.Request.Uri.ToString()); + context.Response.Redirect(context.Request.GetEncodedUrl()); } } - if (Next != null && !shortCircuit) + if (next != null && !shortCircuit) { - await Next.Invoke(context); + await next(context); } } } diff --git a/src/Umbraco.Web.BackOffice/Security/PreviewAuthenticationMiddleware.cs b/src/Umbraco.Web.BackOffice/Middleware/PreviewAuthenticationMiddleware.cs similarity index 97% rename from src/Umbraco.Web.BackOffice/Security/PreviewAuthenticationMiddleware.cs rename to src/Umbraco.Web.BackOffice/Middleware/PreviewAuthenticationMiddleware.cs index 1854187a2a..284dbbc913 100644 --- a/src/Umbraco.Web.BackOffice/Security/PreviewAuthenticationMiddleware.cs +++ b/src/Umbraco.Web.BackOffice/Middleware/PreviewAuthenticationMiddleware.cs @@ -10,7 +10,7 @@ using Umbraco.Core.Configuration.Models; using Umbraco.Core.Hosting; using Umbraco.Extensions; -namespace Umbraco.Web.BackOffice.Security +namespace Umbraco.Web.BackOffice.Middleware { /// /// Ensures that preview pages (front-end routed) are authenticated with the back office identity appended to the principal alongside any default authentication that takes place @@ -55,13 +55,11 @@ namespace Umbraco.Web.BackOffice.Security { var backOfficeIdentity = unprotected.Principal.GetUmbracoIdentity(); if (backOfficeIdentity != null) - { //Ok, we've got a real ticket, now we can add this ticket's identity to the current // Principal, this means we'll have 2 identities assigned to the principal which we can // use to authorize the preview and allow for a back office User. context.User.AddIdentity(backOfficeIdentity); - } } } diff --git a/src/Umbraco.Web.BackOffice/Runtime/BackOfficeComposer.cs b/src/Umbraco.Web.BackOffice/Runtime/BackOfficeComposer.cs index 005a0db36e..fbd4d02732 100644 --- a/src/Umbraco.Web.BackOffice/Runtime/BackOfficeComposer.cs +++ b/src/Umbraco.Web.BackOffice/Runtime/BackOfficeComposer.cs @@ -8,6 +8,7 @@ using Umbraco.Core.Services; using Umbraco.Extensions; using Umbraco.Web.BackOffice.Controllers; using Umbraco.Web.BackOffice.Filters; +using Umbraco.Web.BackOffice.Middleware; using Umbraco.Web.BackOffice.Routing; using Umbraco.Web.BackOffice.Security; using Umbraco.Web.BackOffice.Services; diff --git a/src/Umbraco.Web.BackOffice/Services/IconService.cs b/src/Umbraco.Web.BackOffice/Services/IconService.cs index a29f1a3210..cfb4c820be 100644 --- a/src/Umbraco.Web.BackOffice/Services/IconService.cs +++ b/src/Umbraco.Web.BackOffice/Services/IconService.cs @@ -115,8 +115,9 @@ namespace Umbraco.Web.BackOffice.Services private IEnumerable GetAllIconNames() { + // TODO: See comment: https://github.com/umbraco/Umbraco-CMS/pull/8884/files#r510564185 // add icons from plugins - var appPlugins = new DirectoryInfo(_hostingEnvironment.MapPath(Constants.SystemDirectories.AppPlugins)); + var appPlugins = new DirectoryInfo(_hostingEnvironment.MapPathWebRoot(Constants.SystemDirectories.AppPlugins)); var pluginIcons = appPlugins.Exists == false ? new List() : appPlugins.GetDirectories() @@ -125,7 +126,7 @@ namespace Umbraco.Web.BackOffice.Services .SelectMany(x => x.GetFiles("*.svg", SearchOption.TopDirectoryOnly)); // add icons from IconsPath if not already added from plugins - var directory = new DirectoryInfo(_hostingEnvironment.MapPath($"{_globalSettings.IconsPath}/")); + var directory = new DirectoryInfo(_hostingEnvironment.MapPathWebRoot($"{_globalSettings.Value.IconsPath}/")); var iconNames = directory.GetFiles("*.svg") .Where(x => pluginIcons.Any(i => i.Name == x.Name) == false); diff --git a/src/Umbraco.Web.Common/ActionsResults/ValidationErrorResult.cs b/src/Umbraco.Web.Common/ActionsResults/ValidationErrorResult.cs index c279192221..0116c6b77a 100644 --- a/src/Umbraco.Web.Common/ActionsResults/ValidationErrorResult.cs +++ b/src/Umbraco.Web.Common/ActionsResults/ValidationErrorResult.cs @@ -1,16 +1,44 @@ using System.Net; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Common.ActionsResults { /// - /// Custom result to return a validation error message with a 400 http response and required headers + /// Custom result to return a validation error message with required headers /// + /// + /// The default status code is a 400 http response + /// public class ValidationErrorResult : ObjectResult { - public ValidationErrorResult(string errorMessage) : base(new { Message = errorMessage }) + public static ValidationErrorResult CreateNotificationValidationErrorResult(string errorMessage) + { + var notificationModel = new SimpleNotificationModel + { + Message = errorMessage + }; + notificationModel.AddErrorNotification(errorMessage, string.Empty); + return new ValidationErrorResult(notificationModel); + } + + public ValidationErrorResult(object value, int statusCode) : base(value) + { + StatusCode = statusCode; + } + + public ValidationErrorResult(object value) : this(value, StatusCodes.Status400BadRequest) + { + } + + public ValidationErrorResult(string errorMessage, int statusCode) : base(new { Message = errorMessage }) + { + StatusCode = statusCode; + } + + public ValidationErrorResult(string errorMessage) : this(errorMessage, StatusCodes.Status400BadRequest) { - StatusCode = (int)HttpStatusCode.BadRequest; } public override void OnFormatting(ActionContext context) diff --git a/src/Umbraco.Web.Common/Extensions/ViewDataExtensions.cs b/src/Umbraco.Web.Common/Extensions/ViewDataExtensions.cs index 9e63244fcc..655df315f8 100644 --- a/src/Umbraco.Web.Common/Extensions/ViewDataExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ViewDataExtensions.cs @@ -1,7 +1,12 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Semver; - +using Umbraco.Core; +using Umbraco.Core.Security; +using Umbraco.Core.Serialization; namespace Umbraco.Extensions { @@ -21,6 +26,49 @@ namespace Umbraco.Extensions return true; } + /// + /// Copies data from a request cookie to view data and then clears the cookie in the response + /// + /// + /// + /// + /// + /// + /// + /// This is similar to TempData but in some cases we cannot use TempData which relies on the temp data provider and session. + /// The cookie value can either be a simple string value + /// + /// + public static bool FromBase64CookieData(this ViewDataDictionary viewData, HttpContext httpContext, string cookieName, IJsonSerializer serializer) + { + var hasCookie = httpContext.Request.Cookies.ContainsKey(cookieName); + if (!hasCookie) return false; + + // get the cookie value + if (!httpContext.Request.Cookies.TryGetValue(cookieName, out var cookieVal)) + { + return false; + } + + // ensure the cookie is expired (must be done after reading the value) + httpContext.Response.Cookies.Delete(cookieName); + + if (cookieVal.IsNullOrWhiteSpace()) + return false; + + try + { + var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(System.Net.WebUtility.UrlDecode(cookieVal))); + // deserialize to T and store in viewdata + viewData[cookieName] = serializer.Deserialize(decoded); + return true; + } + catch (Exception) + { + return false; + } + } + public static string GetUmbracoPath(this ViewDataDictionary viewData) { return (string)viewData[TokenUmbracoPath]; @@ -60,14 +108,24 @@ namespace Umbraco.Extensions return (SemVersion) viewData[TokenUmbracoVersion]; } - public static IEnumerable GetExternalSignInError(this ViewDataDictionary viewData) + /// + /// Used by the back office login screen to get any registered external login provider errors + /// + /// + /// + public static BackOfficeExternalLoginProviderErrors GetExternalSignInProviderErrors(this ViewDataDictionary viewData) { - return (IEnumerable)viewData[TokenExternalSignInError]; + return (BackOfficeExternalLoginProviderErrors)viewData[TokenExternalSignInError]; } - public static void SetExternalSignInError(this ViewDataDictionary viewData, IEnumerable value) + /// + /// Used by the back office controller to register any external login provider errors + /// + /// + /// + public static void SetExternalSignInProviderErrors(this ViewDataDictionary viewData, BackOfficeExternalLoginProviderErrors errors) { - viewData[TokenExternalSignInError] = value; + viewData[TokenExternalSignInError] = errors; } public static string GetPasswordResetCode(this ViewDataDictionary viewData) diff --git a/src/Umbraco.Web.Common/Filters/UmbracoAuthorizeFilter.cs b/src/Umbraco.Web.Common/Filters/UmbracoAuthorizeFilter.cs index 66b1462ae9..bde15b0f65 100644 --- a/src/Umbraco.Web.Common/Filters/UmbracoAuthorizeFilter.cs +++ b/src/Umbraco.Web.Common/Filters/UmbracoAuthorizeFilter.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Routing; using System; using Umbraco.Core; +using Umbraco.Core.Security; using Umbraco.Extensions; using Umbraco.Web.Security; using IHostingEnvironment = Umbraco.Core.Hosting.IHostingEnvironment; diff --git a/src/Umbraco.Web/Security/BackOfficeExternalLoginProviderOptions.cs b/src/Umbraco.Web.Common/Security/BackOfficeExternalLoginProviderOptions.cs similarity index 69% rename from src/Umbraco.Web/Security/BackOfficeExternalLoginProviderOptions.cs rename to src/Umbraco.Web.Common/Security/BackOfficeExternalLoginProviderOptions.cs index 4ef527460e..de16d0ec14 100644 --- a/src/Umbraco.Web/Security/BackOfficeExternalLoginProviderOptions.cs +++ b/src/Umbraco.Web.Common/Security/BackOfficeExternalLoginProviderOptions.cs @@ -1,10 +1,7 @@ -using Microsoft.Owin; -using Microsoft.Owin.Security; -using System; -using System.Collections; +using System; using System.Runtime.Serialization; -namespace Umbraco.Web.Security +namespace Umbraco.Web.Common.Security { /// @@ -12,19 +9,9 @@ namespace Umbraco.Web.Security /// public class BackOfficeExternalLoginProviderOptions { - /// - /// When specified this will be called to retrieve the used during the authentication Challenge response. - /// - /// - /// This will generally not be needed since OpenIdConnect.RedirectToIdentityProvider options should be used instead - /// - [IgnoreDataMember] - public Func OnChallenge { get; set; } - /// /// Options used to control how users can be auto-linked/created/updated based on the external login provider /// - [IgnoreDataMember] // we are ignoring this one from serialization for backwards compat since these options are manually incuded in the response separately public ExternalSignInAutoLinkOptions AutoLinkOptions { get; set; } = new ExternalSignInAutoLinkOptions(); /// diff --git a/src/Umbraco.Web.Common/Security/BackofficeSecurity.cs b/src/Umbraco.Web.Common/Security/BackofficeSecurity.cs index 7ef628197e..dc312ed9ca 100644 --- a/src/Umbraco.Web.Common/Security/BackofficeSecurity.cs +++ b/src/Umbraco.Web.Common/Security/BackofficeSecurity.cs @@ -7,21 +7,21 @@ using Umbraco.Core.Configuration.Models; using Umbraco.Core.Hosting; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Extensions; -using Umbraco.Web.Security; namespace Umbraco.Web.Common.Security { - public class BackofficeSecurity : IBackOfficeSecurity + public class BackOfficeSecurity : IBackOfficeSecurity { private readonly IUserService _userService; private readonly GlobalSettings _globalSettings; private readonly IHostingEnvironment _hostingEnvironment; private readonly IHttpContextAccessor _httpContextAccessor; - public BackofficeSecurity( + public BackOfficeSecurity( IUserService userService, IOptions globalSettings, IHostingEnvironment hostingEnvironment, diff --git a/src/Umbraco.Web.Common/Security/BackofficeSecurityFactory.cs b/src/Umbraco.Web.Common/Security/BackofficeSecurityFactory.cs index 4341b5524d..0a3c362971 100644 --- a/src/Umbraco.Web.Common/Security/BackofficeSecurityFactory.cs +++ b/src/Umbraco.Web.Common/Security/BackofficeSecurityFactory.cs @@ -35,7 +35,7 @@ namespace Umbraco.Web.Common.Security { if (_backOfficeSecurityAccessor.BackOfficeSecurity is null) { - _backOfficeSecurityAccessor.BackOfficeSecurity = new BackofficeSecurity(_userService, _globalSettings, _hostingEnvironment, _httpContextAccessor); + _backOfficeSecurityAccessor.BackOfficeSecurity = new BackOfficeSecurity(_userService, _globalSettings, _hostingEnvironment, _httpContextAccessor); } } diff --git a/src/Umbraco.Web.Common/Security/ExternalAuthenticationOptions.cs b/src/Umbraco.Web.Common/Security/ExternalAuthenticationOptions.cs deleted file mode 100644 index 9005251835..0000000000 --- a/src/Umbraco.Web.Common/Security/ExternalAuthenticationOptions.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Umbraco.Web.Common.Security -{ - // TODO: We need to implement this and extend it to support the back office external login options - public interface IExternalAuthenticationOptions - { - ExternalSignInAutoLinkOptions Get(string authenticationType); - } - -} diff --git a/src/Umbraco.Web.Common/Security/ExternalSignInAutoLinkOptions.cs b/src/Umbraco.Web.Common/Security/ExternalSignInAutoLinkOptions.cs index d39d54ba3d..0a81a503dd 100644 --- a/src/Umbraco.Web.Common/Security/ExternalSignInAutoLinkOptions.cs +++ b/src/Umbraco.Web.Common/Security/ExternalSignInAutoLinkOptions.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Identity; using System; +using System.Runtime.Serialization; using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration.Models; using SecurityConstants = Umbraco.Core.Constants.Security; diff --git a/src/Umbraco.Web.Common/Security/IBackOfficeExternalLoginProviders.cs b/src/Umbraco.Web.Common/Security/IBackOfficeExternalLoginProviders.cs new file mode 100644 index 0000000000..106f5378bb --- /dev/null +++ b/src/Umbraco.Web.Common/Security/IBackOfficeExternalLoginProviders.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Umbraco.Web.Common.Security +{ + // TODO: We need to implement this and extend it to support the back office external login options + // basically migrate things from AuthenticationManagerExtensions & AuthenticationOptionsExtensions + // and use this to get the back office external login infos + public interface IBackOfficeExternalLoginProviders + { + ExternalSignInAutoLinkOptions Get(string authenticationType); + + IEnumerable GetBackOfficeProviders(); + + /// + /// Returns the authentication type for the last registered external login (oauth) provider that specifies an auto-login redirect option + /// + /// + /// + string GetAutoLoginProvider(); + + bool HasDenyLocalLogin(); + } + + // TODO: we'll need to register these somehow + public class BackOfficeExternalLoginProvider + { + public string Name { get; set; } + public string AuthenticationType { get; set; } + + // TODO: I believe this should be replaced with just a reference to BackOfficeExternalLoginProviderOptions + public IReadOnlyDictionary Properties { get; set; } + } + +} diff --git a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs index 290af371cc..a0bc8b9140 100644 --- a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs +++ b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs @@ -1,14 +1,11 @@ using System; -using Microsoft.Extensions.Options; -using Umbraco.Composing; using Umbraco.Core; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Hosting; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Security; using Umbraco.Web.PublishedCache; using Umbraco.Web.Routing; -using Umbraco.Web.Security; namespace Umbraco.Web { diff --git a/src/Umbraco.Web.UI.NetCore/umbraco/UmbracoBackOffice/AuthorizeUpgrade.cshtml b/src/Umbraco.Web.UI.NetCore/umbraco/UmbracoBackOffice/AuthorizeUpgrade.cshtml index e4e4c1d191..3cf342e6e0 100644 --- a/src/Umbraco.Web.UI.NetCore/umbraco/UmbracoBackOffice/AuthorizeUpgrade.cshtml +++ b/src/Umbraco.Web.UI.NetCore/umbraco/UmbracoBackOffice/AuthorizeUpgrade.cshtml @@ -8,11 +8,11 @@ @using Umbraco.Core.Hosting @using Umbraco.Extensions @using Umbraco.Web.BackOffice.Controllers -@inject BackOfficeSignInManager signInManager @inject BackOfficeServerVariables backOfficeServerVariables @inject IUmbracoVersion umbracoVersion @inject IHostingEnvironment hostingEnvironment @inject IOptions globalSettings +@inject IBackOfficeExternalLoginProviders externalLogins @inject IRuntimeMinifier runtimeMinifier @{ @@ -59,7 +59,7 @@ - - @*And finally we can load in our angular app*@ - - - - - diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs deleted file mode 100644 index b2dbbb345e..0000000000 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ /dev/null @@ -1,614 +0,0 @@ -using System; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Collections.Generic; -using System.Net.Mail; -using System.Security.Principal; -using System.Threading.Tasks; -using System.Web; -using System.Web.Http; -using System.Web.Mvc; -using Microsoft.AspNetCore.Identity; -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Models; -using Umbraco.Core.Services; -using Umbraco.Web.Models; -using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Mvc; -using Umbraco.Web.Security; -using Umbraco.Web.WebApi; -using Umbraco.Web.WebApi.Filters; -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.Web.Routing; -using Umbraco.Web.Editors.Filters; - -namespace Umbraco.Web.Editors -{ - /// - /// The API controller used for editing content - /// - [PluginController("UmbracoApi")] - [ValidationFilter] - [AngularJsonOnlyConfiguration] - [IsBackOffice] - public class AuthenticationController : UmbracoApiController - { - private BackOfficeUserManager _userManager; - private BackOfficeSignInManager _signInManager; - private readonly IUserPasswordConfiguration _passwordConfiguration; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IRuntimeState _runtimeState; - private readonly ISecuritySettings _securitySettings; - private readonly IEmailSender _emailSender; - - public AuthenticationController( - IUserPasswordConfiguration passwordConfiguration, - IGlobalSettings globalSettings, - IHostingEnvironment hostingEnvironment, - IUmbracoContextAccessor umbracoContextAccessor, - ISqlContext sqlContext, - ServiceContext services, - AppCaches appCaches, - IProfilingLogger logger, - IRuntimeState runtimeState, - UmbracoMapper umbracoMapper, - ISecuritySettings securitySettings, - IPublishedUrlProvider publishedUrlProvider, - IEmailSender emailSender) - : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoMapper, publishedUrlProvider) - { - _passwordConfiguration = passwordConfiguration ?? throw new ArgumentNullException(nameof(passwordConfiguration)); - _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); - _runtimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState)); - _securitySettings = securitySettings ?? throw new ArgumentNullException(nameof(securitySettings)); - _emailSender = emailSender; - } - - protected BackOfficeUserManager UserManager => _userManager - ?? (_userManager = TryGetOwinContext().Result.GetBackOfficeUserManager()); - - protected BackOfficeSignInManager SignInManager => _signInManager - ?? (_signInManager = TryGetOwinContext().Result.GetBackOfficeSignInManager()); - - /// - /// Returns the configuration for the backoffice user membership provider - used to configure the change password dialog - /// - /// - [WebApi.UmbracoAuthorize(requireApproval: false)] - public IDictionary GetPasswordConfig(int userId) - { - return _passwordConfiguration.GetConfiguration(userId != UmbracoContext.Security.CurrentUser.Id); - } - - - /// - /// Checks if a valid token is specified for an invited user and if so logs the user in and returns the user object - /// - /// - /// - /// - /// - /// This will also update the security stamp for the user so it can only be used once - /// - [ValidateAngularAntiForgeryToken] - [DenyLocalLoginAuthorization] - public async Task PostVerifyInvite([FromUri]int id, [FromUri]string token) - { - if (string.IsNullOrWhiteSpace(token)) - throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - var decoded = token.FromUrlBase64(); - if (decoded.IsNullOrWhiteSpace()) - throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - var identityUser = await UserManager.FindByIdAsync(id.ToString()); - if (identityUser == null) - throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - var result = await UserManager.ConfirmEmailAsync(identityUser, decoded); - - if (result.Succeeded == false) - { - throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse(result.Errors.ToErrorMessage())); - } - - Request.TryGetOwinContext().Result.Authentication.SignOut( - Core.Constants.Security.BackOfficeAuthenticationType, - Core.Constants.Security.BackOfficeExternalAuthenticationType); - - await SignInManager.SignInAsync(identityUser, false, false); - - var user = Services.UserService.GetUserById(id); - - return Mapper.Map(user); - } - - [WebApi.UmbracoAuthorize] - [ValidateAngularAntiForgeryToken] - public async Task PostUnLinkLogin(UnLinkLoginModel unlinkLoginModel) - { - var owinContext = TryGetOwinContext().Result; - ExternalSignInAutoLinkOptions autoLinkOptions = null; - var authType = owinContext.Authentication.GetExternalAuthenticationTypes().FirstOrDefault(x => x.AuthenticationType == unlinkLoginModel.LoginProvider); - if (authType == null) - { - Logger.Warn("Could not find external authentication provider registered: {LoginProvider}", unlinkLoginModel.LoginProvider); - } - else - { - autoLinkOptions = authType.GetExternalSignInAutoLinkOptions(); - if (!autoLinkOptions.AllowManualLinking) - { - // If AllowManualLinking is disabled for this provider we cannot unlink - return Request.CreateResponse(HttpStatusCode.BadRequest); - } - } - - var user = await UserManager.FindByIdAsync(User.Identity.GetUserId()); - if (user == null) throw new InvalidOperationException("Could not find user"); - - var result = await UserManager.RemoveLoginAsync( - user, - unlinkLoginModel.LoginProvider, - unlinkLoginModel.ProviderKey); - - if (result.Succeeded) - { - await SignInManager.SignInAsync(user, isPersistent: true, rememberBrowser: false); - return Request.CreateResponse(HttpStatusCode.OK); - } - else - { - AddModelErrors(result); - return Request.CreateValidationErrorResponse(ModelState); - } - } - - /// - /// Checks if the current user's cookie is valid and if so returns OK or a 400 (BadRequest) - /// - /// - [System.Web.Http.HttpGet] - public bool IsAuthenticated() - { - var attempt = UmbracoContext.Security.AuthorizeRequest(); - if (attempt == ValidateRequestAttempt.Success) - { - return true; - } - return false; - } - - /// - /// Returns the currently logged in Umbraco user - /// - /// - /// - /// We have the attribute [SetAngularAntiForgeryTokens] applied because this method is called initially to determine if the user - /// is valid before the login screen is displayed. The Auth cookie can be persisted for up to a day but the csrf cookies are only session - /// cookies which means that the auth cookie could be valid but the csrf cookies are no longer there, in that case we need to re-set the csrf cookies. - /// - [WebApi.UmbracoAuthorize] - [SetAngularAntiForgeryTokens] - [CheckIfUserTicketDataIsStale] - public UserDetail GetCurrentUser() - { - var user = UmbracoContext.Security.CurrentUser; - var result = Mapper.Map(user); - var httpContextAttempt = TryGetHttpContext(); - if (httpContextAttempt.Success) - { - //set their remaining seconds - result.SecondsUntilTimeout = httpContextAttempt.Result.GetRemainingAuthSeconds(); - } - - return result; - } - - /// - /// When a user is invited they are not approved but we need to resolve the partially logged on (non approved) - /// user. - /// - /// - /// - /// We cannot user GetCurrentUser since that requires they are approved, this is the same as GetCurrentUser but doesn't require them to be approved - /// - [WebApi.UmbracoAuthorize(requireApproval: false)] - [SetAngularAntiForgeryTokens] - public UserDetail GetCurrentInvitedUser() - { - var user = UmbracoContext.Security.CurrentUser; - - if (user.IsApproved) - { - // if they are approved, than they are no longer invited and we can return an error - throw new HttpResponseException(Request.CreateUserNoAccessResponse()); - } - - var result = Mapper.Map(user); - var httpContextAttempt = TryGetHttpContext(); - if (httpContextAttempt.Success) - { - // set their remaining seconds - result.SecondsUntilTimeout = httpContextAttempt.Result.GetRemainingAuthSeconds(); - } - - return result; - } - - // TODO: This should be on the CurrentUserController? - [WebApi.UmbracoAuthorize] - [ValidateAngularAntiForgeryToken] - public async Task> GetCurrentUserLinkedLogins() - { - var identityUser = await UserManager.FindByIdAsync(UmbracoContext.Security.GetUserId().ResultOr(0).ToString()); - var result = new Dictionary(); - foreach (var l in identityUser.Logins) - { - result[l.LoginProvider] = l.ProviderKey; - } - return result; - } - - /// - /// Logs a user in - /// - /// - [SetAngularAntiForgeryTokens] - [DenyLocalLoginAuthorization] - public async Task PostLogin(LoginModel loginModel) - { - var http = EnsureHttpContext(); - var owinContext = TryGetOwinContext().Result; - - // Sign the user in with username/password, this also gives a chance for developers to - // custom verify the credentials and auto-link user accounts with a custom IBackOfficePasswordChecker - var result = await SignInManager.PasswordSignInAsync( - loginModel.Username, loginModel.Password, isPersistent: true, shouldLockout: true); - - if (result.Succeeded) - { - // get the user - var user = Services.UserService.GetByUsername(loginModel.Username); - UserManager.RaiseLoginSuccessEvent(user.Id); - - return SetPrincipalAndReturnUserDetail(user, owinContext.Request.User); - } - - if (result.RequiresTwoFactor) - { - var twofactorOptions = UserManager as IUmbracoBackOfficeTwoFactorOptions; - if (twofactorOptions == null) - { - throw new HttpResponseException( - Request.CreateErrorResponse( - HttpStatusCode.BadRequest, - "UserManager does not implement " + typeof(IUmbracoBackOfficeTwoFactorOptions))); - } - - var twofactorView = twofactorOptions.GetTwoFactorView( - owinContext, - UmbracoContext, - loginModel.Username); - - if (twofactorView.IsNullOrWhiteSpace()) - { - throw new HttpResponseException( - Request.CreateErrorResponse( - HttpStatusCode.BadRequest, - typeof(IUmbracoBackOfficeTwoFactorOptions) + ".GetTwoFactorView returned an empty string")); - } - - var attemptedUser = Services.UserService.GetByUsername(loginModel.Username); - - // create a with information to display a custom two factor send code view - var verifyResponse = Request.CreateResponse(HttpStatusCode.PaymentRequired, new - { - twoFactorView = twofactorView, - userId = attemptedUser.Id - }); - - UserManager.RaiseLoginRequiresVerificationEvent(attemptedUser.Id); - - return verifyResponse; - } - - // return BadRequest (400), we don't want to return a 401 because that get's intercepted - // by our angular helper because it thinks that we need to re-perform the request once we are - // authorized and we don't want to return a 403 because angular will show a warning message indicating - // that the user doesn't have access to perform this function, we just want to return a normal invalid message. - throw new HttpResponseException(HttpStatusCode.BadRequest); - } - - /// - /// Processes a password reset request. Looks for a match on the provided email address - /// and if found sends an email with a link to reset it - /// - /// - [SetAngularAntiForgeryTokens] - [DenyLocalLoginAuthorization] - public async Task PostRequestPasswordReset(RequestPasswordResetModel model) - { - // If this feature is switched off in configuration the UI will be amended to not make the request to reset password available. - // So this is just a server-side secondary check. - if (_securitySettings.AllowPasswordReset == false) - { - throw new HttpResponseException(HttpStatusCode.BadRequest); - } - var identityUser = await UserManager.FindByEmailAsync(model.Email); - if (identityUser != null) - { - var user = Services.UserService.GetByEmail(model.Email); - if (user != null) - { - var code = await UserManager.GeneratePasswordResetTokenAsync(identityUser); - var callbackUrl = ConstructCallbackUrl(identityUser.Id, code); - - var message = Services.TextService.Localize("resetPasswordEmailCopyFormat", - // Ensure the culture of the found user is used for the email! - UmbracoUserExtensions.GetUserCulture(identityUser.Culture, Services.TextService, GlobalSettings), - new[] { identityUser.UserName, callbackUrl }); - - var subject = Services.TextService.Localize("login/resetPasswordEmailCopySubject", - // Ensure the culture of the found user is used for the email! - UmbracoUserExtensions.GetUserCulture(identityUser.Culture, Services.TextService, GlobalSettings)); - - var mailMessage = new MailMessage() - { - Subject = subject, - Body = message, - IsBodyHtml = true, - To = { user.Email} - }; - - await _emailSender.SendAsync(mailMessage); - - UserManager.RaiseForgotPasswordRequestedEvent(user.Id); - } - } - - return Request.CreateResponse(HttpStatusCode.OK); - } - - /// - /// Used to retrieve the 2FA providers for code submission - /// - /// - [SetAngularAntiForgeryTokens] - public async Task> Get2FAProviders() - { - var userId = await SignInManager.GetVerifiedUserIdAsync(); - if (string.IsNullOrWhiteSpace(userId)) - { - Logger.Warn("Get2FAProviders :: No verified user found, returning 404"); - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var user = await UserManager.FindByIdAsync(userId); - var userFactors = await UserManager.GetValidTwoFactorProvidersAsync(user); - - return userFactors; - } - - [SetAngularAntiForgeryTokens] - public async Task PostSend2FACode([FromBody]string provider) - { - if (provider.IsNullOrWhiteSpace()) - throw new HttpResponseException(HttpStatusCode.NotFound); - - var userId = await SignInManager.GetVerifiedUserIdAsync(); - if (string.IsNullOrWhiteSpace(userId)) - { - Logger.Warn("Get2FAProviders :: No verified user found, returning 404"); - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - // Generate the token and send it - if (await SignInManager.SendTwoFactorCodeAsync(provider) == false) - { - return BadRequest("Invalid code"); - } - return Ok(); - } - - [SetAngularAntiForgeryTokens] - public async Task PostVerify2FACode(Verify2FACodeModel model) - { - if (ModelState.IsValid == false) - { - return Request.CreateValidationErrorResponse(ModelState); - } - - var userName = await SignInManager.GetVerifiedUserNameAsync(); - if (userName == null) - { - Logger.Warn("Get2FAProviders :: No verified user found, returning 404"); - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var result = await SignInManager.TwoFactorSignInAsync(model.Provider, model.Code, isPersistent: true, rememberBrowser: false); - var owinContext = TryGetOwinContext().Result; - - var user = Services.UserService.GetByUsername(userName); - if (result.Succeeded) - { - UserManager.RaiseLoginSuccessEvent(user.Id); - return SetPrincipalAndReturnUserDetail(user, owinContext.Request.User); - } - - if (result.IsLockedOut) - { - UserManager.RaiseAccountLockedEvent(user.Id); - return Request.CreateValidationErrorResponse("User is locked out"); - } - - return Request.CreateValidationErrorResponse("Invalid code"); - } - - /// - /// Processes a set password request. Validates the request and sets a new password. - /// - /// - [SetAngularAntiForgeryTokens] - public async Task PostSetPassword(SetPasswordModel model) - { - var identityUser = await UserManager.FindByIdAsync(model.UserId.ToString()); - - var result = await UserManager.ResetPasswordAsync(identityUser, model.ResetCode, model.Password); - if (result.Succeeded) - { - var lockedOut = await UserManager.IsLockedOutAsync(identityUser); - if (lockedOut) - { - Logger.Info("User {UserId} is currently locked out, unlocking and resetting AccessFailedCount", model.UserId); - - //// var user = await UserManager.FindByIdAsync(model.UserId); - var unlockResult = await UserManager.SetLockoutEndDateAsync(identityUser, DateTimeOffset.Now); - if (unlockResult.Succeeded == false) - { - Logger.Warn("Could not unlock for user {UserId} - error {UnlockError}", model.UserId, unlockResult.Errors.First().Description); - } - - var resetAccessFailedCountResult = await UserManager.ResetAccessFailedCountAsync(identityUser); - if (resetAccessFailedCountResult.Succeeded == false) - { - Logger.Warn("Could not reset access failed count {UserId} - error {UnlockError}", model.UserId, unlockResult.Errors.First().Description); - } - } - - // They've successfully set their password, we can now update their user account to be confirmed - // if user was only invited, then they have not been approved - // but a successful forgot password flow (e.g. if their token had expired and they did a forgot password instead of request new invite) - // means we have verified their email - if (!await UserManager.IsEmailConfirmedAsync(identityUser)) - { - await UserManager.ConfirmEmailAsync(identityUser, model.ResetCode); - } - - // invited is not approved, never logged in, invited date present - /* - if (LastLoginDate == default && IsApproved == false && InvitedDate != null) - return UserState.Invited; - */ - if (identityUser != null && !identityUser.IsApproved) - { - var user = Services.UserService.GetByUsername(identityUser.UserName); - // also check InvitedDate and never logged in, otherwise this would allow a disabled user to reactivate their account with a forgot password - if (user.LastLoginDate == default && user.InvitedDate != null) - { - user.IsApproved = true; - user.InvitedDate = null; - Services.UserService.Save(user); - } - } - - UserManager.RaiseForgotPasswordChangedSuccessEvent(model.UserId); - return Request.CreateResponse(HttpStatusCode.OK); - } - return Request.CreateValidationErrorResponse( - result.Errors.Any() ? result.Errors.First().Description : "Set password failed"); - } - - - /// - /// Logs the current user out - /// - /// - [ClearAngularAntiForgeryToken] - [ValidateAngularAntiForgeryToken] - public HttpResponseMessage PostLogout() - { - var owinContext = Request.TryGetOwinContext().Result; - - owinContext.Authentication.SignOut( - Core.Constants.Security.BackOfficeAuthenticationType, - Core.Constants.Security.BackOfficeExternalAuthenticationType); - - Logger.Info("User {UserName} from IP address {RemoteIpAddress} has logged out", User.Identity == null ? "UNKNOWN" : User.Identity.Name, owinContext.Request.RemoteIpAddress); - - if (UserManager != null) - { - int.TryParse(User.Identity.GetUserId(), out var userId); - var args = UserManager.RaiseLogoutSuccessEvent(userId); - if (!args.SignOutRedirectUrl.IsNullOrWhiteSpace()) - return Request.CreateResponse(new - { - signOutRedirectUrl = args.SignOutRedirectUrl - }); - } - - return Request.CreateResponse(HttpStatusCode.OK); - } - - /// - /// This is used when the user is auth'd successfully and we need to return an OK with user details along with setting the current Principal in the request - /// - /// - /// - /// - private HttpResponseMessage SetPrincipalAndReturnUserDetail(IUser user, IPrincipal principal) - { - if (user == null) throw new ArgumentNullException("user"); - if (principal == null) throw new ArgumentNullException(nameof(principal)); - - var userDetail = Mapper.Map(user); - // update the userDetail and set their remaining seconds - userDetail.SecondsUntilTimeout = TimeSpan.FromMinutes(GlobalSettings.TimeOutInMinutes).TotalSeconds; - - // create a response with the userDetail object - var response = Request.CreateResponse(HttpStatusCode.OK, userDetail); - - // ensure the user is set for the current request - Request.SetPrincipalForRequest(principal); - - return response; - } - - private string ConstructCallbackUrl(int userId, string code) - { - // Get an mvc helper to get the url - var http = EnsureHttpContext(); - var urlHelper = new UrlHelper(http.Request.RequestContext); - var action = urlHelper.Action("ValidatePasswordResetCode", "BackOffice", - new - { - area = GlobalSettings.GetUmbracoMvcArea(_hostingEnvironment), - u = userId, - r = code - }); - - // Construct full URL using configured application URL (which will fall back to request) - var applicationUri = _runtimeState.ApplicationUrl; - var callbackUri = new Uri(applicationUri, action); - return callbackUri.ToString(); - } - - - private HttpContextBase EnsureHttpContext() - { - var attempt = this.TryGetHttpContext(); - if (attempt.Success == false) - throw new InvalidOperationException("This method requires that an HttpContext be active"); - return attempt.Result; - } - - - - private void AddModelErrors(IdentityResult result, string prefix = "") - { - foreach (var error in result.Errors) - { - ModelState.AddModelError(prefix, error.Description); - } - } - } -} diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs deleted file mode 100644 index 49dc6d8b58..0000000000 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ /dev/null @@ -1,605 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using System.Web.Mvc; -using System.Web.UI; -using Microsoft.AspNetCore.Identity; -using Microsoft.Owin.Security; -using Newtonsoft.Json; -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Core.Manifest; -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; -using Umbraco.Core.Configuration.Grid; -using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.Hosting; -using Umbraco.Core.IO; -using Umbraco.Core.Runtime; -using Umbraco.Core.WebAssets; -using Umbraco.Web.Trees; -using Umbraco.Web.WebAssets; - -namespace Umbraco.Web.Editors -{ - - /// - /// Represents a controller user to render out the default back office view and JS results. - /// - [UmbracoRequireHttps] - [DisableBrowserCache] - public class BackOfficeController : UmbracoController - { - private readonly UmbracoFeatures _features; - private readonly IRuntimeState _runtimeState; - private BackOfficeUserManager _userManager; - private BackOfficeSignInManager _signInManager; - private readonly IUmbracoVersion _umbracoVersion; - private readonly IGridConfig _gridConfig; - private readonly IContentSettings _contentSettings; - private readonly TreeCollection _treeCollection; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IRuntimeSettings _runtimeSettings; - private readonly ISecuritySettings _securitySettings; - private readonly IRuntimeMinifier _runtimeMinifier; - private readonly IIconService _iconService; - - public BackOfficeController( - UmbracoFeatures features, - IGlobalSettings globalSettings, - IUmbracoContextAccessor umbracoContextAccessor, - ServiceContext services, - AppCaches appCaches, - IProfilingLogger profilingLogger, - IRuntimeState runtimeState, - IUmbracoVersion umbracoVersion, - IGridConfig gridConfig, - IContentSettings contentSettings, - TreeCollection treeCollection, - IHostingEnvironment hostingEnvironment, - IHttpContextAccessor httpContextAccessor, - IRuntimeSettings settings, - ISecuritySettings securitySettings, - IRuntimeMinifier runtimeMinifier, - IIconService iconService) - : base(globalSettings, umbracoContextAccessor, services, appCaches, profilingLogger) - - { - _features = features; - _runtimeState = runtimeState; - _umbracoVersion = umbracoVersion; - _gridConfig = gridConfig ?? throw new ArgumentNullException(nameof(gridConfig)); - _contentSettings = contentSettings ?? throw new ArgumentNullException(nameof(contentSettings)); - _treeCollection = treeCollection ?? throw new ArgumentNullException(nameof(treeCollection)); - _hostingEnvironment = hostingEnvironment; - _httpContextAccessor = httpContextAccessor; - _runtimeSettings = settings; - _securitySettings = securitySettings; - _runtimeMinifier = runtimeMinifier; - _iconService = iconService; - } - - protected BackOfficeSignInManager SignInManager => _signInManager ?? (_signInManager = OwinContext.GetBackOfficeSignInManager()); - - protected BackOfficeUserManager UserManager => _userManager ?? (_userManager = OwinContext.GetBackOfficeUserManager()); - - protected IAuthenticationManager AuthenticationManager => OwinContext.Authentication; - - /// - /// Render the default view - /// - /// - public async Task Default() - { - var backofficeModel = new BackOfficeModel(_features, GlobalSettings, _umbracoVersion, _contentSettings, _treeCollection, _httpContextAccessor, _hostingEnvironment, _runtimeSettings, _securitySettings, _iconService); - return await RenderDefaultOrProcessExternalLoginAsync( - () => - View(GlobalSettings.GetBackOfficePath(_hostingEnvironment).EnsureEndsWith('/') + "Views/Default.cshtml", backofficeModel), - () => - View(GlobalSettings.GetBackOfficePath(_hostingEnvironment).EnsureEndsWith('/') + "Views/Default.cshtml", backofficeModel) - ); - } - - [HttpGet] - public async Task VerifyInvite(string invite) - { - //if you are hitting VerifyInvite, you're already signed in as a different user, and the token is invalid - //you'll exit on one of the return RedirectToAction("Default") but you're still logged in so you just get - //dumped at the default admin view with no detail - if (Security.IsAuthenticated()) - { - AuthenticationManager.SignOut( - Core.Constants.Security.BackOfficeAuthenticationType, - Core.Constants.Security.BackOfficeExternalAuthenticationType); - } - - if (invite == null) - { - Logger.Warn("VerifyUser endpoint reached with invalid token: NULL"); - return RedirectToAction("Default"); - } - - var parts = Server.UrlDecode(invite).Split('|'); - - if (parts.Length != 2) - { - Logger.Warn("VerifyUser endpoint reached with invalid token: {Invite}", invite); - return RedirectToAction("Default"); - } - - var token = parts[1]; - - var decoded = token.FromUrlBase64(); - if (decoded.IsNullOrWhiteSpace()) - { - Logger.Warn("VerifyUser endpoint reached with invalid token: {Invite}", invite); - return RedirectToAction("Default"); - } - - var id = parts[0]; - - var identityUser = await UserManager.FindByIdAsync(id); - if (identityUser == null) - { - Logger.Warn("VerifyUser endpoint reached with non existing user: {UserId}", id); - return RedirectToAction("Default"); - } - - var result = await UserManager.ConfirmEmailAsync(identityUser, decoded); - - if (result.Succeeded == false) - { - Logger.Warn("Could not verify email, Error: {Errors}, Token: {Invite}", result.Errors.ToErrorMessage(), invite); - return new RedirectResult(Url.Action("Default") + "#/login/false?invite=3"); - } - - //sign the user in - DateTime? previousLastLoginDate = identityUser.LastLoginDateUtc; - await SignInManager.SignInAsync(identityUser, false, false); - //reset the lastlogindate back to previous as the user hasn't actually logged in, to add a flag or similar to SignInManager would be a breaking change - identityUser.LastLoginDateUtc = previousLastLoginDate; - await UserManager.UpdateAsync(identityUser); - - return new RedirectResult(Url.Action("Default") + "#/login/false?invite=1"); - } - - /// - /// This Action is used by the installer when an upgrade is detected but the admin user is not logged in. We need to - /// ensure the user is authenticated before the install takes place so we redirect here to show the standard login screen. - /// - /// - [HttpGet] - [StatusCodeResult(System.Net.HttpStatusCode.ServiceUnavailable)] - public async Task AuthorizeUpgrade() - { - return await RenderDefaultOrProcessExternalLoginAsync( - //The default view to render when there is no external login info or errors - () => View(GlobalSettings.GetBackOfficePath(_hostingEnvironment).EnsureEndsWith('/') + "Views/AuthorizeUpgrade.cshtml", new BackOfficeModel(_features, GlobalSettings, _umbracoVersion, _contentSettings, _treeCollection, _httpContextAccessor, _hostingEnvironment, _runtimeSettings, _securitySettings, _iconService)), - //The ActionResult to perform if external login is successful - () => Redirect("/")); - } - - /// - /// Get the json localized text for a given culture or the culture for the current user - /// - /// - /// - [HttpGet] - public JsonNetResult LocalizedText(string culture = null) - { - var cultureInfo = string.IsNullOrWhiteSpace(culture) - //if the user is logged in, get their culture, otherwise default to 'en' - ? Security.IsAuthenticated() - //current culture is set at the very beginning of each request - ? Thread.CurrentThread.CurrentCulture - : CultureInfo.GetCultureInfo(GlobalSettings.DefaultUILanguage) - : CultureInfo.GetCultureInfo(culture); - - var allValues = Services.TextService.GetAllStoredValues(cultureInfo); - var pathedValues = allValues.Select(kv => - { - var slashIndex = kv.Key.IndexOf('/'); - var areaAlias = kv.Key.Substring(0, slashIndex); - var valueAlias = kv.Key.Substring(slashIndex + 1); - return new - { - areaAlias, - valueAlias, - value = kv.Value - }; - }); - - Dictionary> nestedDictionary = pathedValues - .GroupBy(pv => pv.areaAlias) - .ToDictionary(pv => pv.Key, pv => - pv.ToDictionary(pve => pve.valueAlias, pve => pve.value)); - - return new JsonNetResult { Data = nestedDictionary, Formatting = Formatting.None }; - } - - /// - /// Returns the JavaScript main file including all references found in manifests - /// - /// - [MinifyJavaScriptResult(Order = 0)] - [OutputCache(Order = 1, VaryByParam = "none", Location = OutputCacheLocation.Server, Duration = 5000)] - public async Task Application() - { - var result = await _runtimeMinifier.GetScriptForLoadingBackOfficeAsync(GlobalSettings, _hostingEnvironment); - - return JavaScript(result); - } - - [UmbracoAuthorize(Order = 0)] - [HttpGet] - public JsonNetResult GetGridConfig() - { - return new JsonNetResult { Data = _gridConfig.EditorsConfig.Editors, Formatting = Formatting.None }; - } - - - - /// - /// Returns the JavaScript object representing the static server variables javascript object - /// - /// - [UmbracoAuthorize(Order = 0)] - [MinifyJavaScriptResult(Order = 1)] - public JavaScriptResult ServerVariables() - { - var serverVars = new BackOfficeServerVariables(Url, _runtimeState, _features, GlobalSettings, _umbracoVersion, _contentSettings, _treeCollection, _httpContextAccessor, _hostingEnvironment, _runtimeSettings, _securitySettings, _runtimeMinifier); - - //cache the result if debugging is disabled - var result = _hostingEnvironment.IsDebugMode - ? ServerVariablesParser.Parse(serverVars.GetServerVariables()) - : AppCaches.RuntimeCache.GetCacheItem( - typeof(BackOfficeController) + "ServerVariables", - () => ServerVariablesParser.Parse(serverVars.GetServerVariables()), - new TimeSpan(0, 10, 0)); - - return JavaScript(result); - } - - - - [HttpPost] - public ActionResult ExternalLogin(string provider, string redirectUrl = null) - { - if (redirectUrl == null) - { - redirectUrl = Url.Action("Default", "BackOffice"); - } - - // Request a redirect to the external login provider - return new ChallengeResult(provider, redirectUrl); - } - - [UmbracoAuthorize] - [HttpPost] - public ActionResult LinkLogin(string provider) - { - // Request a redirect to the external login provider to link a login for the current user - return new ChallengeResult(provider, - Url.Action("ExternalLinkLoginCallback", "BackOffice"), - User.Identity.GetUserId()); - } - - [HttpGet] - public async Task ValidatePasswordResetCode([Bind(Prefix = "u")] int userId, [Bind(Prefix = "r")] string resetCode) - { - var user = await UserManager.FindByIdAsync(userId.ToString()); - if (user != null) - { - var result = await UserManager.VerifyUserTokenAsync(user, "ResetPassword", "ResetPassword", resetCode); - if (result) - { - //Add a flag and redirect for it to be displayed - TempData[ViewDataExtensions.TokenPasswordResetCode] = new ValidatePasswordResetCodeModel { UserId = userId, ResetCode = resetCode }; - return RedirectToLocal(Url.Action("Default", "BackOffice")); - } - } - - //Add error and redirect for it to be displayed - TempData[ViewDataExtensions.TokenPasswordResetCode] = new[] { Services.TextService.Localize("login/resetCodeExpired") }; - return RedirectToLocal(Url.Action("Default", "BackOffice")); - } - - [HttpGet] - public async Task ExternalLinkLoginCallback() - { - var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync( - Constants.Security.BackOfficeExternalAuthenticationType, - XsrfKey, User.Identity.GetUserId()); - - if (loginInfo == null) - { - //Add error and redirect for it to be displayed - TempData[ViewDataExtensions.TokenExternalSignInError] = new[] { "An error occurred, could not get external login info" }; - return RedirectToLocal(Url.Action("Default", "BackOffice")); - } - - var user = await UserManager.FindByIdAsync(User.Identity.GetUserId()); - if (user == null) throw new InvalidOperationException("Could not find user"); - - var result = await UserManager.AddLoginAsync(user, - new UserLoginInfo(loginInfo.Login.LoginProvider, loginInfo.Login.ProviderKey, loginInfo.Login.LoginProvider)); - if (result.Succeeded) - { - return RedirectToLocal(Url.Action("Default", "BackOffice")); - } - - //Add errors and redirect for it to be displayed - TempData[ViewDataExtensions.TokenExternalSignInError] = result.Errors; - return RedirectToLocal(Url.Action("Default", "BackOffice")); - } - - /// - /// Used by Default and AuthorizeUpgrade to render as per normal if there's no external login info, - /// otherwise process the external login info. - /// - /// - private async Task RenderDefaultOrProcessExternalLoginAsync( - Func defaultResponse, - Func externalSignInResponse) - { - if (defaultResponse == null) throw new ArgumentNullException("defaultResponse"); - if (externalSignInResponse == null) throw new ArgumentNullException("externalSignInResponse"); - - ViewData.SetUmbracoPath(GlobalSettings.GetUmbracoMvcArea(_hostingEnvironment)); - - //check if there is the TempData or cookies with the any token name specified, if so, assign to view bag and render the view - if (ViewData.FromBase64CookieData(HttpContext, ViewDataExtensions.TokenExternalSignInError) || - ViewData.FromTempData(TempData, ViewDataExtensions.TokenExternalSignInError) || - ViewData.FromTempData(TempData, ViewDataExtensions.TokenPasswordResetCode)) - return defaultResponse(); - - //First check if there's external login info, if there's not proceed as normal - var loginInfo = await OwinContext.Authentication.GetExternalLoginInfoAsync( - Constants.Security.BackOfficeExternalAuthenticationType); - - if (loginInfo == null || loginInfo.ExternalIdentity.IsAuthenticated == false) - { - - // if the user is not logged in, check if there's any auto login redirects specified - if (UmbracoContext.Security.ValidateCurrentUser(false) != ValidateRequestAttempt.Success) - { - var oauthRedirectAuthProvider = OwinContext.Authentication.GetAutoLoginProvider(); - if (!oauthRedirectAuthProvider.IsNullOrWhiteSpace()) - { - return ExternalLogin(oauthRedirectAuthProvider); - } - } - - return defaultResponse(); - } - - //we're just logging in with an external source, not linking accounts - return await ExternalSignInAsync(loginInfo, externalSignInResponse); - } - - private async Task ExternalSignInAsync(ExternalLoginInfo loginInfo, Func response) - { - if (loginInfo == null) throw new ArgumentNullException("loginInfo"); - if (response == null) throw new ArgumentNullException("response"); - ExternalSignInAutoLinkOptions autoLinkOptions = null; - - // Here we can check if the provider associated with the request has been configured to allow - // new users (auto-linked external accounts). This would never be used with public providers such as - // Google, unless you for some reason wanted anybody to be able to access the backend if they have a Google account - // .... not likely! - var authType = OwinContext.Authentication.GetExternalAuthenticationTypes().FirstOrDefault(x => x.AuthenticationType == loginInfo.Login.LoginProvider); - if (authType == null) - { - Logger.Warn("Could not find external authentication provider registered: {LoginProvider}", loginInfo.Login.LoginProvider); - } - else - { - autoLinkOptions = authType.GetExternalSignInAutoLinkOptions(); - } - - // Sign in the user with this external login provider if the user already has a login - var user = await UserManager.FindByLoginAsync(loginInfo.Login.LoginProvider, loginInfo.Login.ProviderKey); - if (user != null) - { - var shouldSignIn = true; - if (autoLinkOptions != null && autoLinkOptions.OnExternalLogin != null) - { - shouldSignIn = autoLinkOptions.OnExternalLogin(user, loginInfo); - if (shouldSignIn == false) - { - Logger.Warn("The AutoLinkOptions of the external authentication provider '{LoginProvider}' have refused the login based on the OnExternalLogin method. Affected user id: '{UserId}'", loginInfo.Login.LoginProvider, user.Id); - } - } - - if (shouldSignIn) - { - //sign in - await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false); - } - } - else - { - if (await AutoLinkAndSignInExternalAccount(loginInfo, autoLinkOptions) == false) - { - ViewData.SetExternalSignInProviderErrors( - new BackOfficeExternalLoginProviderErrors( - loginInfo.Login.LoginProvider, - new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not been linked to an account" })); - } - - //Remove the cookie otherwise this message will keep appearing - if (Response.Cookies[Constants.Security.BackOfficeExternalCookieName] != null) - { - Response.Cookies[Constants.Security.BackOfficeExternalCookieName].Expires = DateTime.MinValue; - } - } - - return response(); - } - - private async Task AutoLinkAndSignInExternalAccount(ExternalLoginInfo loginInfo, ExternalSignInAutoLinkOptions autoLinkOptions) - { - if (autoLinkOptions == null) - return false; - - if (autoLinkOptions.ShouldAutoLinkExternalAccount(UmbracoContext, loginInfo) == false) - return true; - - //we are allowing auto-linking/creating of local accounts - if (loginInfo.Email.IsNullOrWhiteSpace()) - { - ViewData.SetExternalSignInProviderErrors( - new BackOfficeExternalLoginProviderErrors( - loginInfo.Login.LoginProvider, - new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not provided an email address, the account cannot be linked." })); - } - else - { - //Now we need to perform the auto-link, so first we need to lookup/create a user with the email address - var autoLinkUser = await UserManager.FindByEmailAsync(loginInfo.Email); - if (autoLinkUser != null) - { - try - { - //call the callback if one is assigned - autoLinkOptions.OnAutoLinking?.Invoke(autoLinkUser, loginInfo); - } - catch (Exception ex) - { - var msg = "Could not link login provider " + loginInfo.Login.LoginProvider + "."; - Logger.Error(ex, msg); - ViewData.SetExternalSignInProviderErrors( - new BackOfficeExternalLoginProviderErrors( - loginInfo.Login.LoginProvider, - new[] { msg + " " + ex.Message })); - return true; - } - - await LinkUser(autoLinkUser, loginInfo); - } - else - { - if (loginInfo.Email.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Email value cannot be null"); - if (loginInfo.ExternalIdentity.Name.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Name value cannot be null"); - - var groups = Services.UserService.GetUserGroupsByAlias(autoLinkOptions.GetDefaultUserGroups(UmbracoContext, loginInfo)); - - autoLinkUser = BackOfficeIdentityUser.CreateNew( - GlobalSettings, - loginInfo.Email, - loginInfo.Email, - autoLinkOptions.GetDefaultCulture(UmbracoContext, loginInfo)); - autoLinkUser.Name = loginInfo.ExternalIdentity.Name; - foreach (var userGroup in groups) - { - autoLinkUser.AddRole(userGroup.Alias); - } - - //call the callback if one is assigned - try - { - autoLinkOptions.OnAutoLinking?.Invoke(autoLinkUser, loginInfo); - } - catch (Exception ex) - { - var msg = "Could not link login provider " + loginInfo.Login.LoginProvider + "."; - Logger.Error(ex, msg); - ViewData.SetExternalSignInProviderErrors( - new BackOfficeExternalLoginProviderErrors( - loginInfo.Login.LoginProvider, - new[] { msg + " " + ex.Message })); - return true; - } - - var userCreationResult = await UserManager.CreateAsync(autoLinkUser); - - if (userCreationResult.Succeeded == false) - { - ViewData.SetExternalSignInProviderErrors( - new BackOfficeExternalLoginProviderErrors( - loginInfo.Login.LoginProvider, - userCreationResult.Errors.Select(x => x.Description).ToList())); - } - else - { - await LinkUser(autoLinkUser, loginInfo); - } - } - - } - return true; - } - - private async Task LinkUser(BackOfficeIdentityUser autoLinkUser, ExternalLoginInfo loginInfo) - { - var identityUser = await UserManager.FindByIdAsync(autoLinkUser.Id.ToString()); - - UserLoginInfo exists = null; - if (!(identityUser is null)) - { - var existingLogins = await UserManager.GetLoginsAsync(identityUser); - exists = existingLogins.FirstOrDefault(x => x.LoginProvider == loginInfo.Login.LoginProvider && x.ProviderKey == loginInfo.Login.ProviderKey); - } - // if it already exists (perhaps it was added in the AutoLink callbak) then we just continue - if (exists != null) - { - //sign in - await SignInManager.SignInAsync(autoLinkUser, isPersistent: false, rememberBrowser: false); - return; - } - - var linkResult = await UserManager.AddLoginAsync(autoLinkUser, - new UserLoginInfo(loginInfo.Login.LoginProvider, loginInfo.Login.ProviderKey, loginInfo.Login.LoginProvider)); - if (linkResult.Succeeded) - { - //we're good! sign in - await SignInManager.SignInAsync(autoLinkUser, isPersistent: false, rememberBrowser: false); - return; - } - - ViewData.SetExternalSignInProviderErrors( - new BackOfficeExternalLoginProviderErrors( - loginInfo.Login.LoginProvider, - linkResult.Errors.Select(x => x.Description).ToList())); - - //If this fails, we should really delete the user since it will be in an inconsistent state! - var deleteResult = await UserManager.DeleteAsync(autoLinkUser); - if (!deleteResult.Succeeded) - { - //DOH! ... this isn't good, combine all errors to be shown - ViewData.SetExternalSignInProviderErrors( - new BackOfficeExternalLoginProviderErrors( - loginInfo.Login.LoginProvider, - linkResult.Errors.Concat(deleteResult.Errors).Select(x => x.Description).ToList())); - } - } - - private ActionResult RedirectToLocal(string returnUrl) - { - if (Url.IsLocalUrl(returnUrl)) - { - return Redirect(returnUrl); - } - return Redirect("/"); - } - - // Used for XSRF protection when adding external logins - public const string XsrfKey = "XsrfId"; - - } - -} diff --git a/src/Umbraco.Web/Editors/ChallengeResult.cs b/src/Umbraco.Web/Editors/ChallengeResult.cs deleted file mode 100644 index 00c0aa187d..0000000000 --- a/src/Umbraco.Web/Editors/ChallengeResult.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Linq; -using System.Web; -using System.Web.Mvc; -using Microsoft.Owin.Security; -using Umbraco.Core; -using Umbraco.Web.Security; - -namespace Umbraco.Web.Editors -{ - public class ChallengeResult : HttpUnauthorizedResult - { - public ChallengeResult(string provider, string redirectUri, string userId = null) - { - LoginProvider = provider; - RedirectUri = redirectUri; - UserId = userId; - } - - private string LoginProvider { get; set; } - private string RedirectUri { get; set; } - private string UserId { get; set; } - - public override void ExecuteResult(ControllerContext context) - { - //Ensure the forms auth module doesn't do a redirect! - context.HttpContext.Response.SuppressFormsAuthenticationRedirect = true; - - var owinCtx = context.HttpContext.GetOwinContext(); - - //First, see if a custom challenge result callback is specified for the provider - // and use it instead of the default if one is supplied. - var loginProvider = owinCtx.Authentication - .GetExternalAuthenticationTypes() - .FirstOrDefault(p => p.AuthenticationType == LoginProvider); - if (loginProvider != null) - { - var providerChallengeResult = loginProvider.GetSignInChallengeResult(owinCtx); - if (providerChallengeResult != null) - { - owinCtx.Authentication.Challenge(providerChallengeResult, LoginProvider); - return; - } - } - - var properties = new AuthenticationProperties() { RedirectUri = RedirectUri.EnsureEndsWith('/') }; - if (UserId != null) - { - properties.Dictionary[BackOfficeController.XsrfKey] = UserId; - } - owinCtx.Authentication.Challenge(properties, LoginProvider); - } - } - -} diff --git a/src/Umbraco.Web/Editors/Filters/IsCurrentUserModelFilterAttribute.cs b/src/Umbraco.Web/Editors/Filters/IsCurrentUserModelFilterAttribute.cs deleted file mode 100644 index 61122e2b6e..0000000000 --- a/src/Umbraco.Web/Editors/Filters/IsCurrentUserModelFilterAttribute.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Collections.Generic; -using System.Net.Http; -using System.Web.Http.Filters; -using Umbraco.Web.Composing; -using Umbraco.Web.Models.ContentEditing; - -namespace Umbraco.Web.Editors.Filters -{ - /// - /// This sets the IsCurrentUser property on any outgoing model or any collection of models - /// - internal class IsCurrentUserModelFilterAttribute : ActionFilterAttribute - { - public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) - { - if (actionExecutedContext.Response == null) return; - - var user = Current.UmbracoContext.Security.CurrentUser; - if (user == null) return; - - var objectContent = actionExecutedContext.Response.Content as ObjectContent; - if (objectContent != null) - { - var model = objectContent.Value as UserBasic; - if (model != null && model.Id is int userId) - { - model.IsCurrentUser = userId == user.Id; - } - else - { - var collection = objectContent.Value as IEnumerable; - if (collection != null) - { - foreach (var userBasic in collection) - { - if (userBasic.Id is int uid) - { - userBasic.IsCurrentUser = uid == user.Id; - } - } - } - else - { - var paged = objectContent.Value as UsersController.PagedUserResult; - if (paged != null && paged.Items != null) - { - foreach (var userBasic in paged.Items) - { - if (userBasic.Id is int uid) - { - userBasic.IsCurrentUser = uid == user.Id; - } - } - } - } - } - } - - base.OnActionExecuted(actionExecutedContext); - } - } -} diff --git a/src/Umbraco.Web/OwinExtensions.cs b/src/Umbraco.Web/OwinExtensions.cs index 61b6f24f2e..62f1643074 100644 --- a/src/Umbraco.Web/OwinExtensions.cs +++ b/src/Umbraco.Web/OwinExtensions.cs @@ -9,21 +9,6 @@ namespace Umbraco.Web { public static class OwinExtensions { - /// - /// Used by external login providers to set any errors that occur during the OAuth negotiation - /// - /// - /// - public static void SetExternalLoginProviderErrors(this IOwinContext owinContext, BackOfficeExternalLoginProviderErrors errors) - => owinContext.Set(errors); - - /// - /// Retrieve any errors set by external login providers during OAuth negotiation - /// - /// - /// - internal static BackOfficeExternalLoginProviderErrors GetExternalLoginProviderErrors(this IOwinContext owinContext) - => owinContext.Get(); /// /// Gets the for the Umbraco back office cookie @@ -67,16 +52,6 @@ namespace Umbraco.Web return ctx == null ? Attempt.Fail() : Attempt.Succeed(ctx); } - /// - /// Gets the back office sign in manager out of OWIN - /// - /// - /// - public static BackOfficeSignInManager GetBackOfficeSignInManager(this IOwinContext owinContext) - { - return owinContext.Get() - ?? throw new NullReferenceException($"Could not resolve an instance of {typeof (BackOfficeSignInManager)} from the {typeof(IOwinContext)}."); - } /// /// Gets the back office user manager out of OWIN diff --git a/src/Umbraco.Web/Security/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/AppBuilderExtensions.cs index b4e9df84f4..aa5ff7a4e1 100644 --- a/src/Umbraco.Web/Security/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/AppBuilderExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Threading; using Microsoft.Owin; +using Microsoft.Owin.Extensions; using Microsoft.Owin.Security; using Microsoft.Owin.Security.Cookies; using Microsoft.Owin.Security.DataProtection; @@ -21,6 +22,8 @@ namespace Umbraco.Web.Security public static class AppBuilderExtensions { + // TODO: Migrate this! + /// /// Ensures that the cookie middleware for validating external logins is assigned to the pipeline with the correct /// Umbraco back office configuration @@ -72,22 +75,6 @@ namespace Umbraco.Web.Security CookieDomain = new SecuritySettings().AuthCookieDomain // TODO inject settings }, stage); - return app; - } - - app.UseStageMarker(stage); - return app; - } - - /// - /// Enable the back office to detect and handle errors registered with external login providers - /// - /// - /// - /// - public static IAppBuilder UseUmbracoBackOfficeExternalLoginErrors(this IAppBuilder app, PipelineStage stage = PipelineStage.Authorize) - { - app.Use(typeof(BackOfficeExternalLoginProviderErrorMiddlware)); app.UseStageMarker(stage); return app; } @@ -97,41 +84,5 @@ namespace Umbraco.Web.Security Thread.CurrentThread.SanitizeThreadCulture(); } - public static IAppBuilder CreatePerOwinContext(this IAppBuilder app, Func createCallback) - where T : class, IDisposable - { - return CreatePerOwinContext(app, (options, context) => createCallback()); - } - - public static IAppBuilder CreatePerOwinContext(this IAppBuilder app, - Func, IOwinContext, T> createCallback) where T : class, IDisposable - { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - return app.CreatePerOwinContext(createCallback, (options, instance) => instance.Dispose()); - } - - public static IAppBuilder CreatePerOwinContext(this IAppBuilder app, - Func, IOwinContext, T> createCallback, - Action, T> disposeCallback) where T : class, IDisposable - { - if (app == null) throw new ArgumentNullException(nameof(app)); - if (createCallback == null) throw new ArgumentNullException(nameof(createCallback)); - if (disposeCallback == null) throw new ArgumentNullException(nameof(disposeCallback)); - - app.Use(typeof(IdentityFactoryMiddleware>), - new IdentityFactoryOptions - { - DataProtectionProvider = app.GetDataProtectionProvider(), - Provider = new IdentityFactoryProvider - { - OnCreate = createCallback, - OnDispose = disposeCallback - } - }); - return app; - } } } diff --git a/src/Umbraco.Web/Security/AuthenticationManagerExtensions.cs b/src/Umbraco.Web/Security/AuthenticationManagerExtensions.cs deleted file mode 100644 index a450e5ec8b..0000000000 --- a/src/Umbraco.Web/Security/AuthenticationManagerExtensions.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; -using Microsoft.Owin.Security; -using Umbraco.Core; -using Constants = Umbraco.Core.Constants; - -namespace Umbraco.Web.Security -{ - public static class AuthenticationManagerExtensions - { - private static ExternalLoginInfo GetExternalLoginInfo(AuthenticateResult result) - { - if (result == null || result.Identity == null) - { - return null; - } - var idClaim = result.Identity.FindFirst(ClaimTypes.NameIdentifier); - if (idClaim == null) - { - return null; - } - // By default we don't allow spaces in user names - var name = result.Identity.Name; - if (name != null) - { - name = name.Replace(" ", ""); - } - - var email = result.Identity.FindFirstValue(ClaimTypes.Email); - return new ExternalLoginInfo - { - ExternalIdentity = result.Identity, - Login = new UserLoginInfo(idClaim.Issuer, idClaim.Value, idClaim.Issuer), - DefaultUserName = name, - Email = email - }; - } - - public static IEnumerable GetBackOfficeExternalLoginProviders(this IAuthenticationManager manager) - { - return manager.GetExternalAuthenticationTypes().Where(p => p.Properties.ContainsKey(Constants.Security.BackOfficeAuthenticationType)); - } - - /// - /// Returns the authentication type for the last registered external login (oauth) provider that specifies an auto-login redirect option - /// - /// - /// - public static string GetAutoLoginProvider(this IAuthenticationManager manager) - { - var found = manager.GetExternalAuthenticationTypes() - .LastOrDefault(p => p.Properties.ContainsKey(Constants.Security.BackOfficeAuthenticationType) - && p.Properties.TryGetValue(Constants.Security.BackOfficeExternalLoginOptionsProperty, out var options) - && options is BackOfficeExternalLoginProviderOptions externalLoginProviderOptions - && externalLoginProviderOptions.AutoRedirectLoginToExternalProvider); - - return found?.AuthenticationType; - } - - public static bool HasDenyLocalLogin(this IAuthenticationManager manager) - { - return manager.GetExternalAuthenticationTypes() - .Any(p => p.Properties.ContainsKey(Constants.Security.BackOfficeAuthenticationType) - && p.Properties.TryGetValue(Constants.Security.BackOfficeExternalLoginOptionsProperty, out var options) - && options is BackOfficeExternalLoginProviderOptions externalLoginProviderOptions - && externalLoginProviderOptions.DenyLocalLogin); - } - - /// - /// Extracts login info out of an external identity - /// - /// - /// - /// key that will be used to find the userId to verify - /// - /// the value expected to be found using the xsrfKey in the AuthenticationResult.Properties - /// dictionary - /// - /// - public static async Task GetExternalLoginInfoAsync(this IAuthenticationManager manager, - string authenticationType, - string xsrfKey, string expectedValue) - { - if (manager == null) - { - throw new ArgumentNullException("manager"); - } - var result = await manager.AuthenticateAsync(authenticationType); - // Verify that the userId is the same as what we expect if requested - if (result != null && - result.Properties != null && - result.Properties.Dictionary != null && - result.Properties.Dictionary.ContainsKey(xsrfKey) && - result.Properties.Dictionary[xsrfKey] == expectedValue) - { - return GetExternalLoginInfo(result); - } - return null; - } - - /// - /// Extracts login info out of an external identity - /// - /// - /// - /// - public static async Task GetExternalLoginInfoAsync(this IAuthenticationManager manager, string authenticationType) - { - if (manager == null) throw new ArgumentNullException(nameof(manager)); - return GetExternalLoginInfo(await manager.AuthenticateAsync(authenticationType)); - } - - public static IEnumerable GetExternalAuthenticationTypes(this IAuthenticationManager manager) - { - if (manager == null) throw new ArgumentNullException(nameof(manager)); - return manager.GetAuthenticationTypes(d => d.Properties != null && d.Caption != null); - } - - public static ClaimsIdentity CreateTwoFactorRememberBrowserIdentity(this IAuthenticationManager manager, string userId) - { - if (manager == null) throw new ArgumentNullException(nameof(manager)); - - var rememberBrowserIdentity = new ClaimsIdentity(Constants.Web.TwoFactorRememberBrowserCookie); - rememberBrowserIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId)); - - return rememberBrowserIdentity; - } - } - - public class ExternalLoginInfo - { - /// Associated login data - public UserLoginInfo Login { get; set; } - - /// Suggested user name for a user - public string DefaultUserName { get; set; } - - /// Email claim from the external identity - public string Email { get; set; } - - /// The external identity - public ClaimsIdentity ExternalIdentity { get; set; } - } -} diff --git a/src/Umbraco.Web/Security/AuthenticationOptionsExtensions.cs b/src/Umbraco.Web/Security/AuthenticationOptionsExtensions.cs index 98ff6b349e..bedc74d12c 100644 --- a/src/Umbraco.Web/Security/AuthenticationOptionsExtensions.cs +++ b/src/Umbraco.Web/Security/AuthenticationOptionsExtensions.cs @@ -12,62 +12,45 @@ namespace Umbraco.Web.Security { public static class AuthenticationOptionsExtensions { - // TODO: Migrate this! - // these are used for backwards compat - private const string ExternalSignInAutoLinkOptionsProperty = "ExternalSignInAutoLinkOptions"; - private const string ChallengeResultCallbackProperty = "ChallengeResultCallback"; + // TODO: Migrate this! This will basically be an implementation of sorts for IBackOfficeExternalLoginProviders - /// - /// Used to specify all back office external login options - /// - /// - /// - public static void SetBackOfficeExternalLoginProviderOptions(this AuthenticationOptions authOptions, - BackOfficeExternalLoginProviderOptions externalLoginProviderOptions) - { - authOptions.Description.Properties[Constants.Security.BackOfficeExternalLoginOptionsProperty] = externalLoginProviderOptions; + //// these are used for backwards compat + //private const string ExternalSignInAutoLinkOptionsProperty = "ExternalSignInAutoLinkOptions"; + //private const string ChallengeResultCallbackProperty = "ChallengeResultCallback"; - // for backwards compat, we need to add these: - if (externalLoginProviderOptions.AutoLinkOptions != null) - authOptions.Description.Properties[ExternalSignInAutoLinkOptionsProperty] = externalLoginProviderOptions.AutoLinkOptions; - if (externalLoginProviderOptions.OnChallenge != null) - authOptions.Description.Properties[ChallengeResultCallbackProperty] = externalLoginProviderOptions.OnChallenge; - } + ///// + ///// Used to specify all back office external login options + ///// + ///// + ///// + //public static void SetBackOfficeExternalLoginProviderOptions(this AuthenticationOptions authOptions, + // BackOfficeExternalLoginProviderOptions externalLoginProviderOptions) + //{ + // authOptions.Description.Properties[Constants.Security.BackOfficeExternalLoginOptionsProperty] = externalLoginProviderOptions; - [Obsolete("Use SetBackOfficeExternalLoginProviderOptions instead")] - public static void SetSignInChallengeResultCallback( - this AuthenticationOptions authOptions, - Func authProperties) - { - authOptions.Description.Properties[ChallengeResultCallbackProperty] = authProperties; - } + // // for backwards compat, we need to add these: + // // TODO: NOOOOOOOOOOO - that is not what we want to do in netcore when we figure out how to migrate this, + // // we'll also need to update angular to reference the correct properties, but that will all be figured out when we start testing + // if (externalLoginProviderOptions.AutoLinkOptions != null) + // authOptions.Description.Properties[ExternalSignInAutoLinkOptionsProperty] = externalLoginProviderOptions.AutoLinkOptions; + // if (externalLoginProviderOptions.OnChallenge != null) + // authOptions.Description.Properties[ChallengeResultCallbackProperty] = externalLoginProviderOptions.OnChallenge; + //} - public static AuthenticationProperties GetSignInChallengeResult(this AuthenticationDescription authenticationDescription, IOwinContext ctx) - { - if (authenticationDescription.Properties.ContainsKey(ChallengeResultCallbackProperty) == false) return null; - var cb = authenticationDescription.Properties[ChallengeResultCallbackProperty] as Func; - if (cb == null) return null; - return cb(ctx); - } + //public static AuthenticationProperties GetSignInChallengeResult(this AuthenticationDescription authenticationDescription, IOwinContext ctx) + //{ + // if (authenticationDescription.Properties.ContainsKey(ChallengeResultCallbackProperty) == false) return null; + // var cb = authenticationDescription.Properties[ChallengeResultCallbackProperty] as Func; + // if (cb == null) return null; + // return cb(ctx); + //} - [Obsolete("Use SetBackOfficeExternalLoginProviderOptions instead")] - public static void SetExternalSignInAutoLinkOptions( - this AuthenticationOptions authOptions, - ExternalSignInAutoLinkOptions options) - { - authOptions.Description.Properties[ExternalSignInAutoLinkOptionsProperty] = options; - } - - [Obsolete("Use GetExternalSignInAutoLinkOptions instead")] - public static ExternalSignInAutoLinkOptions GetExternalAuthenticationOptions(this AuthenticationDescription authenticationDescription) - => authenticationDescription.GetExternalSignInAutoLinkOptions(); - - public static ExternalSignInAutoLinkOptions GetExternalSignInAutoLinkOptions(this AuthenticationDescription authenticationDescription) - { - if (authenticationDescription.Properties.ContainsKey(ExternalSignInAutoLinkOptionsProperty) == false) return null; - var options = authenticationDescription.Properties[ExternalSignInAutoLinkOptionsProperty] as ExternalSignInAutoLinkOptions; - return options; - } + //public static ExternalSignInAutoLinkOptions GetExternalSignInAutoLinkOptions(this AuthenticationDescription authenticationDescription) + //{ + // if (authenticationDescription.Properties.ContainsKey(ExternalSignInAutoLinkOptionsProperty) == false) return null; + // var options = authenticationDescription.Properties[ExternalSignInAutoLinkOptionsProperty] as ExternalSignInAutoLinkOptions; + // return options; + //} ///// ///// Configures the properties of the authentication description instance for use with Umbraco back office diff --git a/src/Umbraco.Web/Security/BackOfficeSignInManager.cs b/src/Umbraco.Web/Security/BackOfficeSignInManager.cs index 640fc495a7..e5ba931b0b 100644 --- a/src/Umbraco.Web/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Web/Security/BackOfficeSignInManager.cs @@ -13,11 +13,7 @@ using Umbraco.Core.Configuration.Models; namespace Umbraco.Web.Security { - /// - /// Custom sign in manager due to SignInManager not being .NET Standard. - /// Code ported from Umbraco's BackOfficeSignInManager. - /// Can be removed once the web project moves to .NET Core. - /// + // TODO: This has been migrated to netcore public class BackOfficeSignInManager : IDisposable { private readonly IBackOfficeUserManager _userManager; @@ -43,88 +39,8 @@ namespace Umbraco.Web.Security _request = request ?? throw new ArgumentNullException(nameof(request)); } - public async Task CreateUserIdentityAsync(BackOfficeIdentityUser user) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - - var claimsPrincipal = await _claimsPrincipalFactory.CreateAsync(user); - return claimsPrincipal.Identity as ClaimsIdentity; - } - - public static BackOfficeSignInManager Create(IOwinContext context, GlobalSettings globalSettings, ILogger logger) - { - var userManager = context.GetBackOfficeUserManager(); - - return new BackOfficeSignInManager( - userManager, - new BackOfficeClaimsPrincipalFactory(userManager, new OptionsWrapper(userManager.Options)), - context.Authentication, - logger, - globalSettings, - context.Request); - } - - /// - /// Creates a user identity and then signs the identity using the AuthenticationManager - /// - /// - /// - /// - /// - public async Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent, bool rememberBrowser) - { - var userIdentity = await CreateUserIdentityAsync(user); - - // Clear any partial cookies from external or two factor partial sign ins - _authenticationManager.SignOut( - Constants.Security.BackOfficeExternalAuthenticationType, - Constants.Security.BackOfficeTwoFactorAuthenticationType); - - var nowUtc = DateTime.Now.ToUniversalTime(); - - if (rememberBrowser) - { - var rememberBrowserIdentity = _authenticationManager.CreateTwoFactorRememberBrowserIdentity(user.Id.ToString()); - _authenticationManager.SignIn(new AuthenticationProperties() - { - IsPersistent = isPersistent, - AllowRefresh = true, - IssuedUtc = nowUtc, - ExpiresUtc = nowUtc.AddMinutes(_globalSettings.TimeOutInMinutes) - }, userIdentity, rememberBrowserIdentity); - } - else - { - _authenticationManager.SignIn(new AuthenticationProperties() - { - IsPersistent = isPersistent, - AllowRefresh = true, - IssuedUtc = nowUtc, - ExpiresUtc = nowUtc.AddMinutes(_globalSettings.TimeOutInMinutes) - }, userIdentity); - } - - //track the last login date - user.LastLoginDateUtc = DateTime.UtcNow; - if (user.AccessFailedCount > 0) - //we have successfully logged in, reset the AccessFailedCount - user.AccessFailedCount = 0; - await _userManager.UpdateAsync(user); - - //set the current request's principal to the identity just signed in! - _request.User = new ClaimsPrincipal(userIdentity); - - _logger.WriteCore(TraceEventType.Information, 0, - string.Format( - "Login attempt succeeded for username {0} from IP address {1}", - user.UserName, - _request.RemoteIpAddress), null, null); - } - - public void Dispose() { - _userManager?.Dispose(); } } } diff --git a/src/Umbraco.Web/Security/BackofficeSecurity.cs b/src/Umbraco.Web/Security/BackofficeSecurity.cs index fb25159e48..9cd2616c39 100644 --- a/src/Umbraco.Web/Security/BackofficeSecurity.cs +++ b/src/Umbraco.Web/Security/BackofficeSecurity.cs @@ -1,6 +1,7 @@ using System; using Umbraco.Core; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Security; namespace Umbraco.Web.Security { diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index af4d1a79a4..d4505e23eb 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -165,21 +165,6 @@ - - - - - - - - - - - - - - - @@ -211,16 +196,9 @@ - - - - - - - @@ -287,7 +265,6 @@ - diff --git a/src/Umbraco.Web/UmbracoContext.cs b/src/Umbraco.Web/UmbracoContext.cs index b1ca469dfb..0e7343e8e9 100644 --- a/src/Umbraco.Web/UmbracoContext.cs +++ b/src/Umbraco.Web/UmbracoContext.cs @@ -4,6 +4,7 @@ using Umbraco.Core; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Hosting; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Security; using Umbraco.Web.Composing; using Umbraco.Web.PublishedCache; using Umbraco.Web.Routing; diff --git a/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs b/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs index 759065934c..bedea5a983 100644 --- a/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs +++ b/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs @@ -75,9 +75,6 @@ namespace Umbraco.Web // Configure OWIN for authentication. ConfigureUmbracoAuthentication(app); - // must come after all authentication - app.UseUmbracoBackOfficeExternalLoginErrors(); - app .UseSignalR(GlobalSettings, HostingEnvironment) .FinalizeMiddlewareConfiguration(); diff --git a/src/Umbraco.Web/UmbracoHttpHandler.cs b/src/Umbraco.Web/UmbracoHttpHandler.cs index 262eeb8560..8a76770fda 100644 --- a/src/Umbraco.Web/UmbracoHttpHandler.cs +++ b/src/Umbraco.Web/UmbracoHttpHandler.cs @@ -1,12 +1,10 @@ -using System; -using System.Web; +using System.Web; using System.Web.Mvc; using Microsoft.Extensions.Logging; -using Umbraco.Core.Cache; using Umbraco.Core.Logging; +using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.Composing; -using Umbraco.Web.Security; namespace Umbraco.Web { diff --git a/src/Umbraco.Web/UmbracoWebService.cs b/src/Umbraco.Web/UmbracoWebService.cs index 368a7dd368..0d4c4646f0 100644 --- a/src/Umbraco.Web/UmbracoWebService.cs +++ b/src/Umbraco.Web/UmbracoWebService.cs @@ -3,9 +3,9 @@ using System.Web.Services; using Microsoft.Extensions.Logging; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Logging; +using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.Composing; -using Umbraco.Web.Security; namespace Umbraco.Web { diff --git a/src/Umbraco.Web/ViewDataExtensions.cs b/src/Umbraco.Web/ViewDataExtensions.cs deleted file mode 100644 index ac4f4cdf75..0000000000 --- a/src/Umbraco.Web/ViewDataExtensions.cs +++ /dev/null @@ -1,144 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Web; -using System.Web.Mvc; -using Umbraco.Core; -using Umbraco.Web.Security; - -namespace Umbraco.Web -{ - public static class ViewDataExtensions - { - public const string TokenUmbracoPath = "UmbracoPath"; - public const string TokenInstallApiBaseUrl = "InstallApiBaseUrl"; - public const string TokenUmbracoBaseFolder = "UmbracoBaseFolder"; - public const string TokenExternalSignInError = "ExternalSignInError"; - public const string TokenPasswordResetCode = "PasswordResetCode"; - - public static bool FromTempData(this ViewDataDictionary viewData, TempDataDictionary tempData, string token) - { - if (tempData[token] == null) return false; - viewData[token] = tempData[token]; - return true; - } - - /// - /// Copies data from a request cookie to view data and then clears the cookie in the response - /// - /// - /// - /// - /// - /// - /// - /// This is similar to TempData but in some cases we cannot use TempData which relies on the temp data provider and session. - /// The cookie value can either be a simple string value - /// - /// - internal static bool FromBase64CookieData(this ViewDataDictionary viewData, HttpContextBase httpContext, string cookieName) - { - var hasCookie = httpContext.Request.HasCookie(cookieName); - if (!hasCookie) return false; - - // get the cookie value - var cookieVal = httpContext.Request.GetCookieValue(cookieName); - - if (cookieVal == null) - return false; - - // ensure the cookie is expired (must be done after reading the value) - httpContext.ExpireCookie(cookieName); - - if (cookieVal.IsNullOrWhiteSpace()) - return false; - - try - { - var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(System.Net.WebUtility.UrlDecode(cookieVal))); - // deserialize to T and store in viewdata - viewData[cookieName] = JsonConvert.DeserializeObject(decoded); - return true; - } - catch (Exception) - { - return false; - } - } - - public static string GetUmbracoPath(this ViewDataDictionary viewData) - { - return (string)viewData[TokenUmbracoPath]; - } - - public static void SetUmbracoPath(this ViewDataDictionary viewData, string value) - { - viewData[TokenUmbracoPath] = value; - } - - public static string GetInstallApiBaseUrl(this ViewDataDictionary viewData) - { - return (string)viewData[TokenInstallApiBaseUrl]; - } - - public static void SetInstallApiBaseUrl(this ViewDataDictionary viewData, string value) - { - viewData[TokenInstallApiBaseUrl] = value; - } - - public static string GetUmbracoBaseFolder(this ViewDataDictionary viewData) - { - return (string)viewData[TokenUmbracoBaseFolder]; - } - - public static void SetUmbracoBaseFolder(this ViewDataDictionary viewData, string value) - { - viewData[TokenUmbracoBaseFolder] = value; - } - - /// - /// Used by the back office login screen to get any registered external login provider errors - /// - /// - /// - public static BackOfficeExternalLoginProviderErrors GetExternalSignInProviderErrors(this ViewDataDictionary viewData) - { - return (BackOfficeExternalLoginProviderErrors)viewData[TokenExternalSignInError]; - } - - [Obsolete("Use GetExternalSignInProviderErrors instead")] - public static IEnumerable GetExternalSignInError(this ViewDataDictionary viewData) - { - var errs = viewData.GetExternalSignInProviderErrors(); - return errs?.Errors ?? Enumerable.Empty(); - } - - /// - /// Used by the back office controller to register any external login provider errors - /// - /// - /// - public static void SetExternalSignInProviderErrors(this ViewDataDictionary viewData, BackOfficeExternalLoginProviderErrors errors) - { - viewData[TokenExternalSignInError] = errors; - } - - [Obsolete("Use SetExternalSignInProviderErrors instead")] - public static void SetExternalSignInError(this ViewDataDictionary viewData, IEnumerable value) - { - viewData[TokenExternalSignInError] = new BackOfficeExternalLoginProviderErrors(string.Empty, value); - } - - public static string GetPasswordResetCode(this ViewDataDictionary viewData) - { - return (string)viewData[TokenPasswordResetCode]; - } - - public static void SetPasswordResetCode(this ViewDataDictionary viewData, string value) - { - viewData[TokenPasswordResetCode] = value; - } - } -} diff --git a/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs b/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs index b040b3ad7f..55960538a3 100644 --- a/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs +++ b/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs @@ -5,7 +5,6 @@ using Microsoft.Owin; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Web.Composing; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Logging; using Umbraco.Core.Mapping; @@ -13,8 +12,8 @@ using Umbraco.Core.Persistence; using Umbraco.Core.Services; using Umbraco.Web.Features; using Umbraco.Web.Routing; -using Umbraco.Web.Security; using Umbraco.Web.WebApi.Filters; +using Umbraco.Core.Security; namespace Umbraco.Web.WebApi {