From 6f464567bb3924c7c103eec46252b198cdd554f4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 31 Jul 2013 17:08:56 +1000 Subject: [PATCH] Implements real FormsAuthentication for back office cookie authentication... finally :) --- .../Configuration/UmbracoSettings.cs | 27 ++ .../Security/AuthenticationExtensions.cs | 204 +++++++++++++ .../Security/UmbracoBackOfficeIdentity.cs | 99 ++++++ src/Umbraco.Core/Security/UserData.cs | 40 +++ src/Umbraco.Core/Umbraco.Core.csproj | 3 + .../config/umbracoSettings.config | 1 + src/Umbraco.Web.UI/install/Default.aspx.cs | 2 +- .../Editors/AuthenticationController.cs | 4 +- .../UmbracoInstallAuthorizeAttribute.cs | 22 +- .../Mvc/MemberAuthorizeAttribute.cs | 14 +- .../Mvc/UmbracoAuthorizeAttribute.cs | 25 +- .../Mvc/UmbracoAuthorizedController.cs | 44 +-- src/Umbraco.Web/Mvc/UmbracoController.cs | 9 + src/Umbraco.Web/Security/WebSecurity.cs | 239 ++++++--------- src/Umbraco.Web/UmbracoContext.cs | 18 +- src/Umbraco.Web/UmbracoModule.cs | 288 ++++++++++-------- .../WebApi/MemberAuthorizeAttribute.cs | 17 +- .../WebApi/UmbracoApiController.cs | 9 + .../WebApi/UmbracoAuthorizeAttribute.cs | 25 +- .../WebApi/UmbracoAuthorizedApiController.cs | 53 +--- .../UmbracoAuthorizedHttpHandler.cs | 2 +- .../UmbracoAuthorizedWebService.cs | 2 +- .../BasePages/BasePage.cs | 204 ++++--------- 23 files changed, 818 insertions(+), 533 deletions(-) create mode 100644 src/Umbraco.Core/Security/AuthenticationExtensions.cs create mode 100644 src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs create mode 100644 src/Umbraco.Core/Security/UserData.cs diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings.cs b/src/Umbraco.Core/Configuration/UmbracoSettings.cs index 4ed5027539..9292fd21e9 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading; using System.Web; using System.Web.Caching; +using System.Web.Security; using System.Xml; using System.Configuration; @@ -299,6 +300,32 @@ namespace Umbraco.Core.Configuration } } + internal static string AuthCookieName + { + get + { + var value = GetKey("/settings/security/authCookieName"); + if (string.IsNullOrEmpty(value) == false) + { + return value; + } + return "UMB_UCONTEXT"; + } + } + + internal static string AuthCookieDomain + { + get + { + var value = GetKey("/settings/security/authCookieDomain"); + if (string.IsNullOrEmpty(value) == false) + { + return value; + } + return FormsAuthentication.CookieDomain; + } + } + /// /// Enables the experimental canvas (live) editing on the frontend of the website /// diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs new file mode 100644 index 0000000000..5460156e15 --- /dev/null +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -0,0 +1,204 @@ +using System; +using System.Web; +using System.Web.Security; +using Newtonsoft.Json; +using Umbraco.Core.Configuration; + +namespace Umbraco.Core.Security +{ + /// + /// Extensions to create and renew and remove authentication tickets for the Umbraco back office + /// + internal static class AuthenticationExtensions + { + public static UmbracoBackOfficeIdentity GetCurrentIdentity(this HttpContextBase http) + { + return http.User.Identity as UmbracoBackOfficeIdentity; + } + + internal static UmbracoBackOfficeIdentity GetCurrentIdentity(this HttpContext http) + { + return new HttpContextWrapper(http).GetCurrentIdentity(); + } + + /// + /// This clears the forms authentication cookie + /// + public static void UmbracoLogout(this HttpContextBase http) + { + Logout(http, UmbracoSettings.AuthCookieName); + } + + internal static void UmbracoLogout(this HttpContext http) + { + new HttpContextWrapper(http).UmbracoLogout(); + } + + /// + /// Renews the Umbraco authentication ticket + /// + /// + /// + /// + public static bool RenewUmbracoAuthTicket(this HttpContextBase http, int timeoutInMinutes = 60) + { + return RenewAuthTicket(http, UmbracoSettings.AuthCookieName, UmbracoSettings.AuthCookieDomain, timeoutInMinutes); + } + + internal static bool RenewUmbracoAuthTicket(this HttpContext http, int timeoutInMinutes = 60) + { + return new HttpContextWrapper(http).RenewUmbracoAuthTicket(timeoutInMinutes); + } + + /// + /// Creates the umbraco authentication ticket + /// + /// + /// + public static void CreateUmbracoAuthTicket(this HttpContextBase http, UserData userdata) + { + var userDataString = JsonConvert.SerializeObject(userdata); + CreateAuthTicket(http, userdata.Username, userDataString, GlobalSettings.TimeOutInMinutes, GlobalSettings.TimeOutInMinutes, "/", UmbracoSettings.AuthCookieName, UmbracoSettings.AuthCookieDomain); + } + + internal static void CreateUmbracoAuthTicket(this HttpContext http, UserData userdata) + { + new HttpContextWrapper(http).CreateUmbracoAuthTicket(userdata); + } + + /// + /// Gets the umbraco auth ticket + /// + /// + /// + public static FormsAuthenticationTicket GetUmbracoAuthTicket(this HttpContextBase http) + { + return GetAuthTicket(http, UmbracoSettings.AuthCookieName); + } + + internal static FormsAuthenticationTicket GetUmbracoAuthTicket(this HttpContext http) + { + return new HttpContextWrapper(http).GetUmbracoAuthTicket(); + } + + /// + /// This clears the forms authentication cookie + /// + /// + /// + private static void Logout(this HttpContextBase http, string cookieName) + { + //remove from the request + http.Request.Cookies.Remove(cookieName); + + //expire from the response + var formsCookie = http.Response.Cookies[cookieName]; + if (formsCookie != null) + { + //this will expire immediately and be removed from the browser + formsCookie.Expires = DateTime.Now.AddYears(-1); + } + else + { + //ensure there's def an expired cookie + http.Response.Cookies.Add(new HttpCookie(cookieName) { Expires = DateTime.Now.AddYears(-1) }); + } + } + + private static FormsAuthenticationTicket GetAuthTicket(this HttpContextBase http, string cookieName) + { + var formsCookie = http.Request.Cookies[cookieName]; + if (formsCookie == null) + { + return null; + } + //get the ticket + try + { + return FormsAuthentication.Decrypt(formsCookie.Value); + } + catch (Exception) + { + //occurs when decryption fails + http.Logout(cookieName); + return null; + } + } + + /// + /// Renews the forms authentication ticket & cookie + /// + /// + /// + /// + /// + /// + private static bool RenewAuthTicket(this HttpContextBase http, string cookieName, string cookieDomain, int minutesPersisted) + { + //get the ticket + var ticket = GetAuthTicket(http, cookieName); + //renew the ticket + var renewed = FormsAuthentication.RenewTicketIfOld(ticket); + if (renewed == null) + { + return false; + } + //encrypt it + var hash = FormsAuthentication.Encrypt(renewed); + //write it to the response + var cookie = new HttpCookie(cookieName, hash) + { + Expires = DateTime.Now.AddMinutes(minutesPersisted), + Domain = cookieDomain + }; + //rewrite the cooke + http.Response.Cookies.Remove(cookieName); + http.Response.Cookies.Add(cookie); + return true; + } + + /// + /// Creates a custom FormsAuthentication ticket with the data specified + /// + /// The HTTP. + /// The username. + /// The user data. + /// The login timeout mins. + /// The minutes persisted. + /// The cookie path. + /// Name of the cookie. + /// The cookie domain. + private static void CreateAuthTicket(this HttpContextBase http, + string username, + string userData, + int loginTimeoutMins, + int minutesPersisted, + string cookiePath, + string cookieName, + string cookieDomain) + { + // Create a new ticket used for authentication + var ticket = new FormsAuthenticationTicket( + 4, + username, + DateTime.Now, + DateTime.Now.AddMinutes(loginTimeoutMins), + true, + userData, + cookiePath + ); + + // Encrypt the cookie using the machine key for secure transport + var hash = FormsAuthentication.Encrypt(ticket); + var cookie = new HttpCookie( + cookieName, + hash) + { + Expires = DateTime.Now.AddMinutes(minutesPersisted), + Domain = cookieDomain + }; + + http.Response.Cookies.Set(cookie); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs new file mode 100644 index 0000000000..3223e37faf --- /dev/null +++ b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs @@ -0,0 +1,99 @@ +using System.Web.Security; +using Newtonsoft.Json; + +namespace Umbraco.Core.Security +{ + /// + /// A custom user identity for the Umbraco backoffice + /// + /// + /// All values are lazy loaded for performance reasons as the constructor is called for every single request + /// + public class UmbracoBackOfficeIdentity : FormsIdentity + { + public UmbracoBackOfficeIdentity(FormsAuthenticationTicket ticket) + : base(ticket) + { + UserData = ticket.UserData; + } + + protected readonly string UserData; + internal UserData DeserializedData; + + public int StartContentNode + { + get + { + EnsureDeserialized(); + return DeserializedData.StartContentNode; + } + } + + public int StartMediaNode + { + get + { + EnsureDeserialized(); + return DeserializedData.StartMediaNode; + } + } + + public string[] AllowedApplications + { + get + { + EnsureDeserialized(); + return DeserializedData.AllowedApplications; + } + } + + public int Id + { + get + { + EnsureDeserialized(); + return DeserializedData.Id; + } + } + + public string RealName + { + get + { + EnsureDeserialized(); + return DeserializedData.RealName; + } + } + + //public int SessionTimeout + //{ + // get + // { + // EnsureDeserialized(); + // return DeserializedData.SessionTimeout; + // } + //} + + public string[] Roles + { + get + { + EnsureDeserialized(); + return DeserializedData.Roles; + } + } + + protected void EnsureDeserialized() + { + if (DeserializedData != null) + return; + + if (string.IsNullOrEmpty(UserData)) + { + DeserializedData = new UserData(); + return; + } + DeserializedData = JsonConvert.DeserializeObject(UserData); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/UserData.cs b/src/Umbraco.Core/Security/UserData.cs new file mode 100644 index 0000000000..61d337d813 --- /dev/null +++ b/src/Umbraco.Core/Security/UserData.cs @@ -0,0 +1,40 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Core.Security +{ + /// + /// Data structure used to store information in the authentication cookie + /// + [DataContract(Name = "userData", Namespace = "")] + internal class UserData + { + public UserData() + { + AllowedApplications = new string[] {}; + Roles = new string[] {}; + } + + [DataMember(Name = "id")] + public int Id { get; set; } + + [DataMember(Name = "roles")] + public string[] Roles { get; set; } + + //public int SessionTimeout { get; set; } + + [DataMember(Name = "username")] + public string Username { get; set; } + + [DataMember(Name = "name")] + public string RealName { get; set; } + + [DataMember(Name = "startContent")] + public int StartContentNode { get; set; } + + [DataMember(Name = "startMedia")] + public int StartMediaNode { get; set; } + + [DataMember(Name = "allowedApps")] + public string[] AllowedApplications { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 137a2bfbe5..385f1fcc50 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -701,6 +701,9 @@ + + + diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.config b/src/Umbraco.Web.UI/config/umbracoSettings.config index 4510e6ae2f..2f6a19a663 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.config @@ -116,6 +116,7 @@ false + diff --git a/src/Umbraco.Web.UI/install/Default.aspx.cs b/src/Umbraco.Web.UI/install/Default.aspx.cs index b9b848aab6..5a95459020 100644 --- a/src/Umbraco.Web.UI/install/Default.aspx.cs +++ b/src/Umbraco.Web.UI/install/Default.aspx.cs @@ -63,7 +63,7 @@ namespace Umbraco.Web.UI.Install // It's not considered an upgrade if the ConfigurationStatus is missing or empty. if (string.IsNullOrWhiteSpace(GlobalSettings.ConfigurationStatus) == false) { - var result = Security.ValidateCurrentUser(); + var result = Security.ValidateCurrentUser(false); if (result == ValidateRequestAttempt.FailedTimedOut || result == ValidateRequestAttempt.FailedNoPrivileges) { diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index 2f1fd24a5d..457391dc42 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -50,9 +50,7 @@ namespace Umbraco.Web.Editors var attempt = UmbracoContext.Security.AuthorizeRequest(); if (attempt == ValidateRequestAttempt.Success) { - var user = - Services.UserService.GetUserById( - UmbracoContext.Security.GetUserId(UmbracoContext.Security.UmbracoUserContextId)); + var user = Services.UserService.GetUserById(UmbracoContext.Security.GetUserId()); return _userModelMapper.ToUserDetail(user); } diff --git a/src/Umbraco.Web/Install/UmbracoInstallAuthorizeAttribute.cs b/src/Umbraco.Web/Install/UmbracoInstallAuthorizeAttribute.cs index 994703bfd3..1ea41f6e58 100644 --- a/src/Umbraco.Web/Install/UmbracoInstallAuthorizeAttribute.cs +++ b/src/Umbraco.Web/Install/UmbracoInstallAuthorizeAttribute.cs @@ -16,6 +16,20 @@ namespace Umbraco.Web.Install private readonly ApplicationContext _applicationContext; private readonly UmbracoContext _umbracoContext; + private ApplicationContext GetApplicationContext() + { + return _applicationContext ?? ApplicationContext.Current; + } + + private UmbracoContext GetUmbracoContext() + { + return _umbracoContext ?? UmbracoContext.Current; + } + + /// + /// THIS SHOULD BE ONLY USED FOR UNIT TESTS + /// + /// public UmbracoInstallAuthorizeAttribute(UmbracoContext umbracoContext) { if (umbracoContext == null) throw new ArgumentNullException("umbracoContext"); @@ -24,9 +38,7 @@ namespace Umbraco.Web.Install } public UmbracoInstallAuthorizeAttribute() - : this(UmbracoContext.Current) - { - + { } /// @@ -41,13 +53,13 @@ namespace Umbraco.Web.Install try { //if its not configured then we can continue - if (!_applicationContext.IsConfigured) + if (!GetApplicationContext().IsConfigured) { return true; } //otherwise we need to ensure that a user is logged in - var isLoggedIn = _umbracoContext.Security.ValidateUserContextId(_umbracoContext.Security.UmbracoUserContextId); + var isLoggedIn = GetUmbracoContext().Security.ValidateCurrentUser(); if (isLoggedIn) { return true; diff --git a/src/Umbraco.Web/Mvc/MemberAuthorizeAttribute.cs b/src/Umbraco.Web/Mvc/MemberAuthorizeAttribute.cs index bdcbd275ba..dd9720cdcf 100644 --- a/src/Umbraco.Web/Mvc/MemberAuthorizeAttribute.cs +++ b/src/Umbraco.Web/Mvc/MemberAuthorizeAttribute.cs @@ -18,18 +18,24 @@ namespace Umbraco.Web.Mvc public sealed class MemberAuthorizeAttribute : AuthorizeAttribute { - private readonly ApplicationContext _applicationContext; private readonly UmbracoContext _umbracoContext; + private UmbracoContext GetUmbracoContext() + { + return _umbracoContext ?? UmbracoContext.Current; + } + + /// + /// THIS SHOULD BE ONLY USED FOR UNIT TESTS + /// + /// public MemberAuthorizeAttribute(UmbracoContext umbracoContext) { if (umbracoContext == null) throw new ArgumentNullException("umbracoContext"); _umbracoContext = umbracoContext; - _applicationContext = _umbracoContext.Application; } public MemberAuthorizeAttribute() - : this(UmbracoContext.Current) { } @@ -76,7 +82,7 @@ namespace Umbraco.Web.Mvc } } - return _umbracoContext.Security.IsMemberAuthorized(AllowAll, + return GetUmbracoContext().Security.IsMemberAuthorized(AllowAll, AllowType.Split(','), AllowGroup.Split(','), members); diff --git a/src/Umbraco.Web/Mvc/UmbracoAuthorizeAttribute.cs b/src/Umbraco.Web/Mvc/UmbracoAuthorizeAttribute.cs index 084ef39b62..fe55d69d29 100644 --- a/src/Umbraco.Web/Mvc/UmbracoAuthorizeAttribute.cs +++ b/src/Umbraco.Web/Mvc/UmbracoAuthorizeAttribute.cs @@ -15,6 +15,20 @@ namespace Umbraco.Web.Mvc private readonly ApplicationContext _applicationContext; private readonly UmbracoContext _umbracoContext; + private ApplicationContext GetApplicationContext() + { + return _applicationContext ?? ApplicationContext.Current; + } + + private UmbracoContext GetUmbracoContext() + { + return _umbracoContext ?? UmbracoContext.Current; + } + + /// + /// THIS SHOULD BE ONLY USED FOR UNIT TESTS + /// + /// public UmbracoAuthorizeAttribute(UmbracoContext umbracoContext) { if (umbracoContext == null) throw new ArgumentNullException("umbracoContext"); @@ -23,9 +37,7 @@ namespace Umbraco.Web.Mvc } public UmbracoAuthorizeAttribute() - : this(UmbracoContext.Current) { - } /// @@ -38,11 +50,14 @@ namespace Umbraco.Web.Mvc if (httpContext == null) throw new ArgumentNullException("httpContext"); try - { + { + var appContext = GetApplicationContext(); + var umbContext = GetUmbracoContext(); + //we need to that the app is configured and that a user is logged in - if (!_applicationContext.IsConfigured) + if (!appContext.IsConfigured) return false; - var isLoggedIn = _umbracoContext.Security.ValidateUserContextId(_umbracoContext.Security.UmbracoUserContextId); + var isLoggedIn = umbContext.Security.ValidateCurrentUser(); return isLoggedIn; } catch (Exception) diff --git a/src/Umbraco.Web/Mvc/UmbracoAuthorizedController.cs b/src/Umbraco.Web/Mvc/UmbracoAuthorizedController.cs index ae62f7acee..83c3b6ee23 100644 --- a/src/Umbraco.Web/Mvc/UmbracoAuthorizedController.cs +++ b/src/Umbraco.Web/Mvc/UmbracoAuthorizedController.cs @@ -21,19 +21,8 @@ namespace Umbraco.Web.Mvc public abstract class UmbracoAuthorizedController : UmbracoController { - private User _user; private bool _userisValidated = false; - /// - /// The current user ID - /// - private int _uid = 0; - - /// - /// The page timeout in seconds. - /// - private long _timeout = 0; - /// /// Returns the currently logged in Umbraco User /// @@ -41,40 +30,13 @@ namespace Umbraco.Web.Mvc { get { - if (!_userisValidated) ValidateUser(); - return _user; - } - } - - private void ValidateUser() - { - if ((UmbracoContext.Security.UmbracoUserContextId != "")) - { - _uid = UmbracoContext.Security.GetUserId(UmbracoContext.Security.UmbracoUserContextId); - _timeout = UmbracoContext.Security.GetTimeout(UmbracoContext.Security.UmbracoUserContextId); - - if (_timeout > DateTime.Now.Ticks) + if (!_userisValidated) { - _user = global::umbraco.BusinessLogic.User.GetUser(_uid); - - // Check for console access - if (_user.Disabled || (_user.NoConsole && GlobalSettings.RequestIsInUmbracoApplication(HttpContext) && !GlobalSettings.RequestIsLiveEditRedirector(HttpContext))) - { - throw new ArgumentException("You have no priviledges to the umbraco console. Please contact your administrator"); - } + Security.ValidateCurrentUser(); _userisValidated = true; - UmbracoContext.Security.UpdateLogin(_timeout); - } - else - { - throw new ArgumentException("User has timed out!!"); } + return Security.CurrentUser; } - else - { - throw new InvalidOperationException("The user has no umbraco contextid - try logging in"); - } - } } diff --git a/src/Umbraco.Web/Mvc/UmbracoController.cs b/src/Umbraco.Web/Mvc/UmbracoController.cs index 78dc6502fc..99efb77f82 100644 --- a/src/Umbraco.Web/Mvc/UmbracoController.cs +++ b/src/Umbraco.Web/Mvc/UmbracoController.cs @@ -2,6 +2,7 @@ using System.Web.Mvc; using Umbraco.Core; using Umbraco.Core.Services; +using Umbraco.Web.Security; namespace Umbraco.Web.Mvc { @@ -59,5 +60,13 @@ namespace Umbraco.Web.Mvc { get { return ApplicationContext.DatabaseContext; } } + + /// + /// Returns the WebSecurity instance + /// + public WebSecurity Security + { + get { return UmbracoContext.Security; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index db59d41c2a..ab7286dec1 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -3,10 +3,12 @@ using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Security; +using Newtonsoft.Json.Linq; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; +using Umbraco.Core.Security; using umbraco.BusinessLogic; using umbraco.DataLayer; using umbraco.businesslogic.Exceptions; @@ -17,15 +19,19 @@ namespace Umbraco.Web.Security /// /// A utility class used for dealing with security in Umbraco /// - public class WebSecurity + public class WebSecurity : DisposableObject { - private readonly HttpContextBase _httpContext; + private HttpContextBase _httpContext; public WebSecurity(HttpContextBase httpContext) { _httpContext = httpContext; + //This ensures the dispose method is called when the request terminates, though + // we also ensure this happens in the Umbraco module because the UmbracoContext is added to the + // http context items. + _httpContext.DisposeOnPipelineCompleted(this); } - + /// /// Returns true or false if the currently logged in member is authorized based on the parameters provided /// @@ -90,16 +96,6 @@ namespace Umbraco.Web.Security return allowAction; } - /// - /// Gets the SQL helper. - /// - /// The SQL helper. - private ISqlHelper SqlHelper - { - get { return Application.SqlHelper; } - } - - private const long TicksPrMinute = 600000000; private static readonly int UmbracoTimeOutInMinutes = GlobalSettings.TimeOutInMinutes; private User _currentUser; @@ -117,7 +113,17 @@ namespace Umbraco.Web.Security get { //only load it once per instance! - return _currentUser ?? (_currentUser = User.GetCurrent()); + if (_currentUser == null) + { + var id = GetUserId(); + if (id == -1) + { + return null; + } + _currentUser = User.GetUser(id); + } + + return _currentUser; } } @@ -127,16 +133,25 @@ namespace Umbraco.Web.Security /// The user Id public void PerformLogin(int userId) { - var retVal = Guid.NewGuid(); - SqlHelper.ExecuteNonQuery( - "insert into umbracoUserLogins (contextID, userID, timeout) values (@contextId,'" + userId + "','" + - (DateTime.Now.Ticks + (TicksPrMinute * UmbracoTimeOutInMinutes)) + - "') ", - SqlHelper.CreateParameter("@contextId", retVal)); - UmbracoUserContextId = retVal.ToString(); + var user = User.GetUser(userId); + PerformLogin(user); + } - LogHelper.Info("User Id: {0} logged in", () => userId); + internal void PerformLogin(User user) + { + _httpContext.CreateUmbracoAuthTicket(new UserData + { + Id = user.Id, + AllowedApplications = user.GetApplications().Select(x => x.alias).ToArray(), + RealName = user.Name, + //currently we only have one user type! + Roles = new[] { user.UserType.Alias }, + StartContentNode = user.StartNodeId, + StartMediaNode = user.StartMediaId, + Username = user.LoginName + }); + LogHelper.Info("User Id: {0} logged in", () => user.Id); } /// @@ -144,30 +159,12 @@ namespace Umbraco.Web.Security /// public void ClearCurrentLogin() { - // Added try-catch in case login doesn't exist in the database - // Either due to old cookie or running multiple sessions on localhost with different port number - try - { - SqlHelper.ExecuteNonQuery( - "DELETE FROM umbracoUserLogins WHERE contextId = @contextId", - SqlHelper.CreateParameter("@contextId", UmbracoUserContextId)); - } - catch (Exception ex) - { - LogHelper.Error(string.Format("Login with contextId {0} didn't exist in the database", UmbracoUserContextId), ex); - } - - //this clears the cookie - UmbracoUserContextId = ""; + _httpContext.UmbracoLogout(); } public void RenewLoginTimeout() { - // only call update if more than 1/10 of the timeout has passed - SqlHelper.ExecuteNonQuery( - "UPDATE umbracoUserLogins SET timeout = @timeout WHERE contextId = @contextId", - SqlHelper.CreateParameter("@timeout", DateTime.Now.Ticks + (TicksPrMinute * UmbracoTimeOutInMinutes)), - SqlHelper.CreateParameter("@contextId", UmbracoUserContextId)); + _httpContext.RenewUmbracoAuthTicket(UmbracoTimeOutInMinutes); } /// @@ -215,82 +212,65 @@ namespace Umbraco.Web.Security return CurrentUser.Applications.Any(uApp => uApp.alias == app); } - internal void UpdateLogin(long timeout) + internal void UpdateLogin() { - // only call update if more than 1/10 of the timeout has passed - if (timeout - (((TicksPrMinute * UmbracoTimeOutInMinutes) * 0.8)) < DateTime.Now.Ticks) - SqlHelper.ExecuteNonQuery( - "UPDATE umbracoUserLogins SET timeout = @timeout WHERE contextId = @contextId", - SqlHelper.CreateParameter("@timeout", DateTime.Now.Ticks + (TicksPrMinute * UmbracoTimeOutInMinutes)), - SqlHelper.CreateParameter("@contextId", UmbracoUserContextId)); + _httpContext.RenewUmbracoAuthTicket(UmbracoTimeOutInMinutes); } - internal long GetTimeout(string umbracoUserContextId) + internal long GetTimeout() { - return ApplicationContext.Current.ApplicationCache.GetCacheItem( - CacheKeys.UserContextTimeoutCacheKey + umbracoUserContextId, - new TimeSpan(0, UmbracoTimeOutInMinutes / 10, 0), - () => GetTimeout(true)); + var ticket = _httpContext.GetUmbracoAuthTicket(); + var ticks = ticket.Expiration.Ticks - DateTime.Now.Ticks; + return ticks; } - - internal long GetTimeout(bool byPassCache) - { - if (UmbracoSettings.KeepUserLoggedIn) - RenewLoginTimeout(); - - if (byPassCache) - { - return SqlHelper.ExecuteScalar("select timeout from umbracoUserLogins where contextId=@contextId", - SqlHelper.CreateParameter("@contextId", new Guid(UmbracoUserContextId)) - ); - } - - return GetTimeout(UmbracoUserContextId); - } - + /// /// Gets the user id. /// - /// The umbraco user context ID. + /// This is not used /// + [Obsolete("This method is no longer used, use the GetUserId() method without parameters instead")] public int GetUserId(string umbracoUserContextId) - { - //need to parse to guid - Guid guid; - if (Guid.TryParse(umbracoUserContextId, out guid) == false) - { - return -1; - } + { + return GetUserId(); + } - var id = ApplicationContext.Current.ApplicationCache.GetCacheItem( - CacheKeys.UserContextCacheKey + umbracoUserContextId, - new TimeSpan(0, UmbracoTimeOutInMinutes / 10, 0), - () => SqlHelper.ExecuteScalar( - "select userID from umbracoUserLogins where contextID = @contextId", - SqlHelper.CreateParameter("@contextId", guid))); - if (id == null) + /// + /// Gets the currnet user's id. + /// + /// + public int GetUserId() + { + var identity = _httpContext.GetCurrentIdentity(); + if (identity == null) return -1; - return id.Value; + return identity.Id; } /// /// Validates the user context ID. /// - /// The umbraco user context ID. + /// This doesn't do anything /// + [Obsolete("This method is no longer used, use the ValidateCurrentUser() method instead")] public bool ValidateUserContextId(string currentUmbracoUserContextId) { - if ((currentUmbracoUserContextId != "")) - { - int uid = GetUserId(currentUmbracoUserContextId); - long timeout = GetTimeout(currentUmbracoUserContextId); + return ValidateCurrentUser(); + } - if (timeout > DateTime.Now.Ticks) + /// + /// Validates the currently logged in user and ensures they are not timed out + /// + /// + public bool ValidateCurrentUser() + { + var ticket = _httpContext.GetUmbracoAuthTicket(); + if (ticket != null) + { + if (ticket.Expired == false) { return true; } - var user = User.GetUser(uid); - LogHelper.Info(typeof(WebSecurity), "User {0} (Id:{1}) logged out", () => user.Name, () => user.Id); } return false; } @@ -300,16 +280,15 @@ namespace Umbraco.Web.Security /// /// set to true if you want exceptions to be thrown if failed /// - internal ValidateRequestAttempt ValidateCurrentUser(bool throwExceptions = false) + internal ValidateRequestAttempt ValidateCurrentUser(bool throwExceptions) { - if (UmbracoUserContextId != "") - { - var uid = GetUserId(UmbracoUserContextId); - var timeout = GetTimeout(UmbracoUserContextId); + var ticket = _httpContext.GetUmbracoAuthTicket(); - if (timeout > DateTime.Now.Ticks) + if (ticket != null) + { + if (ticket.Expired == false) { - var user = User.GetUser(uid); + var user = User.GetUser(GetUserId()); // Check for console access if (user.Disabled || (user.NoConsole && GlobalSettings.RequestIsInUmbracoApplication(_httpContext) && GlobalSettings.RequestIsLiveEditRedirector(_httpContext) == false)) @@ -317,12 +296,13 @@ namespace Umbraco.Web.Security if (throwExceptions) throw new ArgumentException("You have no priviledges to the umbraco console. Please contact your administrator"); return ValidateRequestAttempt.FailedNoPrivileges; } - UpdateLogin(timeout); + UpdateLogin(); return ValidateRequestAttempt.Success; } if (throwExceptions) throw new ArgumentException("User has timed out!!"); return ValidateRequestAttempt.FailedTimedOut; } + if (throwExceptions) throw new InvalidOperationException("The user has no umbraco contextid - try logging in"); return ValidateRequestAttempt.FailedNoContextId; } @@ -369,62 +349,21 @@ namespace Umbraco.Web.Security return UserHasAppAccess(app, usr); } - /// - /// Gets or sets the umbraco user context ID. - /// - /// The umbraco user context ID. + [Obsolete("This is no longer used at all, it will always return a new GUID though if a user is logged in")] public string UmbracoUserContextId - { + { get { - if (StateHelper.Cookies.HasCookies && StateHelper.Cookies.UserContext.HasValue) - { - try - { - var encTicket = StateHelper.Cookies.UserContext.GetValue(); - if (string.IsNullOrEmpty(encTicket) == false) - { - return encTicket.DecryptWithMachineKey(); - } - } - catch (Exception ex) - { - if (ex is ArgumentException || ex is FormatException || ex is HttpException) - { - StateHelper.Cookies.UserContext.Clear(); - } - else - { - throw; - } - } - } - return ""; + return _httpContext.GetUmbracoAuthTicket() == null ? "" : Guid.NewGuid().ToString(); } set { - // zb-00004 #29956 : refactor cookies names & handling - if (StateHelper.Cookies.HasCookies) - { - // Clearing all old cookies before setting a new one. - if (StateHelper.Cookies.UserContext.HasValue) - StateHelper.Cookies.ClearAll(); - - if (string.IsNullOrEmpty(value) == false) - { - // Encrypt the value - var encTicket = value.EncryptWithMachineKey(); - - // Create new cookie. - StateHelper.Cookies.UserContext.SetValue(encTicket, 1); - } - else - { - StateHelper.Cookies.UserContext.Clear(); - } - } } } + protected override void DisposeResources() + { + _httpContext = null; + } } } diff --git a/src/Umbraco.Web/UmbracoContext.cs b/src/Umbraco.Web/UmbracoContext.cs index 7542764abb..e3756ae26e 100644 --- a/src/Umbraco.Web/UmbracoContext.cs +++ b/src/Umbraco.Web/UmbracoContext.cs @@ -26,7 +26,7 @@ namespace Umbraco.Web /// /// Class that encapsulates Umbraco information of a specific HTTP request /// - public class UmbracoContext + public class UmbracoContext : DisposableObject { private const string HttpContextItemName = "Umbraco.Web.UmbracoContext"; private static readonly object Locker = new object(); @@ -37,6 +37,7 @@ namespace Umbraco.Web /// /// Used if not running in a web application (no real HttpContext) /// + [ThreadStatic] private static UmbracoContext _umbracoContext; /// @@ -127,6 +128,11 @@ namespace Umbraco.Web IPublishedCaches publishedCaches, bool? preview = null) { + //This ensures the dispose method is called when the request terminates, though + // we also ensure this happens in the Umbraco module because the UmbracoContext is added to the + // http context items. + httpContext.DisposeOnPipelineCompleted(this); + if (httpContext == null) throw new ArgumentNullException("httpContext"); if (applicationContext == null) throw new ArgumentNullException("applicationContext"); @@ -366,5 +372,15 @@ namespace Umbraco.Web } + protected override void DisposeResources() + { + Security.Dispose(); + Security = null; + _previewContent = null; + _umbracoContext = null; + Application = null; + ContentCache = null; + MediaCache = null; + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index 6b4668854a..f7df4bab03 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -1,12 +1,17 @@ using System; +using System.Collections; using System.IO; using System.Linq; +using System.Security.Principal; +using System.Threading; using System.Web; using System.Web.Routing; using Umbraco.Core; using Umbraco.Core.IO; using Umbraco.Core.Logging; +using Umbraco.Core.Security; using Umbraco.Web.Routing; +using Umbraco.Web.Security; using umbraco; using GlobalSettings = Umbraco.Core.Configuration.GlobalSettings; using UmbracoSettings = Umbraco.Core.Configuration.UmbracoSettings; @@ -28,7 +33,7 @@ namespace Umbraco.Web /// Begins to process a request. /// /// - void BeginRequest(HttpContextBase httpContext) + static void BeginRequest(HttpContextBase httpContext) { //we need to set the initial url in our ApplicationContext, this is so our keep alive service works and this must //exist on a global context because the keep alive service doesn't run in a web context. @@ -47,16 +52,16 @@ namespace Umbraco.Web //write the trace output for diagnostics at the end of the request httpContext.Trace.Write("UmbracoModule", "Umbraco request begins"); - // ok, process + // ok, process - // create the LegacyRequestInitializer - // and initialize legacy stuff - var legacyRequestInitializer = new LegacyRequestInitializer(httpContext.Request.Url, httpContext); - legacyRequestInitializer.InitializeRequest(); + // create the LegacyRequestInitializer + // and initialize legacy stuff + var legacyRequestInitializer = new LegacyRequestInitializer(httpContext.Request.Url, httpContext); + legacyRequestInitializer.InitializeRequest(); - // create the UmbracoContext singleton, one per request, and assign - // NOTE: we assign 'true' to ensure the context is replaced if it is already set (i.e. during app startup) - UmbracoContext.EnsureContext(httpContext, ApplicationContext.Current, true); + // create the UmbracoContext singleton, one per request, and assign + // NOTE: we assign 'true' to ensure the context is replaced if it is already set (i.e. during app startup) + UmbracoContext.EnsureContext(httpContext, ApplicationContext.Current, true); } /// @@ -133,74 +138,49 @@ namespace Umbraco.Web RewriteToUmbracoHandler(httpContext, pcr); } - // returns a value indicating whether redirection took place and the request has - // been completed - because we don't want to Response.End() here to terminate - // everything properly. - internal static bool HandleHttpResponseStatus(HttpContextBase context, PublishedContentRequest pcr) + /// + /// Authenticates the request by reading the FormsAuthentication cookie and setting the + /// context and thread principle object + /// + /// + /// + static void AuthenticateRequest(object sender, EventArgs e) { - var end = false; - var response = context.Response; + var app = (HttpApplication)sender; + var http = new HttpContextWrapper(app.Context); - LogHelper.Debug("Response status: Redirect={0}, Is404={1}, StatusCode={2}", - () => pcr.IsRedirect ? (pcr.IsRedirectPermanent ? "permanent" : "redirect") : "none", - () => pcr.Is404 ? "true" : "false", () => pcr.ResponseStatusCode); + // do not process if client-side request + if (http.Request.Url.IsClientSideRequest()) + return; - if (pcr.IsRedirect) + if (app.Request.Url.IsBackOfficeRequest() || app.Request.Url.IsInstallerRequest()) { - if (pcr.IsRedirectPermanent) - response.RedirectPermanent(pcr.RedirectUrl, false); // do not end response - else - response.Redirect(pcr.RedirectUrl, false); // do not end response - end = true; - } - else if (pcr.Is404) - { - response.StatusCode = 404; - response.TrySkipIisCustomErrors = UmbracoSettings.For().TrySkipIisCustomErrors; + var ticket = http.GetUmbracoAuthTicket(); + if (ticket != null && !ticket.Expired && http.RenewUmbracoAuthTicket()) + { + //create the Umbraco user identity + var identity = new UmbracoBackOfficeIdentity(ticket); + + //set the principal object + var principal = new GenericPrincipal(identity, identity.Roles); + + //It is actually not good enough to set this on the current app Context and the thread, it also needs + // to be set explicitly on the HttpContext.Current !! This is a strange web api thing that is actually + // an underlying fault of asp.net not propogating the User correctly. + if (HttpContext.Current != null) + { + HttpContext.Current.User = principal; + } + app.Context.User = principal; + Thread.CurrentPrincipal = principal; + } } - if (pcr.ResponseStatusCode > 0) - { - // set status code -- even for redirects - response.StatusCode = pcr.ResponseStatusCode; - response.StatusDescription = pcr.ResponseStatusDescription; - } - //if (pcr.IsRedirect) - // response.End(); // end response -- kills the thread and does not return! - - if (pcr.IsRedirect) - { - response.Flush(); - // bypass everything and directly execute EndRequest event -- but returns - context.ApplicationInstance.CompleteRequest(); - // though some say that .CompleteRequest() does not properly shutdown the response - // and the request will hang until the whole code has run... would need to test? - LogHelper.Debug("Response status: redirecting, complete request now."); - } - - return end; } - /// - /// Checks if the xml cache file needs to be updated/persisted - /// - /// - /// - /// TODO: This needs an overhaul, see the error report created here: - /// https://docs.google.com/document/d/1neGE3q3grB4lVJfgID1keWY2v9JYqf-pw75sxUUJiyo/edit - /// - void PersistXmlCache(HttpContextBase httpContext) - { - if (content.Instance.IsXmlQueuedForPersistenceToFile) - { - content.Instance.RemoveXmlFilePersistenceQueue(); - content.Instance.PersistXmlToFile(); - } - } - #endregion - #region Route helper methods + #region Methods /// /// Checks the current request and ensures that it is routable based on the structure of the request and URI @@ -244,7 +224,7 @@ namespace Umbraco.Web /// /// /// - bool EnsureDocumentRequest(HttpContextBase httpContext, Uri uri) + static bool EnsureDocumentRequest(HttpContextBase httpContext, Uri uri) { var maybeDoc = true; var lpath = uri.AbsolutePath.ToLowerInvariant(); @@ -293,7 +273,7 @@ namespace Umbraco.Web // ensures Umbraco is ready to handle requests // if not, set status to 503 and transfer request, and return false // if yes, return true - bool EnsureIsReady(HttpContextBase httpContext, Uri uri) + static bool EnsureIsReady(HttpContextBase httpContext, Uri uri) { var ready = ApplicationContext.Current.IsReady; @@ -358,13 +338,59 @@ namespace Umbraco.Web return false; } - #endregion + // returns a value indicating whether redirection took place and the request has + // been completed - because we don't want to Response.End() here to terminate + // everything properly. + internal static bool HandleHttpResponseStatus(HttpContextBase context, PublishedContentRequest pcr) + { + var end = false; + var response = context.Response; + + LogHelper.Debug("Response status: Redirect={0}, Is404={1}, StatusCode={2}", + () => pcr.IsRedirect ? (pcr.IsRedirectPermanent ? "permanent" : "redirect") : "none", + () => pcr.Is404 ? "true" : "false", () => pcr.ResponseStatusCode); + + if (pcr.IsRedirect) + { + if (pcr.IsRedirectPermanent) + response.RedirectPermanent(pcr.RedirectUrl, false); // do not end response + else + response.Redirect(pcr.RedirectUrl, false); // do not end response + end = true; + } + else if (pcr.Is404) + { + response.StatusCode = 404; + response.TrySkipIisCustomErrors = UmbracoSettings.For().TrySkipIisCustomErrors; + } + + if (pcr.ResponseStatusCode > 0) + { + // set status code -- even for redirects + response.StatusCode = pcr.ResponseStatusCode; + response.StatusDescription = pcr.ResponseStatusDescription; + } + //if (pcr.IsRedirect) + // response.End(); // end response -- kills the thread and does not return! + + if (pcr.IsRedirect) + { + response.Flush(); + // bypass everything and directly execute EndRequest event -- but returns + context.ApplicationInstance.CompleteRequest(); + // though some say that .CompleteRequest() does not properly shutdown the response + // and the request will hang until the whole code has run... would need to test? + LogHelper.Debug("Response status: redirecting, complete request now."); + } + + return end; + } /// /// Rewrites to the default back office page. /// /// - private void RewriteToBackOfficeHandler(HttpContextBase context) + private static void RewriteToBackOfficeHandler(HttpContextBase context) { // GlobalSettings.Path has already been through IOHelper.ResolveUrl() so it begins with / and vdir (if any) var rewritePath = GlobalSettings.Path.TrimEnd(new[] { '/' }) + "/Default"; @@ -384,21 +410,21 @@ namespace Umbraco.Web urlRouting.PostResolveRequestCache(context); } - /// - /// Rewrites to the correct Umbraco handler, either WebForms or Mvc - /// - /// + /// + /// Rewrites to the correct Umbraco handler, either WebForms or Mvc + /// + /// /// - private void RewriteToUmbracoHandler(HttpContextBase context, PublishedContentRequest pcr) - { - // NOTE: we do not want to use TransferRequest even though many docs say it is better with IIS7, turns out this is - // not what we need. The purpose of TransferRequest is to ensure that .net processes all of the rules for the newly - // rewritten url, but this is not what we want! - // read: http://forums.iis.net/t/1146511.aspx + private static void RewriteToUmbracoHandler(HttpContextBase context, PublishedContentRequest pcr) + { + // NOTE: we do not want to use TransferRequest even though many docs say it is better with IIS7, turns out this is + // not what we need. The purpose of TransferRequest is to ensure that .net processes all of the rules for the newly + // rewritten url, but this is not what we want! + // read: http://forums.iis.net/t/1146511.aspx - string query = pcr.Uri.Query.TrimStart(new[] { '?' }); + string query = pcr.Uri.Query.TrimStart(new[] { '?' }); - string rewritePath; + string rewritePath; if (pcr.RenderingEngine == RenderingEngine.Unknown) { @@ -409,37 +435,69 @@ namespace Umbraco.Web pcr.RenderingEngine = RenderingEngine.Mvc; } - switch (pcr.RenderingEngine) - { - case RenderingEngine.Mvc: - // GlobalSettings.Path has already been through IOHelper.ResolveUrl() so it begins with / and vdir (if any) - rewritePath = GlobalSettings.Path.TrimEnd(new[] { '/' }) + "/RenderMvc"; - // rewrite the path to the path of the handler (i.e. /umbraco/RenderMvc) - context.RewritePath(rewritePath, "", query, false); + switch (pcr.RenderingEngine) + { + case RenderingEngine.Mvc: + // GlobalSettings.Path has already been through IOHelper.ResolveUrl() so it begins with / and vdir (if any) + rewritePath = GlobalSettings.Path.TrimEnd(new[] { '/' }) + "/RenderMvc"; + // rewrite the path to the path of the handler (i.e. /umbraco/RenderMvc) + context.RewritePath(rewritePath, "", query, false); - //if it is MVC we need to do something special, we are not using TransferRequest as this will - //require us to rewrite the path with query strings and then reparse the query strings, this would - //also mean that we need to handle IIS 7 vs pre-IIS 7 differently. Instead we are just going to create - //an instance of the UrlRoutingModule and call it's PostResolveRequestCache method. This does: - // * Looks up the route based on the new rewritten URL - // * Creates the RequestContext with all route parameters and then executes the correct handler that matches the route - //we also cannot re-create this functionality because the setter for the HttpContext.Request.RequestContext is internal - //so really, this is pretty much the only way without using Server.TransferRequest and if we did that, we'd have to rethink - //a bunch of things! - var urlRouting = new UrlRoutingModule(); - urlRouting.PostResolveRequestCache(context); - break; + //if it is MVC we need to do something special, we are not using TransferRequest as this will + //require us to rewrite the path with query strings and then reparse the query strings, this would + //also mean that we need to handle IIS 7 vs pre-IIS 7 differently. Instead we are just going to create + //an instance of the UrlRoutingModule and call it's PostResolveRequestCache method. This does: + // * Looks up the route based on the new rewritten URL + // * Creates the RequestContext with all route parameters and then executes the correct handler that matches the route + //we also cannot re-create this functionality because the setter for the HttpContext.Request.RequestContext is internal + //so really, this is pretty much the only way without using Server.TransferRequest and if we did that, we'd have to rethink + //a bunch of things! + var urlRouting = new UrlRoutingModule(); + urlRouting.PostResolveRequestCache(context); + break; - case RenderingEngine.WebForms: - rewritePath = "~/default.aspx"; - // rewrite the path to the path of the handler (i.e. default.aspx) - context.RewritePath(rewritePath, "", query, false); - break; + case RenderingEngine.WebForms: + rewritePath = "~/default.aspx"; + // rewrite the path to the path of the handler (i.e. default.aspx) + context.RewritePath(rewritePath, "", query, false); + break; default: throw new Exception("Invalid RenderingEngine."); } - } + } + + /// + /// Checks if the xml cache file needs to be updated/persisted + /// + /// + /// + /// TODO: This needs an overhaul, see the error report created here: + /// https://docs.google.com/document/d/1neGE3q3grB4lVJfgID1keWY2v9JYqf-pw75sxUUJiyo/edit + /// + static void PersistXmlCache(HttpContextBase httpContext) + { + if (content.Instance.IsXmlQueuedForPersistenceToFile) + { + content.Instance.RemoveXmlFilePersistenceQueue(); + content.Instance.PersistXmlToFile(); + } + } + + /// + /// Any object that is in the HttpContext.Items collection that is IDisposable will get disposed on the end of the request + /// + /// + private static void DisposeHttpContextItems(HttpContext http) + { + foreach (DictionaryEntry i in http.Items) + { + i.Value.DisposeIfDisposable(); + i.Key.DisposeIfDisposable(); + } + } + + #endregion #region IHttpModule @@ -458,6 +516,8 @@ namespace Umbraco.Web BeginRequest(new HttpContextWrapper(httpContext)); }; + app.AuthenticateRequest += AuthenticateRequest; + app.PostResolveRequestCache += (sender, e) => { var httpContext = ((HttpApplication)sender).Context; @@ -508,18 +568,6 @@ namespace Umbraco.Web #endregion - /// - /// Any object that is in the HttpContext.Items collection that is IDisposable will get disposed on the end of the request - /// - /// - private static void DisposeHttpContextItems(HttpContext http) - { - foreach(var i in http.Items) - { - i.DisposeIfDisposable(); - } - } - #region Events internal static event EventHandler RouteAttempt; private void OnRouteAttempt(RoutableAttemptEventArgs args) diff --git a/src/Umbraco.Web/WebApi/MemberAuthorizeAttribute.cs b/src/Umbraco.Web/WebApi/MemberAuthorizeAttribute.cs index bb15fe7c47..a0dcdd033c 100644 --- a/src/Umbraco.Web/WebApi/MemberAuthorizeAttribute.cs +++ b/src/Umbraco.Web/WebApi/MemberAuthorizeAttribute.cs @@ -14,20 +14,25 @@ namespace Umbraco.Web.WebApi /// to just authenticated members, and optionally of a particular type and/or group /// public sealed class MemberAuthorizeAttribute : AuthorizeAttribute - { - - private readonly ApplicationContext _applicationContext; + { private readonly UmbracoContext _umbracoContext; + private UmbracoContext GetUmbracoContext() + { + return _umbracoContext ?? UmbracoContext.Current; + } + + /// + /// THIS SHOULD BE ONLY USED FOR UNIT TESTS + /// + /// public MemberAuthorizeAttribute(UmbracoContext umbracoContext) { if (umbracoContext == null) throw new ArgumentNullException("umbracoContext"); _umbracoContext = umbracoContext; - _applicationContext = _umbracoContext.Application; } public MemberAuthorizeAttribute() - : this(UmbracoContext.Current) { } @@ -74,7 +79,7 @@ namespace Umbraco.Web.WebApi } } - return _umbracoContext.Security.IsMemberAuthorized(AllowAll, + return GetUmbracoContext().Security.IsMemberAuthorized(AllowAll, AllowType.Split(','), AllowGroup.Split(','), members); diff --git a/src/Umbraco.Web/WebApi/UmbracoApiController.cs b/src/Umbraco.Web/WebApi/UmbracoApiController.cs index e4c2b4a57e..5fbb2942c5 100644 --- a/src/Umbraco.Web/WebApi/UmbracoApiController.cs +++ b/src/Umbraco.Web/WebApi/UmbracoApiController.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.Validation; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Security; namespace Umbraco.Web.WebApi { @@ -80,6 +81,14 @@ namespace Umbraco.Web.WebApi /// public UmbracoContext UmbracoContext { get; private set; } + /// + /// Returns the WebSecurity instance + /// + public WebSecurity Security + { + get { return UmbracoContext.Security; } + } + /// /// Useful for debugging /// diff --git a/src/Umbraco.Web/WebApi/UmbracoAuthorizeAttribute.cs b/src/Umbraco.Web/WebApi/UmbracoAuthorizeAttribute.cs index dc83b043df..e804158b4a 100644 --- a/src/Umbraco.Web/WebApi/UmbracoAuthorizeAttribute.cs +++ b/src/Umbraco.Web/WebApi/UmbracoAuthorizeAttribute.cs @@ -13,6 +13,20 @@ namespace Umbraco.Web.WebApi private readonly ApplicationContext _applicationContext; private readonly UmbracoContext _umbracoContext; + private ApplicationContext GetApplicationContext() + { + return _applicationContext ?? ApplicationContext.Current; + } + + private UmbracoContext GetUmbracoContext() + { + return _umbracoContext ?? UmbracoContext.Current; + } + + /// + /// THIS SHOULD BE ONLY USED FOR UNIT TESTS + /// + /// public UmbracoAuthorizeAttribute(UmbracoContext umbracoContext) { if (umbracoContext == null) throw new ArgumentNullException("umbracoContext"); @@ -21,19 +35,22 @@ namespace Umbraco.Web.WebApi } public UmbracoAuthorizeAttribute() - : this(UmbracoContext.Current) { - } protected override bool IsAuthorized(System.Web.Http.Controllers.HttpActionContext actionContext) { try { + var appContext = GetApplicationContext(); + var umbContext = GetUmbracoContext(); + //we need to that the app is configured and that a user is logged in - if (!_applicationContext.IsConfigured) + if (appContext.IsConfigured == false) return false; - var isLoggedIn = _umbracoContext.Security.ValidateUserContextId(_umbracoContext.Security.UmbracoUserContextId); + + var isLoggedIn = umbContext.Security.ValidateCurrentUser(); + return isLoggedIn; } catch (Exception) diff --git a/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs b/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs index 9101e3ac66..d03c52e158 100644 --- a/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs +++ b/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs @@ -19,61 +19,26 @@ namespace Umbraco.Web.WebApi : base(umbracoContext) { } - - private User _user; + private bool _userisValidated = false; - - /// - /// The current user ID - /// - private int _uid = 0; - - /// - /// The page timeout in seconds. - /// - private long _timeout = 0; - + /// /// Returns the currently logged in Umbraco User /// protected User UmbracoUser { get - { - if (!_userisValidated) ValidateUser(); - return _user; - } - } - - private void ValidateUser() - { - if ((UmbracoContext.Security.UmbracoUserContextId != "")) - { - _uid = UmbracoContext.Security.GetUserId(UmbracoContext.Security.UmbracoUserContextId); - _timeout = UmbracoContext.Security.GetTimeout(UmbracoContext.Security.UmbracoUserContextId); - - if (_timeout > DateTime.Now.Ticks) + { + //throw exceptions if not valid (true) + if (!_userisValidated) { - _user = global::umbraco.BusinessLogic.User.GetUser(_uid); - - // Check for console access - if (_user.Disabled || (_user.NoConsole && GlobalSettings.RequestIsInUmbracoApplication(HttpContext.Current) && !GlobalSettings.RequestIsLiveEditRedirector(HttpContext.Current))) - { - throw new ArgumentException("You have no priviledges to the umbraco console. Please contact your administrator"); - } + Security.ValidateCurrentUser(true); _userisValidated = true; - UmbracoContext.Security.UpdateLogin(_timeout); } - else - { - throw new ArgumentException("User has timed out!!"); - } - } - else - { - throw new InvalidOperationException("The user has no umbraco contextid - try logging in"); - } + return Security.CurrentUser; + } } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/WebServices/UmbracoAuthorizedHttpHandler.cs b/src/Umbraco.Web/WebServices/UmbracoAuthorizedHttpHandler.cs index f83120f0d0..413a12511f 100644 --- a/src/Umbraco.Web/WebServices/UmbracoAuthorizedHttpHandler.cs +++ b/src/Umbraco.Web/WebServices/UmbracoAuthorizedHttpHandler.cs @@ -32,7 +32,7 @@ namespace Umbraco.Web.WebServices /// protected bool ValidateUserContextId(string currentUmbracoUserContextId) { - return UmbracoContext.Security.ValidateUserContextId(currentUmbracoUserContextId); + return UmbracoContext.Security.ValidateCurrentUser(); } /// diff --git a/src/Umbraco.Web/WebServices/UmbracoAuthorizedWebService.cs b/src/Umbraco.Web/WebServices/UmbracoAuthorizedWebService.cs index d59ed125d8..84ae905439 100644 --- a/src/Umbraco.Web/WebServices/UmbracoAuthorizedWebService.cs +++ b/src/Umbraco.Web/WebServices/UmbracoAuthorizedWebService.cs @@ -37,7 +37,7 @@ namespace Umbraco.Web.WebServices /// protected bool ValidateUserContextId(string currentUmbracoUserContextId) { - return UmbracoContext.Security.ValidateUserContextId(currentUmbracoUserContextId); + return UmbracoContext.Security.ValidateCurrentUser(); } /// diff --git a/src/umbraco.businesslogic/BasePages/BasePage.cs b/src/umbraco.businesslogic/BasePages/BasePage.cs index 99c1172b1c..b226176ae6 100644 --- a/src/umbraco.businesslogic/BasePages/BasePage.cs +++ b/src/umbraco.businesslogic/BasePages/BasePage.cs @@ -14,6 +14,7 @@ using Umbraco.Core.Services; using umbraco.BusinessLogic; using umbraco.DataLayer; using Umbraco.Core; +using Umbraco.Core.Security; namespace umbraco.BasePages { @@ -30,8 +31,6 @@ namespace umbraco.BasePages private bool _userisValidated = false; private ClientTools _clientTools; - // ticks per minute 600,000,000 - private const long TicksPrMinute = 600000000; private static readonly int UmbracoTimeOutInMinutes = GlobalSettings.TimeOutInMinutes; /// @@ -138,26 +137,21 @@ namespace umbraco.BasePages private void ValidateUser() { - if ((umbracoUserContextID != "")) - { - uid = GetUserId(umbracoUserContextID); - timeout = GetTimeout(umbracoUserContextID); + var ticket = Context.GetUmbracoAuthTicket(); - if (timeout > DateTime.Now.Ticks) + if (ticket != null) + { + if (ticket.Expired == false) { - _user = BusinessLogic.User.GetUser(uid); + _user = BusinessLogic.User.GetUser(GetUserId("")); // Check for console access - if (_user.Disabled || (_user.NoConsole && GlobalSettings.RequestIsInUmbracoApplication(HttpContext.Current) && !GlobalSettings.RequestIsLiveEditRedirector(HttpContext.Current))) + if (_user.Disabled || (_user.NoConsole && GlobalSettings.RequestIsInUmbracoApplication(Context) && GlobalSettings.RequestIsLiveEditRedirector(Context) == false)) { throw new ArgumentException("You have no priviledges to the umbraco console. Please contact your administrator"); } - else - { - _userisValidated = true; - UpdateLogin(); - } - + _userisValidated = true; + UpdateLogin(); } else { @@ -166,85 +160,68 @@ namespace umbraco.BasePages } else { - throw new InvalidOperationException("The user has no umbraco contextid - try logging in"); + throw new InvalidOperationException("The user has no umbraco contextid - try logging in"); } - } /// /// Gets the user id. /// - /// The umbraco user context ID. + /// This is not used /// - //[Obsolete("Use Umbraco.Web.Security.WebSecurity.GetUserId instead")] + [Obsolete("This method is no longer used, use the GetUserId() method without parameters instead")] public static int GetUserId(string umbracoUserContextID) { - //need to parse to guid - Guid gid; - if (!Guid.TryParse(umbracoUserContextID, out gid)) - { - return -1; - } - - var id = ApplicationContext.Current.ApplicationCache.GetCacheItem( - CacheKeys.UserContextCacheKey + umbracoUserContextID, - new TimeSpan(0, UmbracoTimeOutInMinutes / 10, 0), - () => SqlHelper.ExecuteScalar( - "select userID from umbracoUserLogins where contextID = @contextId", - SqlHelper.CreateParameter("@contextId", gid))); - if (id == null) - return -1; - return id.Value; + return GetUserId(); } + /// + /// Gets the currnet user's id. + /// + /// + public static int GetUserId() + { + var identity = HttpContext.Current.GetCurrentIdentity(); + if (identity == null) + return -1; + return identity.Id; + } // Added by NH to use with webservices authentications /// /// Validates the user context ID. /// - /// The umbraco user context ID. + /// This doesn't do anything /// - //[Obsolete("Use Umbraco.Web.Security.WebSecurity.ValidateUserContextId instead")] + [Obsolete("This method is no longer used, use the ValidateCurrentUser() method instead")] public static bool ValidateUserContextID(string currentUmbracoUserContextID) { - if (!currentUmbracoUserContextID.IsNullOrWhiteSpace()) - { - var uid = GetUserId(currentUmbracoUserContextID); - var timeout = GetTimeout(currentUmbracoUserContextID); + return ValidateCurrentUser(); + } - if (timeout > DateTime.Now.Ticks) + /// + /// Validates the currently logged in user and ensures they are not timed out + /// + /// + public static bool ValidateCurrentUser() + { + var ticket = HttpContext.Current.GetUmbracoAuthTicket(); + if (ticket != null) + { + if (ticket.Expired == false) { return true; } - var user = BusinessLogic.User.GetUser(uid); - //TODO: We don't actually log anyone out here, not sure why we're logging ?? - LogHelper.Info("User {0} (Id:{1}) logged out", () => user.Name, () => user.Id); } return false; } - private static long GetTimeout(string umbracoUserContextID) - { - return ApplicationContext.Current.ApplicationCache.GetCacheItem( - CacheKeys.UserContextTimeoutCacheKey + umbracoUserContextID, - new TimeSpan(0, UmbracoTimeOutInMinutes / 10, 0), - () => GetTimeout(true)); - } - //[Obsolete("Use Umbraco.Web.Security.WebSecurity.GetTimeout instead")] public static long GetTimeout(bool bypassCache) { - if (UmbracoSettings.KeepUserLoggedIn) - RenewLoginTimeout(); - - if (bypassCache) - { - return SqlHelper.ExecuteScalar("select timeout from umbracoUserLogins where contextId=@contextId", - SqlHelper.CreateParameter("@contextId", new Guid(umbracoUserContextID)) - ); - } - else - return GetTimeout(umbracoUserContextID); + var ticket = HttpContext.Current.GetUmbracoAuthTicket(); + var ticks = ticket.Expiration.Ticks - DateTime.Now.Ticks; + return ticks; } // Changed to public by NH to help with webservice authentication @@ -252,57 +229,15 @@ namespace umbraco.BasePages /// Gets or sets the umbraco user context ID. /// /// The umbraco user context ID. - //[Obsolete("Use Umbraco.Web.Security.WebSecurity.UmbracoUserContextId instead")] + [Obsolete("This is no longer used at all, it will always return a new GUID though if a user is logged in")] public static string umbracoUserContextID { get { - if (StateHelper.Cookies.HasCookies && StateHelper.Cookies.UserContext.HasValue) - { - try - { - var encTicket = StateHelper.Cookies.UserContext.GetValue(); - if (string.IsNullOrEmpty(encTicket) == false) - { - return encTicket.DecryptWithMachineKey(); - } - } - catch (Exception ex) - { - if (ex is ArgumentException || ex is FormatException || ex is HttpException) - { - StateHelper.Cookies.UserContext.Clear(); - } - else - { - throw; - } - } - } - return ""; + return HttpContext.Current.GetUmbracoAuthTicket() == null ? "" : Guid.NewGuid().ToString(); } set { - // zb-00004 #29956 : refactor cookies names & handling - if (StateHelper.Cookies.HasCookies) - { - // Clearing all old cookies before setting a new one. - if (StateHelper.Cookies.UserContext.HasValue) - StateHelper.Cookies.ClearAll(); - - if (string.IsNullOrEmpty(value) == false) - { - // Encrypt the value - var encTicket = value.EncryptWithMachineKey(); - - // Create new cookie. - StateHelper.Cookies.UserContext.SetValue(encTicket, 1); - } - else - { - StateHelper.Cookies.UserContext.Clear(); - } - } } } @@ -312,61 +247,36 @@ namespace umbraco.BasePages /// public void ClearLogin() { - DeleteLogin(); - umbracoUserContextID = ""; - } - - private void DeleteLogin() - { - // Added try-catch in case login doesn't exist in the database - // Either due to old cookie or running multiple sessions on localhost with different port number - try - { - SqlHelper.ExecuteNonQuery( - "DELETE FROM umbracoUserLogins WHERE contextId = @contextId", - SqlHelper.CreateParameter("@contextId", umbracoUserContextID)); - } - catch (Exception ex) - { - LogHelper.Error(string.Format("Login with contextId {0} didn't exist in the database", umbracoUserContextID), ex); - } + Context.UmbracoLogout(); } private void UpdateLogin() { - // only call update if more than 1/10 of the timeout has passed - if (timeout - (((TicksPrMinute * UmbracoTimeOutInMinutes) * 0.8)) < DateTime.Now.Ticks) - SqlHelper.ExecuteNonQuery( - "UPDATE umbracoUserLogins SET timeout = @timeout WHERE contextId = @contextId", - SqlHelper.CreateParameter("@timeout", DateTime.Now.Ticks + (TicksPrMinute * UmbracoTimeOutInMinutes)), - SqlHelper.CreateParameter("@contextId", umbracoUserContextID)); + Context.RenewUmbracoAuthTicket(UmbracoTimeOutInMinutes); } - //[Obsolete("Use Umbraco.Web.Security.WebSecurity.RenewLoginTimeout instead")] public static void RenewLoginTimeout() { - // only call update if more than 1/10 of the timeout has passed - SqlHelper.ExecuteNonQuery( - "UPDATE umbracoUserLogins SET timeout = @timeout WHERE contextId = @contextId", - SqlHelper.CreateParameter("@timeout", DateTime.Now.Ticks + (TicksPrMinute * UmbracoTimeOutInMinutes)), - SqlHelper.CreateParameter("@contextId", umbracoUserContextID)); + HttpContext.Current.RenewUmbracoAuthTicket(UmbracoTimeOutInMinutes); } /// /// Logs a user in. /// /// The user - //[Obsolete("Use Umbraco.Web.Security.WebSecurity.PerformLogin instead")] public static void doLogin(User u) { - Guid retVal = Guid.NewGuid(); - SqlHelper.ExecuteNonQuery( - "insert into umbracoUserLogins (contextID, userID, timeout) values (@contextId,'" + u.Id + "','" + - (DateTime.Now.Ticks + (TicksPrMinute * UmbracoTimeOutInMinutes)).ToString() + - "') ", - SqlHelper.CreateParameter("@contextId", retVal)); - umbracoUserContextID = retVal.ToString(); - + HttpContext.Current.CreateUmbracoAuthTicket(new UserData + { + Id = u.Id, + AllowedApplications = u.GetApplications().Select(x => x.alias).ToArray(), + RealName = u.Name, + //currently we only have one user type! + Roles = new[] { u.UserType.Alias }, + StartContentNode = u.StartNodeId, + StartMediaNode = u.StartMediaId, + Username = u.LoginName + }); LogHelper.Info("User {0} (Id: {1}) logged in", () => u.Name, () => u.Id); }