From 90b562a0a1d28e98164f7da21af2c58e46120dd0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 24 Mar 2015 20:17:37 +1100 Subject: [PATCH] Update the PostLogin method to write the auth ticket the way that webapi is supposed to, not sure how this was actually working before because writing cookies directly with HttpContext and then also using WebApi normally doesn't work (maybe in very specific circumstances), so now the cookie writing is done consistently and it is working, prior to this i was getting lots of issues with the xsrf tokens. Updated some user model mappings for convenience and update naming conventions for some properties of the BackOfficeIdentityUser for consistency. --- .../Models/Identity/BackOfficeIdentityUser.cs | 6 +- .../Models/Identity/IdentityModelMappings.cs | 8 +- .../Security/AuthenticationExtensions.cs | 61 +++++++++++- .../BackOfficeClaimsIdentityFactory.cs | 6 +- .../Security/BackOfficeUserStore.cs | 22 ++--- .../Editors/AuthenticationController.cs | 99 +++++++++---------- .../Models/Mapping/UserModelMapper.cs | 29 +++++- .../Security/Identity/AppBuilderExtensions.cs | 2 + src/Umbraco.Web/Security/WebSecurity.cs | 23 ++--- .../Filters/AngularAntiForgeryHelper.cs | 7 +- .../UmbracoBackOfficeLogoutAttribute.cs | 2 +- 11 files changed, 175 insertions(+), 90 deletions(-) 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. ///