diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs index 283f1e8315..83059dbcbb 100644 --- a/src/Umbraco.Core/Constants-Security.cs +++ b/src/Umbraco.Core/Constants-Security.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core { public const string AdminGroupAlias = "admin"; - + public const string BackOfficeAuthenticationType = "UmbracoBackOffice"; public const string BackOfficeExternalAuthenticationType = "UmbracoExternalCookie"; public const string BackOfficeExternalCookieName = "UMB_EXTLOGIN"; @@ -17,6 +17,7 @@ namespace Umbraco.Core public const string BackOfficeTwoFactorAuthenticationType = "UmbracoTwoFactorCookie"; internal const string EmptyPasswordPrefix = "___UIDEMPTYPWORD__"; + internal const string ForceReAuthFlag = "umbraco-force-auth"; /// /// The prefix used for external identity providers for their authentication type diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs index 8f396db956..e2ab9ec692 100644 --- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs @@ -106,6 +106,16 @@ namespace Umbraco.Core.Models.Identity private ObservableCollection _logins; private Lazy> _getLogins; + private List> _roles; + + //TODO: We need to override this but need to wait until the rest of the PRs are merged in + ///// + ///// Override Roles because the value of these are the user's group aliases + ///// + //public override ICollection> Roles + //{ + // get { return _roles ?? (_roles = Groups.Select(x => x.Alias).ToArray()); } + //} /// /// Used to set a lazy call back to populate the user's Login list diff --git a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs index b6d5a899d2..74d4ed8a98 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs @@ -32,7 +32,8 @@ namespace Umbraco.Core.Models.Identity .ConstructUsing((BackOfficeIdentityUser user) => new UserData(Guid.NewGuid().ToString("N"))) //this is the 'session id' .ForMember(detail => detail.Id, opt => opt.MapFrom(user => user.Id)) .ForMember(detail => detail.AllowedApplications, opt => opt.MapFrom(user => user.AllowedSections)) - .ForMember(detail => detail.Roles, opt => opt.MapFrom(user => user.Groups)) + //TODO: This should really be mapping Roles -> Roles + .ForMember(detail => detail.Roles, opt => opt.MapFrom(user => user.Groups.Select(x => x.Alias).ToArray())) .ForMember(detail => detail.RealName, opt => opt.MapFrom(user => user.Name)) //When mapping to UserData which is used in the authcookie we want ALL start nodes including ones defined on the groups .ForMember(detail => detail.StartContentNodes, opt => opt.MapFrom(user => user.AllStartContentIds)) diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index 17cea2976c..3ded4d9eab 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -218,7 +218,7 @@ namespace Umbraco.Core.Security public static bool RenewUmbracoAuthTicket(this HttpContextBase http) { if (http == null) throw new ArgumentNullException("http"); - http.Items["umbraco-force-auth"] = true; + http.Items[Constants.Security.ForceReAuthFlag] = true; return true; } @@ -230,7 +230,7 @@ namespace Umbraco.Core.Security internal static bool RenewUmbracoAuthTicket(this HttpContext http) { if (http == null) throw new ArgumentNullException("http"); - http.Items["umbraco-force-auth"] = true; + http.Items[Constants.Security.ForceReAuthFlag] = true; return true; } diff --git a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs index 63880e369e..3725e84969 100644 --- a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs +++ b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs @@ -34,7 +34,11 @@ namespace Umbraco.Core.Security RealName = user.Name, AllowedApplications = user.AllowedSections, Culture = user.Culture, - Roles = user.Roles.Select(x => x.RoleId).ToArray(), + //TODO: In order for this to work, the user.Roles would need to be filled in! + //Currently that is not the case because the BackOfficeIdentityUser deals with Groups (which we need to update) + //For now, I'll fix this by using the user.Groups instead + //Roles = user.Roles.Select(x => x.RoleId).ToArray(), + Roles = user.Groups.Select(x => x.Alias).ToArray(), StartContentNodes = user.StartContentIds, StartMediaNodes = user.StartMediaIds, SessionId = user.SecurityStamp diff --git a/src/Umbraco.Core/Security/MembershipProviderExtensions.cs b/src/Umbraco.Core/Security/MembershipProviderExtensions.cs index 750526976a..7f787050d4 100644 --- a/src/Umbraco.Core/Security/MembershipProviderExtensions.cs +++ b/src/Umbraco.Core/Security/MembershipProviderExtensions.cs @@ -32,7 +32,8 @@ namespace Umbraco.Core.Security var identity = Thread.CurrentPrincipal.GetUmbracoIdentity(); if (identity != null) { - var user = userService.GetByUsername(identity.Username); + var user = userService.GetUserById(identity.Id.TryConvertTo().Result); + if (user == null) throw new InvalidOperationException("No user with username " + identity.Username + " found"); var userIsAdmin = user.IsAdmin(); if (userIsAdmin) { diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 2d5fff536c..572453109d 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Data.Common; using System.Globalization; using System.Linq; @@ -214,14 +215,8 @@ namespace Umbraco.Core.Services Save(membershipUser); } - /// - /// This is simply a helper method which essentially just wraps the MembershipProvider's ChangePassword method - /// - /// - /// This method exists so that Umbraco developers can use one entry point to create/update users if they choose to. - /// - /// The user to save the password for - /// The password to save + [Obsolete("ASP.NET Identity APIs like the BackOfficeUserManager should be used to manage passwords, this will not work with correct security practices because you would need the existing password")] + [EditorBrowsable(EditorBrowsableState.Never)] public void SavePassword(IUser user, string password) { if (user == null) throw new ArgumentNullException("user"); diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index a9390ca5c7..f61e11a791 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -149,7 +149,8 @@ namespace Umbraco.Web.Editors /// 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] + [SetAngularAntiForgeryTokens] + [VerifyIfUserTicketDataIsStale] public UserDetail GetCurrentUser() { var user = UmbracoContext.Security.CurrentUser; @@ -194,7 +195,8 @@ namespace Umbraco.Web.Editors return result; } - + + //TODO: This should be on the CurrentUserController? [WebApi.UmbracoAuthorize] [ValidateAngularAntiForgeryToken] public async Task> GetCurrentUserLinkedLogins() diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index f76bdb57fa..f7e22fdab1 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -325,7 +325,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(detail => detail.Id, opt => opt.MapFrom(user => user.Id)) .ForMember(detail => detail.AllowedApplications, opt => opt.MapFrom(user => user.AllowedSections)) .ForMember(detail => detail.RealName, opt => opt.MapFrom(user => user.Name)) - .ForMember(detail => detail.Roles, opt => opt.MapFrom(user => user.Groups.ToArray())) + .ForMember(detail => detail.Roles, opt => opt.MapFrom(user => user.Groups.Select(x => x.Alias).ToArray())) .ForMember(detail => detail.StartContentNodes, opt => opt.MapFrom(user => user.AllStartContentIds)) .ForMember(detail => detail.StartMediaNodes, opt => opt.MapFrom(user => user.AllStartMediaIds)) .ForMember(detail => detail.Username, opt => opt.MapFrom(user => user.Username)) diff --git a/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs b/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs index 07f4a3317f..2f067212f3 100644 --- a/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs +++ b/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs @@ -96,8 +96,8 @@ namespace Umbraco.Web.Security.Identity if (request.Uri.AbsolutePath.InvariantEquals(_getRemainingSecondsPath)) return false; if (//check the explicit flag - (checkForceAuthTokens && ctx.Get("umbraco-force-auth") != null) - || (checkForceAuthTokens && httpCtx.Success && httpCtx.Result.Items["umbraco-force-auth"] != null) + (checkForceAuthTokens && ctx.Get(Constants.Security.ForceReAuthFlag) != null) + || (checkForceAuthTokens && httpCtx.Success && httpCtx.Result.Items[Constants.Security.ForceReAuthFlag] != null) //check back office || request.Uri.IsBackOfficeRequest(HttpRuntime.AppDomainAppVirtualPath) //check installer diff --git a/src/Umbraco.Web/Security/Identity/ForceRenewalCookieAuthenticationHandler.cs b/src/Umbraco.Web/Security/Identity/ForceRenewalCookieAuthenticationHandler.cs index c7030b2558..1bf5663884 100644 --- a/src/Umbraco.Web/Security/Identity/ForceRenewalCookieAuthenticationHandler.cs +++ b/src/Umbraco.Web/Security/Identity/ForceRenewalCookieAuthenticationHandler.cs @@ -71,7 +71,7 @@ namespace Umbraco.Web.Security.Identity var httpCtx = Context.TryGetHttpContext(); //check for the special flag in either the owin or http context - var shouldRenew = Context.Get("umbraco-force-auth") != null || (httpCtx.Success && httpCtx.Result.Items["umbraco-force-auth"] != null); + var shouldRenew = Context.Get(Constants.Security.ForceReAuthFlag) != null || (httpCtx.Success && httpCtx.Result.Items[Constants.Security.ForceReAuthFlag] != null); if (shouldRenew) { diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 993d117406..afa4790262 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -798,6 +798,7 @@ + diff --git a/src/Umbraco.Web/WebApi/Filters/VerifyIfUserTicketDataIsStaleAttribute.cs b/src/Umbraco.Web/WebApi/Filters/VerifyIfUserTicketDataIsStaleAttribute.cs new file mode 100644 index 0000000000..acf9c6d7a1 --- /dev/null +++ b/src/Umbraco.Web/WebApi/Filters/VerifyIfUserTicketDataIsStaleAttribute.cs @@ -0,0 +1,106 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Web.Http.Controllers; +using System.Web.Http.Filters; +using AutoMapper; +using Umbraco.Core; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Security; +using Umbraco.Web.Security; +using UserExtensions = Umbraco.Core.Models.UserExtensions; + +namespace Umbraco.Web.WebApi.Filters +{ + + /// + /// This filter will check if the current Principal/Identity assigned to the request has stale data in it compared + /// to what is persisted for the current user and will update the current auth ticket with the correct data if required. + /// + public sealed class VerifyIfUserTicketDataIsStaleAttribute : ActionFilterAttribute + { + public override async Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken) + { + await CheckStaleData(actionContext); + } + + public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken) + { + await CheckStaleData(actionExecutedContext.ActionContext); + } + + private async Task CheckStaleData(HttpActionContext actionContext) + { + var identity = actionContext.RequestContext.Principal.Identity as UmbracoBackOfficeIdentity; + if (identity == null) return; + + var userId = identity.Id.TryConvertTo(); + if (userId == false) return; + + var user = ApplicationContext.Current.Services.UserService.GetUserById(userId.Result); + if (user == null) return; + + if (user.Username != identity.Username) + { + await ReSync(user, identity, actionContext); + return; + } + + var culture = UserExtensions.GetUserCulture(user, ApplicationContext.Current.Services.TextService).ToString(); + if (culture != identity.Culture) + { + //TODO: Might have to log out if this happens or somehow refresh the back office UI with a special header maybe? + await ReSync(user, identity, actionContext); + return; + } + + if (user.AllowedSections.UnsortedSequenceEqual(identity.AllowedApplications) == false) + { + await ReSync(user, identity, actionContext); + return; + } + + if (user.Groups.Select(x => x.Alias).UnsortedSequenceEqual(identity.Roles) == false) + { + await ReSync(user, identity, actionContext); + return; + } + + //TODO: This will need to be changed when http://issues.umbraco.org/issue/U4-10173 is merged + var startContentIds = user.AllStartContentIds; + if (startContentIds.UnsortedSequenceEqual(identity.StartContentNodes) == false) + { + await ReSync(user, identity, actionContext); + return; + } + + //TODO: This will need to be changed when http://issues.umbraco.org/issue/U4-10173 is merged + var startMediaIds = user.AllStartMediaIds; + if (startMediaIds.UnsortedSequenceEqual(identity.StartMediaNodes) == false) + { + await ReSync(user, identity, actionContext); + return; + } + } + + /// + /// This will update the current request IPrincipal to be correct and re-create the auth ticket + /// + /// + /// + /// + /// + private async Task ReSync(IUser user, UmbracoBackOfficeIdentity identityUser, HttpActionContext actionContext) + { + var owinCtx = actionContext.Request.TryGetOwinContext().Result; + var signInManager = owinCtx.GetBackOfficeSignInManager(); + + //ensure the remainder of the request has the correct principal set + actionContext.Request.SetPrincipalForRequest(user); + + var backOfficeIdentityUser = Mapper.Map(user); + await signInManager.SignInAsync(backOfficeIdentityUser, isPersistent: true, rememberBrowser: false); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs b/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs index 06e0051a1f..430fd01e19 100644 --- a/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs +++ b/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs @@ -22,6 +22,7 @@ namespace Umbraco.Web.WebApi [UmbracoAuthorize] [DisableBrowserCache] [UmbracoWebApiRequireHttps] + [VerifyIfUserTicketDataIsStale] public abstract class UmbracoAuthorizedApiController : UmbracoApiController {