diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index 13dee96b97..60fba0ae40 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -1,4 +1,7 @@ -namespace Umbraco.Core +using System; +using System.ComponentModel; + +namespace Umbraco.Core { public static partial class Constants { @@ -15,6 +18,8 @@ /// /// The auth cookie name /// + [Obsolete("DO NOT USE THIS, USE ISecuritySection.AuthCookieName, this will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] public const string AuthCookieName = "UMB_UCONTEXT"; } diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index e332cb6dca..1c7c544ed8 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.ComponentModel; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; @@ -15,7 +16,6 @@ using Microsoft.Owin; using Newtonsoft.Json; using Umbraco.Core.Configuration; using Umbraco.Core.Models.Membership; -using Microsoft.Owin; using Umbraco.Core.Logging; namespace Umbraco.Core.Security @@ -157,9 +157,6 @@ namespace Umbraco.Core.Security return new HttpContextWrapper(http).GetCurrentIdentity(authenticateRequestIfNotFound); } - /// - /// This clears the forms authentication cookie - /// public static void UmbracoLogout(this HttpContextBase http) { if (http == null) throw new ArgumentNullException("http"); @@ -170,6 +167,8 @@ namespace Umbraco.Core.Security /// This clears the forms authentication cookie for webapi since cookies are handled differently /// /// + [Obsolete("Use OWIN IAuthenticationManager.SignOut instead")] + [EditorBrowsable(EditorBrowsableState.Never)] public static void UmbracoLogoutWebApi(this HttpResponseMessage response) { if (response == null) throw new ArgumentNullException("response"); @@ -195,11 +194,8 @@ namespace Umbraco.Core.Security response.Headers.AddCookies(new[] { authCookie, prevCookie, extLoginCookie }); } - /// - /// This adds the forms authentication cookie for webapi since cookies are handled differently - /// - /// - /// + [Obsolete("Use WebSecurity.SetPrincipalForRequest")] + [EditorBrowsable(EditorBrowsableState.Never)] public static FormsAuthenticationTicket UmbracoLoginWebApi(this HttpResponseMessage response, IUser user) { if (response == null) throw new ArgumentNullException("response"); @@ -250,26 +246,29 @@ namespace Umbraco.Core.Security if (http == null) throw new ArgumentNullException("http"); new HttpContextWrapper(http).UmbracoLogout(); } - + /// - /// Renews the Umbraco authentication ticket + /// This will force ticket renewal in the OWIN pipeline /// /// /// public static bool RenewUmbracoAuthTicket(this HttpContextBase http) { if (http == null) throw new ArgumentNullException("http"); - return RenewAuthTicket(http, - UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName, - UmbracoConfig.For.UmbracoSettings().Security.AuthCookieDomain, - //Umbraco has always persisted it's original cookie for 1 day so we'll keep it that way - 1440); + http.Items["umbraco-force-auth"] = true; + return true; } + /// + /// This will force ticket renewal in the OWIN pipeline + /// + /// + /// internal static bool RenewUmbracoAuthTicket(this HttpContext http) { if (http == null) throw new ArgumentNullException("http"); - return new HttpContextWrapper(http).RenewUmbracoAuthTicket(); + http.Items["umbraco-force-auth"] = true; + return true; } /// @@ -390,8 +389,7 @@ namespace Umbraco.Core.Security //ensure there's def an expired cookie http.Response.Cookies.Add(new HttpCookie(c) { Expires = DateTime.Now.AddYears(-1) }); } - } - + } } private static FormsAuthenticationTicket GetAuthTicket(this HttpContextBase http, string cookieName) @@ -432,51 +430,6 @@ namespace Umbraco.Core.Security return FormsAuthentication.Decrypt(formsCookie); } - /// - /// Renews the forms authentication ticket & cookie - /// - /// - /// - /// - /// - /// true if there was a ticket to renew otherwise false if there was no ticket - private static bool RenewAuthTicket(this HttpContextBase http, string cookieName, string cookieDomain, int minutesPersisted) - { - if (http == null) throw new ArgumentNullException("http"); - //get the ticket - var ticket = GetAuthTicket(http, cookieName); - //renew the ticket - var renewed = FormsAuthentication.RenewTicketIfOld(ticket); - if (renewed == null) - { - return false; - } - - //get the request cookie to get it's expiry date, - //NOTE: this will never be null becaues we already do this - // check in teh GetAuthTicket. - var formsCookie = http.Request.Cookies[cookieName]; - - //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 - }; - - if (GlobalSettings.UseSSL) - cookie.Secure = true; - - //ensure http only, this should only be able to be accessed via the server - cookie.HttpOnly = true; - - //rewrite the cooke - http.Response.Cookies.Set(cookie); - return true; - } - /// /// Creates a custom FormsAuthentication ticket with the data specified /// diff --git a/src/Umbraco.Core/Security/BackOfficeSignInManager.cs b/src/Umbraco.Core/Security/BackOfficeSignInManager.cs index 85d6a0c715..f1d18b9d0f 100644 --- a/src/Umbraco.Core/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeSignInManager.cs @@ -53,6 +53,11 @@ namespace Umbraco.Core.Security switch (result) { case SignInStatus.Success: + _logger.WriteCore(TraceEventType.Information, 0, + string.Format( + "User: {0} logged in from IP address {1}", + userName, + _request.RemoteIpAddress), null, null); break; case SignInStatus.LockedOut: _logger.WriteCore(TraceEventType.Information, 0, diff --git a/src/Umbraco.Tests/TestHelpers/FakeHttpContextFactory.cs b/src/Umbraco.Tests/TestHelpers/FakeHttpContextFactory.cs index 31bdb616b0..7f38bf61a1 100644 --- a/src/Umbraco.Tests/TestHelpers/FakeHttpContextFactory.cs +++ b/src/Umbraco.Tests/TestHelpers/FakeHttpContextFactory.cs @@ -8,6 +8,7 @@ using System.Web; using System.Web.Routing; using Moq; using Umbraco.Core; +using Umbraco.Core.Configuration; namespace Umbraco.Tests.TestHelpers { @@ -60,7 +61,7 @@ namespace Umbraco.Tests.TestHelpers //Cookie collection var cookieCollection = new HttpCookieCollection(); - cookieCollection.Add(new HttpCookie(Constants.Web.AuthCookieName, "FBA996E7-D6BE-489B-B199-2B0F3D2DD826")); + cookieCollection.Add(new HttpCookie("UMB_UCONTEXT", "FBA996E7-D6BE-489B-B199-2B0F3D2DD826")); //Request var requestMock = new Mock(); diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index 7f7b59199a..25b60c5cb7 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -177,19 +177,14 @@ namespace Umbraco.Web.Editors //get the user var user = Security.GetBackOfficeUser(loginModel.Username); var userDetail = Mapper.Map(user); - + //update the userDetail and set their remaining seconds + userDetail.SecondsUntilTimeout = TimeSpan.FromMinutes(GlobalSettings.TimeOutInMinutes).TotalSeconds; + //create a response with the userDetail object var response = Request.CreateResponse(HttpStatusCode.OK, userDetail); - //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); - - //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(); + //ensure the user is set for the current request + Request.SetPrincipalForRequest(user); return response; @@ -241,11 +236,12 @@ namespace Umbraco.Web.Editors /// Logs the current user out /// /// - [UmbracoBackOfficeLogout] [ClearAngularAntiForgeryToken] [ValidateAngularAntiForgeryToken] public HttpResponseMessage PostLogout() { + Request.TryGetOwinContext().Result.Authentication.SignOut(); + Logger.Info("User {0} from IP address {1} has logged out", () => User.Identity == null ? "UNKNOWN" : User.Identity.Name, () => TryGetOwinContext().Result.Request.RemoteIpAddress); diff --git a/src/Umbraco.Web/HttpCookieExtensions.cs b/src/Umbraco.Web/HttpCookieExtensions.cs index dc6d087fb3..7aec1466da 100644 --- a/src/Umbraco.Web/HttpCookieExtensions.cs +++ b/src/Umbraco.Web/HttpCookieExtensions.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Web; +using Microsoft.Owin; using Umbraco.Core; namespace Umbraco.Web @@ -62,6 +63,16 @@ namespace Umbraco.Web return request.Cookies[Constants.Web.PreviewCookieName] != null; } + /// + /// Does a preview cookie exist ? + /// + /// + /// + public static bool HasPreviewCookie(this IOwinRequest request) + { + return request.Cookies[Constants.Web.PreviewCookieName] != null; + } + /// /// Does a cookie exist with the specified key ? /// diff --git a/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs b/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs index b8be5b7a22..6523925505 100644 --- a/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs @@ -43,14 +43,8 @@ namespace Umbraco.Web.Install.InstallSteps var security = new WebSecurity(_httpContext, _applicationContext); security.PerformLogin(0); - ////Clear the auth cookie - this is required so that the login screen is displayed after upgrade and so the - //// csrf anti-forgery tokens are created, otherwise there will just be JS errors if the user has an old - //// login token from a previous version when we didn't have csrf tokens in place - //var security = new WebSecurity(new HttpContextWrapper(Context), ApplicationContext.Current); - //security.ClearCurrentLogin(); - //reports the ended install - InstallHelper ih = new InstallHelper(UmbracoContext.Current); + var ih = new InstallHelper(UmbracoContext.Current); ih.InstallStatus(true, ""); return null; diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index d535bc4f8a..1668b6776d 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -155,7 +155,25 @@ namespace Umbraco.Web.Security.Identity UmbracoConfig.For.UmbracoSettings().Security, app.CreateLogger()); - app.UseCookieAuthentication(authOptions); + app.UseUmbracoBackOfficeCookieAuthentication(authOptions); + + return app; + } + + internal static IAppBuilder UseUmbracoBackOfficeCookieAuthentication(this IAppBuilder app, CookieAuthenticationOptions options) + { + if (app == null) + { + throw new ArgumentNullException("app"); + } + + //First the normal cookie middleware + app.Use(typeof(CookieAuthenticationMiddleware), app, options); + app.UseStageMarker(PipelineStage.Authenticate); + + //Then our custom middleware + app.Use(typeof(ForceRenewalCookieAuthenticationMiddleware), app, options); + app.UseStageMarker(PipelineStage.Authenticate); return app; } diff --git a/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs b/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs index 841c94c80c..06ce201ef9 100644 --- a/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs +++ b/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs @@ -1,6 +1,9 @@ -using Microsoft.Owin; +using System; +using System.Web; +using Microsoft.Owin; using Microsoft.Owin.Infrastructure; using Umbraco.Core; +using Umbraco.Core.IO; namespace Umbraco.Web.Security.Identity { @@ -33,8 +36,8 @@ namespace Umbraco.Web.Security.Identity return null; } - return UmbracoModule.ShouldAuthenticateRequest( - context.HttpContextFromOwinContext().Request, + return ShouldAuthenticateRequest( + context, _umbracoContextAccessor.Value.OriginalRequestUrl) == false //Don't auth request, don't return a cookie ? null @@ -42,5 +45,40 @@ namespace Umbraco.Web.Security.Identity : base.GetRequestCookie(context, key); } + /// + /// Determines if we should authenticate the request + /// + /// + /// + /// + /// + /// We auth the request when: + /// * it is a back office request + /// * it is an installer request + /// * it is a /base request + /// * it is a preview request + /// + internal static bool ShouldAuthenticateRequest(IOwinContext ctx, Uri originalRequestUrl) + { + var request = ctx.Request; + var httpCtx = ctx.TryGetHttpContext(); + + if (//check the explicit flag + ctx.Get("umbraco-force-auth") != null + || (httpCtx.Success && httpCtx.Result.Items["umbraco-force-auth"] != null) + //check back office + || request.Uri.IsBackOfficeRequest(HttpRuntime.AppDomainAppVirtualPath) + //check installer + || request.Uri.IsInstallerRequest() + //detect in preview + || (request.HasPreviewCookie() && request.Uri != null && request.Uri.AbsolutePath.StartsWith(IOHelper.ResolveUrl(SystemDirectories.Umbraco)) == false) + //check for base + || BaseRest.BaseRestHandler.IsBaseRestRequest(originalRequestUrl)) + { + return true; + } + return false; + } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/ForceRenewalCookieAuthenticationHandler.cs b/src/Umbraco.Web/Security/Identity/ForceRenewalCookieAuthenticationHandler.cs new file mode 100644 index 0000000000..24beb708f7 --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/ForceRenewalCookieAuthenticationHandler.cs @@ -0,0 +1,112 @@ +using System.Threading.Tasks; +using Microsoft.Owin; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Cookies; +using Microsoft.Owin.Security.Infrastructure; + +namespace Umbraco.Web.Security.Identity +{ + /// + /// If a flag is set on the context to force renew the ticket, this will do it + /// + internal class ForceRenewalCookieAuthenticationHandler : AuthenticationHandler + { + /// + /// This handler doesn't actually do any auth so we return null; + /// + /// + protected override Task AuthenticateCoreAsync() + { + return Task.FromResult((AuthenticationTicket)null); + } + + /// + /// Gets the ticket from the request + /// + /// + private AuthenticationTicket GetTicket() + { + var cookie = Options.CookieManager.GetRequestCookie(Context, Options.CookieName); + if (string.IsNullOrWhiteSpace(cookie)) + { + return null; + } + var ticket = Options.TicketDataFormat.Unprotect(cookie); + if (ticket == null) + { + return null; + } + return ticket; + } + + /// + /// This will check if the token exists in the request to force renewal + /// + /// + protected override Task ApplyResponseGrantAsync() + { + //Now we need to check if we should force renew this based on a flag in the context + + var httpCtx = Context.TryGetHttpContext(); + //check for the special flag in either the owin or http context + var shouldRenew = Context.Get("umbraco-force-auth") != null || (httpCtx.Success && httpCtx.Result.Items["umbraco-force-auth"] != null); + + if (shouldRenew) + { + var signin = Helper.LookupSignIn(Options.AuthenticationType); + var shouldSignin = signin != null; + var signout = Helper.LookupSignOut(Options.AuthenticationType, Options.AuthenticationMode); + var shouldSignout = signout != null; + + //we don't care about the sign in/sign out scenario, only renewal + if (shouldSignin == false && shouldSignout == false) + { + //get the ticket + var model = GetTicket(); + if (model != null) + { + var currentUtc = Options.SystemClock.UtcNow; + var issuedUtc = model.Properties.IssuedUtc; + var expiresUtc = model.Properties.ExpiresUtc; + + if (expiresUtc.HasValue && issuedUtc.HasValue) + { + //renew the date/times + model.Properties.IssuedUtc = currentUtc; + var timeSpan = expiresUtc.Value.Subtract(issuedUtc.Value); + model.Properties.ExpiresUtc = currentUtc.Add(timeSpan); + + //now save back all the required cookie details + var cookieValue = Options.TicketDataFormat.Protect(model); + var cookieOptions = new CookieOptions + { + Domain = Options.CookieDomain, + HttpOnly = Options.CookieHttpOnly, + Path = Options.CookiePath ?? "/", + }; + if (Options.CookieSecure == CookieSecureOption.SameAsRequest) + { + cookieOptions.Secure = Request.IsSecure; + } + else + { + cookieOptions.Secure = Options.CookieSecure == CookieSecureOption.Always; + } + if (model.Properties.IsPersistent) + { + cookieOptions.Expires = model.Properties.ExpiresUtc.Value.ToUniversalTime().DateTime; + } + Options.CookieManager.AppendResponseCookie( + Context, + Options.CookieName, + cookieValue, + cookieOptions); + } + } + } + } + + return Task.FromResult(0); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/ForceRenewalCookieAuthenticationMiddleware.cs b/src/Umbraco.Web/Security/Identity/ForceRenewalCookieAuthenticationMiddleware.cs new file mode 100644 index 0000000000..e159a1c847 --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/ForceRenewalCookieAuthenticationMiddleware.cs @@ -0,0 +1,22 @@ +using Microsoft.Owin; +using Microsoft.Owin.Security.Cookies; +using Microsoft.Owin.Security.Infrastructure; +using Owin; + +namespace Umbraco.Web.Security.Identity +{ + /// + /// This middleware is used simply to force renew the auth ticket if a flag to do so is found in the request + /// + internal class ForceRenewalCookieAuthenticationMiddleware : CookieAuthenticationMiddleware + { + public ForceRenewalCookieAuthenticationMiddleware(OwinMiddleware next, IAppBuilder app, CookieAuthenticationOptions options) : base(next, app, options) + { + } + + protected override AuthenticationHandler CreateHandler() + { + return new ForceRenewalCookieAuthenticationHandler(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/GetUserSecondsMiddleWare.cs b/src/Umbraco.Web/Security/Identity/GetUserSecondsMiddleWare.cs index 85cab39120..c3ce720fa5 100644 --- a/src/Umbraco.Web/Security/Identity/GetUserSecondsMiddleWare.cs +++ b/src/Umbraco.Web/Security/Identity/GetUserSecondsMiddleWare.cs @@ -98,13 +98,13 @@ namespace Umbraco.Web.Security.Identity remainingSeconds = (ticket.Properties.ExpiresUtc.Value - DateTime.Now.ToUniversalTime()).TotalSeconds; } - else if (remainingSeconds <=30) + else if (remainingSeconds <= 30) { //NOTE: We are using 30 seconds because that is what is coded into angular to force logout to give some headway in // the timeout process. _logger.WriteCore(TraceEventType.Information, 0, - string.Format("User logged will be logged out due to timeout: {0}, IP Address: {1}", ticket.Identity.Name, request.RemoteIpAddress), + string.Format("User logged will be logged out due to timeout: {0}, IP Address: {1}", ticket.Identity.Name, request.RemoteIpAddress), null, null); } diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthOptions.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthOptions.cs index 9a55e42b51..957cb66b0a 100644 --- a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthOptions.cs +++ b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthOptions.cs @@ -36,7 +36,7 @@ namespace Umbraco.Web.Security.Identity } SlidingExpiration = true; - ExpireTimeSpan = TimeSpan.FromMinutes(GlobalSettings.TimeOutInMinutes); + ExpireTimeSpan = TimeSpan.FromMinutes(LoginTimeoutMinutes); CookieDomain = securitySection.AuthCookieDomain; CookieName = securitySection.AuthCookieName; CookieHttpOnly = true; diff --git a/src/Umbraco.Web/Security/Identity/OwinExtensions.cs b/src/Umbraco.Web/Security/OwinExtensions.cs similarity index 51% rename from src/Umbraco.Web/Security/Identity/OwinExtensions.cs rename to src/Umbraco.Web/Security/OwinExtensions.cs index ceb0bdafb2..d1f1fbc1ed 100644 --- a/src/Umbraco.Web/Security/Identity/OwinExtensions.cs +++ b/src/Umbraco.Web/Security/OwinExtensions.cs @@ -1,7 +1,8 @@ -using System.Web; +using System.Web; using Microsoft.Owin; +using Umbraco.Core; -namespace Umbraco.Web.Security.Identity +namespace Umbraco.Web.Security { internal static class OwinExtensions { @@ -11,9 +12,10 @@ namespace Umbraco.Web.Security.Identity /// /// /// - internal static HttpContextBase HttpContextFromOwinContext(this IOwinContext owinContext) + internal static Attempt TryGetHttpContext(this IOwinContext owinContext) { - return owinContext.Get(typeof(HttpContextBase).FullName); + var ctx = owinContext.Get(typeof(HttpContextBase).FullName); + return ctx == null ? Attempt.Fail() : Attempt.Succeed(ctx); } } diff --git a/src/Umbraco.Web/Security/WebAuthExtensions.cs b/src/Umbraco.Web/Security/WebAuthExtensions.cs new file mode 100644 index 0000000000..ec43244b6e --- /dev/null +++ b/src/Umbraco.Web/Security/WebAuthExtensions.cs @@ -0,0 +1,74 @@ +using System.Net.Http; +using System.Security.Claims; +using System.Security.Principal; +using System.ServiceModel.Channels; +using System.Threading; +using System.Web; +using AutoMapper; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Security; +using Umbraco.Web.WebApi; + +namespace Umbraco.Web.Security +{ + internal static class WebAuthExtensions + { + /// + /// This will set a an authenticated IPrincipal to the current request given the IUser object + /// + /// + /// + /// + internal static IPrincipal SetPrincipalForRequest(this HttpRequestMessage request, IUser user) + { + var principal = new ClaimsPrincipal( + new UmbracoBackOfficeIdentity( + new ClaimsIdentity(), + Mapper.Map(user))); + + //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; + } + var http = request.TryGetHttpContext(); + if (http) + { + http.Result.User = principal; + } + Thread.CurrentPrincipal = principal; + + //For WebAPI + request.SetUserPrincipal(principal); + + return principal; + } + + /// + /// This will set a an authenticated IPrincipal to the current request given the IUser object + /// + /// + /// + /// + internal static IPrincipal SetPrincipalForRequest(this HttpContextBase httpContext, IUser user) + { + var principal = new ClaimsPrincipal( + new UmbracoBackOfficeIdentity( + new ClaimsIdentity(), + Mapper.Map(user))); + + //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; + } + httpContext.User = principal; + Thread.CurrentPrincipal = principal; + return principal; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index d3d6f76999..1e986dccfc 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Web; using System.Web.Security; @@ -8,8 +9,9 @@ using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; -using umbraco; +using Microsoft.AspNet.Identity.Owin; using umbraco.businesslogic.Exceptions; +using Umbraco.Web.Models.ContentEditing; using GlobalSettings = Umbraco.Core.Configuration.GlobalSettings; using User = umbraco.BusinessLogic.User; @@ -84,32 +86,27 @@ namespace Umbraco.Web.Security /// returns the number of seconds until their session times out public virtual double PerformLogin(int userId) { + var owinCtx = _httpContext.GetOwinContext(); + var user = _applicationContext.Services.UserService.GetUserById(userId); - return PerformLogin(user).GetRemainingAuthSeconds(); + var userDetail = Mapper.Map(user); + //update the userDetail and set their remaining seconds + userDetail.SecondsUntilTimeout = TimeSpan.FromMinutes(GlobalSettings.TimeOutInMinutes).TotalSeconds; + var principal = _httpContext.SetPrincipalForRequest(user); + owinCtx.Authentication.SignIn((UmbracoBackOfficeIdentity)principal.Identity); + return TimeSpan.FromMinutes(GlobalSettings.TimeOutInMinutes).TotalSeconds; } - /// - /// Logs the user in - /// - /// - /// returns the Forms Auth ticket created which is used to log them in + [Obsolete("This method should not be used, login is performed by the OWIN pipeline, use the overload that returns double and accepts a UserId instead")] public virtual FormsAuthenticationTicket PerformLogin(IUser user) { - //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.BackOfficeExternalCookieName); if (externalLoginCookie != null) { 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); - return ticket; } @@ -119,6 +116,7 @@ namespace Umbraco.Web.Security public virtual void ClearCurrentLogin() { _httpContext.UmbracoLogout(); + _httpContext.GetOwinContext().Authentication.SignOut(); } /// diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 7a4c6a4cd0..b67b410801 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -308,11 +308,15 @@ + + + + diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index 02bb83bd32..a7c41ba90f 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -149,72 +149,7 @@ namespace Umbraco.Web #endregion #region Methods - - /// - /// Determines if we should authenticate the request - /// - /// - /// - /// - /// - /// We auth the request when: - /// * it is a back office request - /// * it is an installer request - /// * it is a /base request - /// * it is a preview request - /// - internal static bool ShouldAuthenticateRequest(HttpRequestBase request, Uri originalRequestUrl) - { - if (//check back office - request.Url.IsBackOfficeRequest(HttpRuntime.AppDomainAppVirtualPath) - //check installer - || request.Url.IsInstallerRequest() - //detect in preview - || (request.HasPreviewCookie() && request.Url != null && request.Url.AbsolutePath.StartsWith(IOHelper.ResolveUrl(SystemDirectories.Umbraco)) == false) - //check for base - || BaseRest.BaseRestHandler.IsBaseRestRequest(originalRequestUrl)) - { - return true; - } - 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); - // } - - // if (IgnoreTicketRenewUrls.Any(x => url.AbsolutePath.StartsWith(x))) - // { - // return true; - // } - - // 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/Filters/UmbracoBackOfficeLogoutAttribute.cs b/src/Umbraco.Web/WebApi/Filters/UmbracoBackOfficeLogoutAttribute.cs index 336d8bb403..29ed4da73e 100644 --- a/src/Umbraco.Web/WebApi/Filters/UmbracoBackOfficeLogoutAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/UmbracoBackOfficeLogoutAttribute.cs @@ -1,16 +1,12 @@ -using System.Web.Http.Filters; +using System; +using System.ComponentModel; +using System.Web.Http.Filters; using Umbraco.Core.Security; 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. - /// 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. - /// + [Obsolete("This is no longer used and will be removed from the codebase in the future, use OWIN IAuthenticationManager.SignOut instead")] + [EditorBrowsable(EditorBrowsableState.Never)] public sealed class UmbracoBackOfficeLogoutAttribute : ActionFilterAttribute { public override void OnActionExecuted(HttpActionExecutedContext context) @@ -20,7 +16,7 @@ namespace Umbraco.Web.WebApi.Filters //this clears out all of our cookies context.Response.UmbracoLogoutWebApi(); - + //this calls the underlying owin sign out logic - which should call the // auth providers middleware callbacks if using custom auth middleware context.Request.TryGetOwinContext().Result.Authentication.SignOut(); diff --git a/src/Umbraco.Web/umbraco.presentation/macro.cs b/src/Umbraco.Web/umbraco.presentation/macro.cs index 53bdaaa9de..19768d15b5 100644 --- a/src/Umbraco.Web/umbraco.presentation/macro.cs +++ b/src/Umbraco.Web/umbraco.presentation/macro.cs @@ -1705,6 +1705,10 @@ namespace umbraco // propagate the user's context // zb-00004 #29956 : refactor cookies names & handling + + //TODO: This is the worst thing ever. This will also not work if people decide to put their own + // custom auth system in place. + HttpCookie inCookie = StateHelper.Cookies.UserContext.RequestCookie; var cookie = new Cookie(inCookie.Name, inCookie.Value, inCookie.Path, HttpContext.Current.Request.ServerVariables["SERVER_NAME"]); diff --git a/src/umbraco.businesslogic/StateHelper.cs b/src/umbraco.businesslogic/StateHelper.cs index e84d3aa129..61df4318ab 100644 --- a/src/umbraco.businesslogic/StateHelper.cs +++ b/src/umbraco.businesslogic/StateHelper.cs @@ -3,6 +3,7 @@ using System.Reflection; using System.Web; using System.Web.UI; using Umbraco.Core; +using Umbraco.Core.Configuration; namespace umbraco.BusinessLogic { @@ -350,7 +351,7 @@ namespace umbraco.BusinessLogic * that actually make sense? shouldn't some cookie have _no_ expires? */ static readonly Cookie _preview = new Cookie(Constants.Web.PreviewCookieName, TimeSpan.Zero); // was "PreviewSet" - static readonly Cookie _userContext = new Cookie(Constants.Web.AuthCookieName, 30d); // was "UserContext" + static readonly Cookie _userContext = new Cookie(UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName, 30d); // was "UserContext" static readonly Cookie _member = new Cookie("UMB_MEMBER", 30d); // was "umbracoMember" public static Cookie Preview { get { return _preview; } }