diff --git a/src/Umbraco.Core/Models/DataTypeDefinition.cs b/src/Umbraco.Core/Models/DataTypeDefinition.cs index e2a226af3d..5d5b9490a5 100644 --- a/src/Umbraco.Core/Models/DataTypeDefinition.cs +++ b/src/Umbraco.Core/Models/DataTypeDefinition.cs @@ -268,11 +268,6 @@ namespace Umbraco.Core.Models get { return _additionalData; } } - /// - /// Some entities may expose additional data that other's might not, this custom data will be available in this collection - /// - public IDictionary AdditionalData { get; private set; } - internal override void AddingEntity() { base.AddingEntity(); diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index 1975fdc5db..e3e2abf42f 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -64,27 +64,7 @@ namespace Umbraco.Web.Editors return _userManager; } } - - /// - /// 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 - /// updated with a new timeout. - /// - /// - [WebApi.UmbracoAuthorize] - [ValidateAngularAntiForgeryToken] - public double GetRemainingTimeoutSeconds() - { - var httpContextAttempt = TryGetHttpContext(); - if (httpContextAttempt.Success) - { - return httpContextAttempt.Result.GetRemainingAuthSeconds(); - } - - //we need an http context - throw new NotSupportedException("An HttpContext is required for this request"); - } - + [WebApi.UmbracoAuthorize] [ValidateAngularAntiForgeryToken] public async Task PostUnLinkLogin(UnLinkLoginModel unlinkLoginModel) @@ -225,8 +205,16 @@ namespace Umbraco.Web.Editors owinContext.Authentication.SignOut(Core.Constants.Security.BackOfficeExternalAuthenticationType); + var nowUtc = DateTime.Now.ToUniversalTime(); + owinContext.Authentication.SignIn( - new AuthenticationProperties() { IsPersistent = isPersistent }, + new AuthenticationProperties() + { + IsPersistent = isPersistent, + AllowRefresh = true, + IssuedUtc = nowUtc, + ExpiresUtc = nowUtc.AddMinutes(GlobalSettings.TimeOutInMinutes) + }, await user.GenerateUserIdentityAsync(UserManager)); } diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index c25d0e50c0..8e3e05d100 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -493,9 +493,17 @@ namespace Umbraco.Web.Editors private async Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent) { OwinContext.Authentication.SignOut(Core.Constants.Security.BackOfficeExternalAuthenticationType); - + + var nowUtc = DateTime.Now.ToUniversalTime(); + OwinContext.Authentication.SignIn( - new AuthenticationProperties() {IsPersistent = isPersistent}, + new AuthenticationProperties() + { + IsPersistent = isPersistent, + AllowRefresh = true, + IssuedUtc = nowUtc, + ExpiresUtc = nowUtc.AddMinutes(GlobalSettings.TimeOutInMinutes) + }, await user.GenerateUserIdentityAsync(UserManager)); } diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index c812384965..a9a884a2e7 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Threading.Tasks; using System.Web; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; @@ -12,6 +11,7 @@ using Microsoft.Owin.Security.Cookies; using Owin; using Umbraco.Core; using Umbraco.Core.Configuration; +using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models.Identity; using Umbraco.Core.Security; @@ -28,7 +28,7 @@ namespace Umbraco.Web.Security.Identity public static void SetUmbracoLoggerFactory(this IAppBuilder app) { app.SetLoggerFactory(new OwinLoggerFactory()); - } + } #region Backoffice @@ -38,8 +38,8 @@ namespace Umbraco.Web.Security.Identity /// /// /// - public static void ConfigureUserManagerForUmbracoBackOffice(this IAppBuilder app, - ApplicationContext appContext, + public static void ConfigureUserManagerForUmbracoBackOffice(this IAppBuilder app, + ApplicationContext appContext, MembershipProviderBase userMembershipProvider) { if (appContext == null) throw new ArgumentNullException("appContext"); @@ -93,7 +93,7 @@ namespace Umbraco.Web.Security.Identity public static void ConfigureUserManagerForUmbracoBackOffice(this IAppBuilder app, ApplicationContext appContext, Func, IOwinContext, TManager> userManager) - where TManager : BackOfficeUserManager + where TManager : BackOfficeUserManager where TUser : BackOfficeIdentityUser { if (appContext == null) throw new ArgumentNullException("appContext"); @@ -120,13 +120,13 @@ namespace Umbraco.Web.Security.Identity //Don't proceed if the app is not ready if (appContext.IsUpgrading == false && appContext.IsConfigured == false) return app; - app.UseCookieAuthentication(new UmbracoBackOfficeCookieAuthOptions( - UmbracoConfig.For.UmbracoSettings().Security, - GlobalSettings.TimeOutInMinutes, - GlobalSettings.UseSSL) + var authOptions = new UmbracoBackOfficeCookieAuthOptions( + UmbracoConfig.For.UmbracoSettings().Security, + GlobalSettings.TimeOutInMinutes, + GlobalSettings.UseSSL) { Provider = new CookieAuthenticationProvider - { + { // 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. @@ -136,7 +136,12 @@ namespace Umbraco.Web.Security.Identity (manager, user) => user.GenerateUserIdentityAsync(manager), identity => identity.GetUserId()) } - }); + }; + + //This is a custom middleware, we need to return the user's remaining logged in seconds + app.Use(authOptions, UmbracoConfig.For.UmbracoSettings().Security); + + app.UseCookieAuthentication(authOptions); return app; } @@ -171,7 +176,7 @@ namespace Umbraco.Web.Security.Identity }); return app; - } + } #endregion } diff --git a/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs b/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs index 7eedf2ecb0..841c94c80c 100644 --- a/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs +++ b/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs @@ -32,7 +32,7 @@ namespace Umbraco.Web.Security.Identity { return null; } - + return UmbracoModule.ShouldAuthenticateRequest( context.HttpContextFromOwinContext().Request, _umbracoContextAccessor.Value.OriginalRequestUrl) == false @@ -41,5 +41,6 @@ namespace Umbraco.Web.Security.Identity //Return the default implementation : base.GetRequestCookie(context, key); } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/FormsAuthenticationSecureDataFormat.cs b/src/Umbraco.Web/Security/Identity/FormsAuthenticationSecureDataFormat.cs index ba46bba881..77e0fe9faf 100644 --- a/src/Umbraco.Web/Security/Identity/FormsAuthenticationSecureDataFormat.cs +++ b/src/Umbraco.Web/Security/Identity/FormsAuthenticationSecureDataFormat.cs @@ -27,12 +27,16 @@ namespace Umbraco.Web.Security.Identity { var backofficeIdentity = (UmbracoBackOfficeIdentity)data.Identity; var userDataString = JsonConvert.SerializeObject(backofficeIdentity.UserData); - + var ticket = new FormsAuthenticationTicket( 5, data.Identity.Name, - data.Properties.IssuedUtc.HasValue ? data.Properties.IssuedUtc.Value.LocalDateTime : DateTime.Now, - data.Properties.ExpiresUtc.HasValue ? data.Properties.ExpiresUtc.Value.LocalDateTime : DateTime.Now.AddMinutes(_loginTimeoutMinutes), + data.Properties.IssuedUtc.HasValue + ? data.Properties.IssuedUtc.Value.LocalDateTime + : DateTime.Now, + data.Properties.ExpiresUtc.HasValue + ? data.Properties.ExpiresUtc.Value.LocalDateTime + : DateTime.Now.AddMinutes(_loginTimeoutMinutes), data.Properties.IsPersistent, userDataString, "/" @@ -65,7 +69,8 @@ namespace Umbraco.Web.Security.Identity { ExpiresUtc = decrypt.Expiration.ToUniversalTime(), IssuedUtc = decrypt.IssueDate.ToUniversalTime(), - IsPersistent = decrypt.IsPersistent + IsPersistent = decrypt.IsPersistent, + AllowRefresh = true }); return ticket; diff --git a/src/Umbraco.Web/Security/Identity/GetUserSecondsMiddleWare.cs b/src/Umbraco.Web/Security/Identity/GetUserSecondsMiddleWare.cs new file mode 100644 index 0000000000..eeebe13f3e --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/GetUserSecondsMiddleWare.cs @@ -0,0 +1,105 @@ +using System; +using System.Globalization; +using System.Threading.Tasks; +using Microsoft.Owin; +using Microsoft.Owin.Security.Cookies; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; + +namespace Umbraco.Web.Security.Identity +{ + /// + /// Custom middleware to return the remaining seconds the user has before they are logged out + /// + /// + /// This is quite a custom request because in most situations we just want to return the seconds and don't want + /// to renew the auth ticket, however if KeepUserLoggedIn is true, then we do want to renew the auth ticket for + /// this request! + /// + internal class GetUserSecondsMiddleWare : OwinMiddleware + { + private readonly UmbracoBackOfficeCookieAuthOptions _authOptions; + private readonly ISecuritySection _security; + + public GetUserSecondsMiddleWare( + OwinMiddleware next, + UmbracoBackOfficeCookieAuthOptions authOptions, + ISecuritySection security) + : base(next) + { + _authOptions = authOptions; + _security = security; + } + + public override async Task Invoke(IOwinContext context) + { + var request = context.Request; + var response = context.Response; + + var rootPath = context.Request.PathBase.HasValue + ? context.Request.PathBase.Value.EnsureStartsWith("/").EnsureEndsWith("/") + : "/"; + + if (request.Uri.Scheme.InvariantStartsWith("http") + && request.Uri.AbsolutePath.InvariantEquals( + string.Format("{0}{1}/backoffice/UmbracoApi/Authentication/GetRemainingTimeoutSeconds", rootPath, GlobalSettings.UmbracoMvcArea))) + { + var cookie = _authOptions.CookieManager.GetRequestCookie(context, _security.AuthCookieName); + if (cookie.IsNullOrWhiteSpace() == false) + { + var ticket = _authOptions.TicketDataFormat.Unprotect(cookie); + if (ticket != null) + { + var remainingSeconds = ticket.Properties.ExpiresUtc.HasValue + ? (ticket.Properties.ExpiresUtc.Value - DateTime.Now.ToUniversalTime()).TotalSeconds + : 0; + + response.ContentType = "application/json; charset=utf-8"; + response.StatusCode = 200; + response.Headers.Add("Cache-Control", new[] { "no-cache" }); + response.Headers.Add("Pragma", new[] { "no-cache" }); + response.Headers.Add("Expires", new[] { "-1" }); + response.Headers.Add("Date", new[] { DateTime.Now.ToUniversalTime().ToString("R") }); + + //Ok, so here we need to check if we want to process/renew the auth ticket for each + // of these requests. If that is the case, the user will really never be logged out until they + // close their browser (there will be edge cases of that, especially when debugging) + if (_security.KeepUserLoggedIn) + { + var utcNow = DateTime.Now.ToUniversalTime(); + ticket.Properties.IssuedUtc = utcNow; + ticket.Properties.ExpiresUtc = utcNow.AddMinutes(_authOptions.LoginTimeoutMinutes); + + var cookieValue = _authOptions.TicketDataFormat.Protect(ticket); + + var cookieOptions = new CookieOptions + { + Path = "/", + Domain = _authOptions.CookieDomain, + Expires = DateTime.Now.AddMinutes(_authOptions.LoginTimeoutMinutes), + HttpOnly = true, + Secure = _authOptions.CookieSecure == CookieSecureOption.Always + || (_authOptions.CookieSecure == CookieSecureOption.SameAsRequest && request.Uri.Scheme.InvariantEquals("https")), + }; + + _authOptions.CookieManager.AppendResponseCookie( + context, + _authOptions.CookieName, + cookieValue, + cookieOptions); + } + + await response.WriteAsync(remainingSeconds.ToString(CultureInfo.InvariantCulture)); + return; + } + } + response.StatusCode = 401; + } + else if (Next != null) + { + await Next.Invoke(context); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthOptions.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthOptions.cs index 1751a57462..9a55e42b51 100644 --- a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthOptions.cs +++ b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthOptions.cs @@ -1,4 +1,5 @@ -using System.Security.Claims; +using System; +using System.Security.Claims; using System.Threading.Tasks; using Microsoft.Owin.Security.Cookies; using Umbraco.Core; @@ -12,6 +13,8 @@ namespace Umbraco.Web.Security.Identity /// public sealed class UmbracoBackOfficeCookieAuthOptions : CookieAuthenticationOptions { + public int LoginTimeoutMinutes { get; private set; } + public UmbracoBackOfficeCookieAuthOptions() : this(UmbracoConfig.For.UmbracoSettings().Security, GlobalSettings.TimeOutInMinutes, GlobalSettings.UseSSL) { @@ -23,6 +26,7 @@ namespace Umbraco.Web.Security.Identity bool forceSsl, bool useLegacyFormsAuthDataFormat = true) { + LoginTimeoutMinutes = loginTimeoutMinutes; AuthenticationType = Constants.Security.BackOfficeAuthenticationType; if (useLegacyFormsAuthDataFormat) @@ -30,12 +34,13 @@ namespace Umbraco.Web.Security.Identity //If this is not explicitly set it will fall back to the default automatically TicketDataFormat = new FormsAuthenticationSecureDataFormat(loginTimeoutMinutes); } - + + SlidingExpiration = true; + ExpireTimeSpan = TimeSpan.FromMinutes(GlobalSettings.TimeOutInMinutes); CookieDomain = securitySection.AuthCookieDomain; CookieName = securitySection.AuthCookieName; CookieHttpOnly = true; CookieSecure = forceSsl ? CookieSecureOption.Always : CookieSecureOption.SameAsRequest; - CookiePath = "/"; //Custom cookie manager so we can filter requests diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index f7b877be35..0f1cf90ed6 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -299,6 +299,7 @@ + diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index cd85e82686..f93347aa20 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -199,41 +199,41 @@ namespace Umbraco.Web return false; } - private static readonly ConcurrentHashSet IgnoreTicketRenewUrls = new ConcurrentHashSet(); - /// - /// Determines if the authentication ticket should be renewed with a new timeout - /// - /// - /// - /// - /// - /// We do not want to renew the ticket when we are checking for the user's remaining timeout unless - - /// UmbracoConfig.For.UmbracoSettings().Security.KeepUserLoggedIn == true - /// - internal static bool ShouldIgnoreTicketRenew(Uri url, HttpContextBase httpContext) - { - //this setting will renew the ticket for all requests. - if (UmbracoConfig.For.UmbracoSettings().Security.KeepUserLoggedIn) - { - return false; - } + //private static readonly ConcurrentHashSet IgnoreTicketRenewUrls = new ConcurrentHashSet(); + ///// + ///// Determines if the authentication ticket should be renewed with a new timeout + ///// + ///// + ///// + ///// + ///// + ///// We do not want to renew the ticket when we are checking for the user's remaining timeout unless - + ///// UmbracoConfig.For.UmbracoSettings().Security.KeepUserLoggedIn == true + ///// + //internal static bool ShouldIgnoreTicketRenew(Uri url, HttpContextBase httpContext) + //{ + // //this setting will renew the ticket for all requests. + // if (UmbracoConfig.For.UmbracoSettings().Security.KeepUserLoggedIn) + // { + // return false; + // } - //initialize the ignore ticket urls - we don't need to lock this, it's concurrent and a hashset - // we don't want to have to gen the url each request so this will speed things up a teeny bit. - if (IgnoreTicketRenewUrls.Any() == false) - { - var urlHelper = new UrlHelper(new RequestContext(httpContext, new RouteData())); - var checkSessionUrl = urlHelper.GetUmbracoApiService(controller => controller.GetRemainingTimeoutSeconds()); - IgnoreTicketRenewUrls.Add(checkSessionUrl); - } + // //initialize the ignore ticket urls - we don't need to lock this, it's concurrent and a hashset + // // we don't want to have to gen the url each request so this will speed things up a teeny bit. + // if (IgnoreTicketRenewUrls.Any() == false) + // { + // var urlHelper = new UrlHelper(new RequestContext(httpContext, new RouteData())); + // var checkSessionUrl = urlHelper.GetUmbracoApiService(controller => controller.GetRemainingTimeoutSeconds()); + // IgnoreTicketRenewUrls.Add(checkSessionUrl); + // } - if (IgnoreTicketRenewUrls.Any(x => url.AbsolutePath.StartsWith(x))) - { - return true; - } + // if (IgnoreTicketRenewUrls.Any(x => url.AbsolutePath.StartsWith(x))) + // { + // return true; + // } - return false; - } + // return false; + //} /// /// Checks the current request and ensures that it is routable based on the structure of the request and URI diff --git a/src/Umbraco.Web/WebApi/UmbracoAuthorizeAttribute.cs b/src/Umbraco.Web/WebApi/UmbracoAuthorizeAttribute.cs index 4517464553..028c835d90 100644 --- a/src/Umbraco.Web/WebApi/UmbracoAuthorizeAttribute.cs +++ b/src/Umbraco.Web/WebApi/UmbracoAuthorizeAttribute.cs @@ -4,6 +4,7 @@ using Umbraco.Core; namespace Umbraco.Web.WebApi { + /// /// Ensures authorization is successful for a back office user ///