diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs index 3ba5b4259a..5060cb5912 100644 --- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs @@ -11,9 +11,9 @@ namespace Umbraco.Core.Models.Identity /// Gets/sets the user's real name /// public string Name { get; set; } - public int StartContentNode { get; set; } - public int StartMediaNode { get; set; } - public string[] AllowedApplications { get; set; } + public int StartContentId { get; set; } + public int StartMediaId { get; set; } + public string[] AllowedSections { get; set; } public string Culture { get; set; } public string UserTypeAlias { get; set; } diff --git a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs index 8fa2703f39..def71a8982 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs @@ -18,12 +18,12 @@ namespace Umbraco.Core.Models.Identity .ForMember(user => user.LockoutEndDateUtc, expression => expression.UseValue(DateTime.MaxValue.ToUniversalTime())) .ForMember(user => user.UserName, expression => expression.MapFrom(user => user.Username)) .ForMember(user => user.PasswordHash, expression => expression.MapFrom(user => GetPasswordHash(user.RawPasswordValue))) - .ForMember(user => user.Culture, expression => expression.MapFrom(user => user.Language)) + .ForMember(user => user.Culture, expression => expression.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService))) .ForMember(user => user.Name, expression => expression.MapFrom(user => user.Name)) - .ForMember(user => user.StartMediaNode, expression => expression.MapFrom(user => user.StartMediaId)) - .ForMember(user => user.StartContentNode, expression => expression.MapFrom(user => user.StartContentId)) + .ForMember(user => user.StartMediaId, expression => expression.MapFrom(user => user.StartMediaId)) + .ForMember(user => user.StartContentId, expression => expression.MapFrom(user => user.StartContentId)) .ForMember(user => user.UserTypeAlias, expression => expression.MapFrom(user => user.UserType.Alias)) - .ForMember(user => user.AllowedApplications, expression => expression.MapFrom(user => user.AllowedSections.ToArray())); + .ForMember(user => user.AllowedSections, expression => expression.MapFrom(user => user.AllowedSections.ToArray())); } private string GetPasswordHash(string storedPass) diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index e71dd0e00c..ca597a8fef 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -9,9 +9,11 @@ using System.Security.Principal; using System.Threading; using System.Web; using System.Web.Security; +using AutoMapper; using Microsoft.Owin; using Newtonsoft.Json; using Umbraco.Core.Configuration; +using Umbraco.Core.Models.Membership; namespace Umbraco.Core.Security { @@ -163,8 +165,60 @@ namespace Umbraco.Core.Security Expires = DateTime.Now.AddYears(-1), Path = "/" }; + //remove the external login cookie too + var extLoginCookie = new CookieHeaderValue(Constants.Security.BackOfficeExternalAuthenticationType, "") + { + Expires = DateTime.Now.AddYears(-1), + Path = "/" + }; - response.Headers.AddCookies(new[] { authCookie, prevCookie }); + response.Headers.AddCookies(new[] { authCookie, prevCookie, extLoginCookie }); + } + + /// + /// This adds the forms authentication cookie for webapi since cookies are handled differently + /// + /// + /// + public static FormsAuthenticationTicket UmbracoLoginWebApi(this HttpResponseMessage response, IUser user) + { + if (response == null) throw new ArgumentNullException("response"); + + //remove the external login cookie + var extLoginCookie = new CookieHeaderValue(Constants.Security.BackOfficeExternalAuthenticationType, "") + { + Expires = DateTime.Now.AddYears(-1), + Path = "/" + }; + + var userDataString = JsonConvert.SerializeObject(Mapper.Map(user)); + + var ticket = new FormsAuthenticationTicket( + 4, + user.Username, + DateTime.Now, + DateTime.Now.AddMinutes(GlobalSettings.TimeOutInMinutes), + true, + userDataString, + "/" + ); + + // Encrypt the cookie using the machine key for secure transport + var encrypted = FormsAuthentication.Encrypt(ticket); + + //add the cookie + var authCookie = new CookieHeaderValue(UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName, encrypted) + { + //Umbraco has always persisted it's original cookie for 1 day so we'll keep it that way + Expires = DateTime.Now.AddMinutes(1440), + Path = "/", + Secure = GlobalSettings.UseSSL, + HttpOnly = true + }; + + response.Headers.AddCookies(new[] { authCookie, extLoginCookie }); + + return ticket; } /// @@ -297,8 +351,8 @@ namespace Umbraco.Core.Security private static void Logout(this HttpContextBase http, string cookieName) { if (http == null) throw new ArgumentNullException("http"); - //clear the preview cookie too - var cookies = new[] { cookieName, Constants.Web.PreviewCookieName }; + //clear the preview cookie and external login + var cookies = new[] { cookieName, Constants.Web.PreviewCookieName, Constants.Security.BackOfficeExternalAuthenticationType }; foreach (var c in cookies) { //remove from the request @@ -411,7 +465,6 @@ namespace Umbraco.Core.Security /// The user data. /// The login timeout mins. /// The minutes persisted. - /// The cookie path. /// Name of the cookie. /// The cookie domain. private static FormsAuthenticationTicket CreateAuthTicketAndCookie(this HttpContextBase http, diff --git a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs index 54b537faab..b6d19b78eb 100644 --- a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs +++ b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs @@ -22,11 +22,11 @@ namespace Umbraco.Core.Security Id = user.Id, Username = user.UserName, RealName = user.Name, - AllowedApplications = user.AllowedApplications, + AllowedApplications = user.AllowedSections, Culture = user.Culture, Roles = user.Roles.Select(x => x.RoleId).ToArray(), - StartContentNode = user.StartContentNode, - StartMediaNode = user.StartMediaNode + StartContentNode = user.StartContentId, + StartMediaNode = user.StartMediaId }); return umbracoIdentity; diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index dd2041fd51..0b1c95deb9 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -414,25 +414,25 @@ namespace Umbraco.Core.Security anythingChanged = true; user.Language = identityUser.Culture; } - if (user.StartMediaId != identityUser.StartMediaNode) + if (user.StartMediaId != identityUser.StartMediaId) { anythingChanged = true; - user.StartMediaId = identityUser.StartMediaNode; + user.StartMediaId = identityUser.StartMediaId; } - if (user.StartContentId != identityUser.StartContentNode) + if (user.StartContentId != identityUser.StartContentId) { anythingChanged = true; - user.StartContentId = identityUser.StartContentNode; + user.StartContentId = identityUser.StartContentId; } - if (user.AllowedSections.ContainsAll(identityUser.AllowedApplications) == false - || identityUser.AllowedApplications.ContainsAll(user.AllowedSections) == false) + if (user.AllowedSections.ContainsAll(identityUser.AllowedSections) == false + || identityUser.AllowedSections.ContainsAll(user.AllowedSections) == false) { anythingChanged = true; foreach (var allowedSection in user.AllowedSections) { user.RemoveAllowedSection(allowedSection); } - foreach (var allowedApplication in identityUser.AllowedApplications) + foreach (var allowedApplication in identityUser.AllowedSections) { user.AddAllowedSection(allowedApplication); } @@ -448,7 +448,7 @@ namespace Umbraco.Core.Security /// public Task AddToRoleAsync(BackOfficeIdentityUser user, string roleName) { - if (user.AllowedApplications.InvariantContains(roleName)) return Task.FromResult(0); + if (user.AllowedSections.InvariantContains(roleName)) return Task.FromResult(0); var asInt = user.Id.TryConvertTo(); if (asInt == false) @@ -474,7 +474,7 @@ namespace Umbraco.Core.Security /// public Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string roleName) { - if (user.AllowedApplications.InvariantContains(roleName) == false) return Task.FromResult(0); + if (user.AllowedSections.InvariantContains(roleName) == false) return Task.FromResult(0); var asInt = user.Id.TryConvertTo(); if (asInt == false) @@ -500,7 +500,7 @@ namespace Umbraco.Core.Security /// public Task> GetRolesAsync(BackOfficeIdentityUser user) { - return Task.FromResult((IList)user.AllowedApplications.ToList()); + return Task.FromResult((IList)user.AllowedSections.ToList()); } /// @@ -510,7 +510,7 @@ namespace Umbraco.Core.Security /// public Task IsInRoleAsync(BackOfficeIdentityUser user, string roleName) { - return Task.FromResult(user.AllowedApplications.InvariantContains(roleName)); + return Task.FromResult(user.AllowedSections.InvariantContains(roleName)); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index a1e12401b1..0606cec1dd 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -43,7 +43,14 @@ namespace Umbraco.Web.Editors [IsBackOffice] public class AuthenticationController : UmbracoApiController { - + + private BackOfficeUserManager _userManager; + + protected BackOfficeUserManager UserManager + { + get { return _userManager ?? (_userManager = TryGetOwinContext().Result.GetUserManager()); } + } + /// /// This is a special method that will return the current users' remaining session seconds, the reason /// it is special is because this route is ignored in the UmbracoModule so that the auth ticket doesn't get @@ -85,33 +92,6 @@ namespace Umbraco.Web.Editors } } - private void AddModelErrors(IdentityResult result, string prefix = "") - { - foreach (var error in result.Errors) - { - ModelState.AddModelError(prefix, error); - } - } - - private async Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent) - { - var owinContext = TryGetOwinContext().Result; - - owinContext.Authentication.SignOut(Core.Constants.Security.BackOfficeExternalAuthenticationType); - - owinContext.Authentication.SignIn( - new AuthenticationProperties() { IsPersistent = isPersistent }, - await GenerateUserIdentityAsync(user)); - } - - private async Task GenerateUserIdentityAsync(BackOfficeIdentityUser user) - { - // NOTE the authenticationType must match the umbraco one - // defined in CookieAuthenticationOptions.AuthenticationType - var userIdentity = await UserManager.CreateIdentityAsync(user, global::Umbraco.Core.Constants.Security.BackOfficeAuthenticationType); - return userIdentity; - } - /// /// Checks if the current user's cookie is valid and if so returns OK or a 400 (BadRequest) /// @@ -154,53 +134,45 @@ namespace Umbraco.Web.Editors } [WebApi.UmbracoAuthorize] - [SetAngularAntiForgeryTokens] + [ValidateAngularAntiForgeryToken] public async Task> GetCurrentUserLinkedLogins() { var identityUser = await UserManager.FindByIdAsync(UmbracoContext.Security.GetUserId()); return identityUser.Logins.ToDictionary(x => x.LoginProvider, x => x.ProviderKey); } - private BackOfficeUserManager _userManager; - - protected BackOfficeUserManager UserManager - { - get { return _userManager ?? (_userManager = TryGetOwinContext().Result.GetUserManager()); } - } - /// /// Logs a user in /// /// [SetAngularAntiForgeryTokens] - public UserDetail PostLogin(LoginModel loginModel) + public HttpResponseMessage PostLogin(LoginModel loginModel) { if (UmbracoContext.Security.ValidateBackOfficeCredentials(loginModel.Username, loginModel.Password)) { + //get the user var user = Security.GetBackOfficeUser(loginModel.Username); + var userDetail = Mapper.Map(user); - //TODO: Clean up the int cast! - var ticket = UmbracoContext.Security.PerformLogin(user); + //create a response with the userDetail object + var response = Request.CreateResponse(HttpStatusCode.OK, userDetail); - //TODO: Normally we'd do something like this for identity, but we're mixing and matching legacy and new here - // so we'll keep the legacy way and move forward with this in our custom handler for now, eventually replacing - // the above legacy logic with the new stuff. - - //OwinContext.Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie); - //OwinContext.Authentication.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, - // await user.GenerateUserIdentityAsync(UserManager)); + //set the response cookies with the ticket (NOTE: This needs to be done with the custom webapi extension because + // we cannot mix HttpContext.Response.Cookies and the way WebApi/Owin work) + var ticket = response.UmbracoLoginWebApi(user); var http = this.TryGetHttpContext(); if (http.Success == false) { throw new InvalidOperationException("This method requires that an HttpContext be active"); } + //This ensure the current principal is set, otherwise any logic executing after this wouldn't actually be authenticated http.Result.AuthenticateCurrentRequest(ticket, false); + + //update the userDetail and set their remaining seconds + userDetail.SecondsUntilTimeout = ticket.GetRemainingAuthSeconds(); - var result = Mapper.Map(user); - //set their remaining seconds - result.SecondsUntilTimeout = ticket.GetRemainingAuthSeconds(); - return result; + return response; } //return BadRequest (400), we don't want to return a 401 because that get's intercepted @@ -222,5 +194,32 @@ namespace Umbraco.Web.Editors { return Request.CreateResponse(HttpStatusCode.OK); } + + private void AddModelErrors(IdentityResult result, string prefix = "") + { + foreach (var error in result.Errors) + { + ModelState.AddModelError(prefix, error); + } + } + + private async Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent) + { + var owinContext = TryGetOwinContext().Result; + + owinContext.Authentication.SignOut(Core.Constants.Security.BackOfficeExternalAuthenticationType); + + owinContext.Authentication.SignIn( + new AuthenticationProperties() { IsPersistent = isPersistent }, + await GenerateUserIdentityAsync(user)); + } + + private async Task GenerateUserIdentityAsync(BackOfficeIdentityUser user) + { + // NOTE the authenticationType must match the umbraco one + // defined in CookieAuthenticationOptions.AuthenticationType + var userIdentity = await UserManager.CreateIdentityAsync(user, global::Umbraco.Core.Constants.Security.BackOfficeAuthenticationType); + return userIdentity; + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index 0bda50e1fc..7200f2a7c2 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -5,6 +5,9 @@ using Umbraco.Core.Models.Mapping; using Umbraco.Core.Models.Membership; using Umbraco.Web.Models.ContentEditing; using umbraco; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Security; namespace Umbraco.Web.Models.Mapping { @@ -17,7 +20,19 @@ namespace Umbraco.Web.Models.Mapping .ForMember(detail => detail.UserType, opt => opt.MapFrom(user => user.UserType.Alias)) .ForMember(detail => detail.StartContentId, opt => opt.MapFrom(user => user.StartContentId)) .ForMember(detail => detail.StartMediaId, opt => opt.MapFrom(user => user.StartMediaId)) - .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => ui.Culture(user))) + .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService))) + .ForMember( + detail => detail.EmailHash, + opt => opt.MapFrom(user => user.Email.ToLowerInvariant().Trim().ToMd5())) + .ForMember(detail => detail.SecondsUntilTimeout, opt => opt.Ignore()); + + config.CreateMap() + .ForMember(detail => detail.UserId, opt => opt.MapFrom(user => user.Id)) + .ForMember(detail => detail.UserType, opt => opt.MapFrom(user => user.UserTypeAlias)) + .ForMember(detail => detail.StartContentId, opt => opt.MapFrom(user => user.StartContentId)) + .ForMember(detail => detail.StartMediaId, opt => opt.MapFrom(user => user.StartMediaId)) + .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.Culture)) + .ForMember(detail => detail.AllowedSections, opt => opt.MapFrom(user => user.AllowedSections)) .ForMember( detail => detail.EmailHash, opt => opt.MapFrom(user => user.Email.ToLowerInvariant().Trim().ToMd5())) @@ -25,6 +40,18 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap() .ForMember(detail => detail.UserId, opt => opt.MapFrom(profile => GetIntId(profile.Id))); + + config.CreateMap() + .ConstructUsing((IUser 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.RealName, opt => opt.MapFrom(user => user.Name)) + .ForMember(detail => detail.Roles, opt => opt.MapFrom(user => new[] {user.UserType.Alias})) + .ForMember(detail => detail.StartContentNode, opt => opt.MapFrom(user => user.StartContentId)) + .ForMember(detail => detail.StartMediaNode, opt => opt.MapFrom(user => user.StartMediaId)) + .ForMember(detail => detail.Username, opt => opt.MapFrom(user => user.Username)) + .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService))); + } private static int GetIntId(object id) diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index 912b19fd2e..7e70ba2958 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -84,6 +84,8 @@ namespace Umbraco.Web.Security.Identity { Provider = new CookieAuthenticationProvider { + //TODO: Need to implement IUserSecurityStampStore on BackOfficeUserStore! + //// Enables the application to validate the security stamp when the user //// logs in. This is a security feature which is used when you //// change a password or add an external login to your account. diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index b6ec3680d8..19857ddbcf 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Security; +using AutoMapper; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Models.Membership; @@ -94,18 +95,18 @@ namespace Umbraco.Web.Security /// returns the Forms Auth ticket created which is used to log them in public virtual FormsAuthenticationTicket PerformLogin(IUser user) { - var ticket = _httpContext.CreateUmbracoAuthTicket(new UserData(Guid.NewGuid().ToString("N")) + //clear the external cookie - we do this without owin context because we're writing cookies directly to httpcontext + // and cookie handling is different with httpcontext vs webapi and owin, normally we'd do: + //_httpContext.GetOwinContext().Authentication.SignOut(Constants.Security.BackOfficeExternalAuthenticationType); + + var externalLoginCookie = _httpContext.Request.Cookies.Get(Constants.Security.BackOfficeExternalAuthenticationType); + if (externalLoginCookie != null) { - Id = user.Id, - AllowedApplications = user.AllowedSections.ToArray(), - RealName = user.Name, - //currently we only have one user type! - Roles = new[] { user.UserType.Alias }, - StartContentNode = user.StartContentId, - StartMediaNode = user.StartMediaId, - Username = user.Username, - Culture = ui.Culture(user) - }); + externalLoginCookie.Expires = DateTime.Now.AddYears(-1); + _httpContext.Response.Cookies.Set(externalLoginCookie); + } + + var ticket = _httpContext.CreateUmbracoAuthTicket(Mapper.Map(user)); LogHelper.Info("User Id: {0} logged in", () => user.Id); diff --git a/src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs b/src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs index 2e4b4176bb..d48cd66077 100644 --- a/src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs +++ b/src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs @@ -1,8 +1,10 @@ -using System.Linq; +using System; +using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Web.Helpers; using Umbraco.Core; +using Umbraco.Core.Logging; namespace Umbraco.Web.WebApi.Filters { @@ -54,8 +56,9 @@ namespace Umbraco.Web.WebApi.Filters { AntiForgery.Validate(cookieToken, headerToken); } - catch + catch (Exception ex) { + LogHelper.Error(typeof(AngularAntiForgeryHelper), "Could not validate XSRF token", ex); return false; } return true; diff --git a/src/Umbraco.Web/WebApi/Filters/UmbracoBackOfficeLogoutAttribute.cs b/src/Umbraco.Web/WebApi/Filters/UmbracoBackOfficeLogoutAttribute.cs index ecde11023b..693d45c792 100644 --- a/src/Umbraco.Web/WebApi/Filters/UmbracoBackOfficeLogoutAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/UmbracoBackOfficeLogoutAttribute.cs @@ -7,7 +7,7 @@ namespace Umbraco.Web.WebApi.Filters /// A filter that is used to remove the authorization cookie for the current user when the request is successful /// /// - /// This is used so that we can log a user out in conjunction with using other filters that modify the cookies collection. + /// This is used so that we can log a user OUT in conjunction with using other filters that modify the cookies collection. /// SD: I beleive this is a bug with web api since if you modify the cookies collection on the HttpContext.Current and then /// use a filter to write the cookie headers, the filter seems to have no affect at all. ///