diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs
index 5876a6b3d2..18d0462b8f 100644
--- a/src/Umbraco.Web/Editors/AuthenticationController.cs
+++ b/src/Umbraco.Web/Editors/AuthenticationController.cs
@@ -1,12 +1,15 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Web;
using System.Web.Http;
using AutoMapper;
+using Umbraco.Core;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Models.Mapping;
using Umbraco.Web.Mvc;
+using Umbraco.Core.Security;
using Umbraco.Web.Security;
using Umbraco.Web.WebApi;
using Umbraco.Web.WebApi.Filters;
@@ -31,6 +34,25 @@ namespace Umbraco.Web.Editors
base.Initialize(controllerContext);
controllerContext.Configuration.Formatters.Remove(controllerContext.Configuration.Formatters.XmlFormatter);
}
+
+ ///
+ /// 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]
+ 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");
+ }
///
/// Checks if the current user's cookie is valid and if so returns OK or a 400 (BadRequest)
@@ -53,23 +75,23 @@ namespace Umbraco.Web.Editors
///
- /// Checks if the current user's cookie is valid and if so returns the user object associated
+ /// Returns the currently logged in Umbraco user
///
///
+ [WebApi.UmbracoAuthorize]
public UserDetail GetCurrentUser()
{
- var attempt = UmbracoContext.Security.AuthorizeRequest();
- if (attempt == ValidateRequestAttempt.Success)
+ var user = Services.UserService.GetUserById(UmbracoContext.Security.GetUserId());
+ var result = Mapper.Map(user);
+ var httpContextAttempt = TryGetHttpContext();
+ if (httpContextAttempt.Success)
{
- var user = Services.UserService.GetUserById(UmbracoContext.Security.GetUserId());
- return Mapper.Map(user);
+ //set their remaining seconds
+ result.SecondsUntilTimeout = httpContextAttempt.Result.GetRemainingAuthSeconds();
}
-
- //return Unauthorized (401) because the user is not authorized right now
- throw new HttpResponseException(HttpStatusCode.Unauthorized);
+ return result;
}
-
///
/// Logs a user in
///
@@ -83,7 +105,14 @@ namespace Umbraco.Web.Editors
var user = Services.UserService.GetUserByUserName(username);
//TODO: Clean up the int cast!
UmbracoContext.Security.PerformLogin((int)user.Id);
- return Mapper.Map(user);
+ var result = Mapper.Map(user);
+ var httpContextAttempt = TryGetHttpContext();
+ if (httpContextAttempt.Success)
+ {
+ //set their remaining seconds
+ result.SecondsUntilTimeout = httpContextAttempt.Result.GetRemainingAuthSeconds();
+ }
+ return result;
}
//return BadRequest (400), we don't want to return a 401 because that get's intercepted
diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs
index 499bacc902..70bbddfa8a 100644
--- a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs
@@ -19,5 +19,11 @@ namespace Umbraco.Web.Models.ContentEditing
///
[DataMember(Name = "emailHash")]
public string EmailHash { get; set; }
+
+ ///
+ /// Gets/sets the number of seconds for the user's auth ticket to expire
+ ///
+ [DataMember(Name = "remainingAuthSeconds")]
+ public double SecondsUntilTimeout { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs
index 66c552726b..d72f7b33d9 100644
--- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs
+++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs
@@ -16,6 +16,7 @@ namespace Umbraco.Web.Models.Mapping
.ForMember(
detail => detail.EmailHash,
opt => opt.MapFrom(user => user.Email.ToLowerInvariant().Trim().ToMd5()));
+
config.CreateMap
()
.ForMember(detail => detail.UserId, opt => opt.MapFrom(profile => GetIntId(profile.Id)));
}
diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs
index adf1caa1e2..4baf64d553 100644
--- a/src/Umbraco.Web/Security/WebSecurity.cs
+++ b/src/Umbraco.Web/Security/WebSecurity.cs
@@ -166,7 +166,7 @@ namespace Umbraco.Web.Security
public void RenewLoginTimeout()
{
- _httpContext.RenewUmbracoAuthTicket(UmbracoTimeOutInMinutes);
+ _httpContext.RenewUmbracoAuthTicket();
}
///
@@ -229,7 +229,7 @@ namespace Umbraco.Web.Security
internal void UpdateLogin()
{
- _httpContext.RenewUmbracoAuthTicket(UmbracoTimeOutInMinutes);
+ _httpContext.RenewUmbracoAuthTicket();
}
internal long GetTimeout()
diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj
index 44df3f328c..46d36a78b8 100644
--- a/src/Umbraco.Web/Umbraco.Web.csproj
+++ b/src/Umbraco.Web/Umbraco.Web.csproj
@@ -839,6 +839,7 @@
+
diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs
index 8a33b2449a..00f8c6f5fc 100644
--- a/src/Umbraco.Web/UmbracoModule.cs
+++ b/src/Umbraco.Web/UmbracoModule.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Security.Principal;
using System.Threading;
using System.Web;
+using System.Web.Mvc;
using System.Web.Routing;
using Newtonsoft.Json;
using Umbraco.Core;
@@ -13,6 +14,7 @@ using Umbraco.Core.Configuration;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Security;
+using Umbraco.Web.Editors;
using Umbraco.Web.Routing;
using Umbraco.Web.Security;
using umbraco;
@@ -173,7 +175,9 @@ namespace Umbraco.Web
if (ShouldAuthenticateRequest(req, UmbracoContext.Current.OriginalRequestUrl))
{
var ticket = http.GetUmbracoAuthTicket();
- if (ticket != null && !ticket.Expired && http.RenewUmbracoAuthTicket())
+ //if there was a ticket, it's not expired, its renewable - or it should not be renewed
+ if (ticket != null && ticket.Expired == false
+ && (http.RenewUmbracoAuthTicket() || ShouldIgnoreTicketRenew(UmbracoContext.Current.OriginalRequestUrl, http)))
{
try
{
@@ -249,6 +253,35 @@ 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.
+ ///
+ internal static bool ShouldIgnoreTicketRenew(Uri url, HttpContextBase httpContext)
+ {
+ //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.GetUmbracoApiServiceBaseUrl(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/UmbracoUserTimeoutFilterAttribute.cs b/src/Umbraco.Web/WebApi/Filters/UmbracoUserTimeoutFilterAttribute.cs
new file mode 100644
index 0000000000..012e2024fb
--- /dev/null
+++ b/src/Umbraco.Web/WebApi/Filters/UmbracoUserTimeoutFilterAttribute.cs
@@ -0,0 +1,30 @@
+using System.Globalization;
+using System.Web.Http.Filters;
+using Umbraco.Core.Security;
+
+namespace Umbraco.Web.WebApi.Filters
+{
+ ///
+ /// This will check if the request is authenticated and if there's an auth ticket present we will
+ /// add a custom header to the response indicating how many seconds are remaining for the current
+ /// user's session. This allows us to keep track of a user's session effectively in the back office.
+ ///
+ public sealed class UmbracoUserTimeoutFilterAttribute : ActionFilterAttribute
+ {
+ public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
+ {
+ base.OnActionExecuted(actionExecutedContext);
+
+ var httpContextAttempt = actionExecutedContext.Request.TryGetHttpContext();
+ if (httpContextAttempt.Success)
+ {
+ var ticket = httpContextAttempt.Result.GetUmbracoAuthTicket();
+ if (ticket != null && ticket.Expired == false)
+ {
+ var remainingSeconds = httpContextAttempt.Result.GetRemainingAuthSeconds();
+ actionExecutedContext.Response.Headers.Add("X-Umb-User-Seconds", remainingSeconds.ToString(CultureInfo.InvariantCulture));
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/WebApi/HttpRequestMessageExtensions.cs b/src/Umbraco.Web/WebApi/HttpRequestMessageExtensions.cs
index 44cc8793fe..f053d7b790 100644
--- a/src/Umbraco.Web/WebApi/HttpRequestMessageExtensions.cs
+++ b/src/Umbraco.Web/WebApi/HttpRequestMessageExtensions.cs
@@ -5,14 +5,39 @@ using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
+using System.Web;
using System.Web.Http;
using System.Web.Http.ModelBinding;
+using Umbraco.Core;
namespace Umbraco.Web.WebApi
{
public static class HttpRequestMessageExtensions
{
+ ///
+ /// Tries to retreive the current HttpContext if one exists.
+ ///
+ ///
+ public static Attempt TryGetHttpContext(this HttpRequestMessage request)
+ {
+ object context;
+ if (request.Properties.TryGetValue("MS_HttpContext", out context))
+ {
+ var httpContext = context as HttpContextBase;
+ if (httpContext != null)
+ {
+ return Attempt.Succeed(httpContext);
+ }
+ }
+ if (HttpContext.Current != null)
+ {
+ return Attempt.Succeed(new HttpContextWrapper(HttpContext.Current));
+ }
+
+ return Attempt.Fail();
+ }
+
///
/// Create a 403 (Forbidden) response indicating that hte current user doesn't have access to the resource
/// requested or the action it needs to take.
diff --git a/src/Umbraco.Web/WebApi/UmbracoApiController.cs b/src/Umbraco.Web/WebApi/UmbracoApiController.cs
index 189acc24f3..cbe855ec3b 100644
--- a/src/Umbraco.Web/WebApi/UmbracoApiController.cs
+++ b/src/Umbraco.Web/WebApi/UmbracoApiController.cs
@@ -35,21 +35,7 @@ namespace Umbraco.Web.WebApi
///
protected Attempt TryGetHttpContext()
{
- object context;
- if (Request.Properties.TryGetValue("MS_HttpContext", out context))
- {
- var httpContext = context as HttpContextBase;
- if (httpContext != null)
- {
- return Attempt.Succeed(httpContext);
- }
- }
- if (HttpContext.Current != null)
- {
- return Attempt.Succeed(new HttpContextWrapper(HttpContext.Current));
- }
-
- return Attempt.Fail();
+ return Request.TryGetHttpContext();
}
///
diff --git a/src/Umbraco.Web/WebApi/UmbracoAuthorizeAttribute.cs b/src/Umbraco.Web/WebApi/UmbracoAuthorizeAttribute.cs
index 337892e0ca..4517464553 100644
--- a/src/Umbraco.Web/WebApi/UmbracoAuthorizeAttribute.cs
+++ b/src/Umbraco.Web/WebApi/UmbracoAuthorizeAttribute.cs
@@ -6,7 +6,7 @@ namespace Umbraco.Web.WebApi
{
///
/// Ensures authorization is successful for a back office user
- ///
+ ///
public sealed class UmbracoAuthorizeAttribute : AuthorizeAttribute
{
///
diff --git a/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs b/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs
index c977200cb6..f27b25273b 100644
--- a/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs
+++ b/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs
@@ -8,6 +8,14 @@ using umbraco.BusinessLogic;
namespace Umbraco.Web.WebApi
{
+ ///
+ /// A base controller that ensures all requests are authorized - the user is logged in.
+ ///
+ ///
+ /// This controller will also append a custom header to the response if the user is logged in using forms authentication
+ /// which indicates the seconds remaining before their timeout expires.
+ ///
+ [UmbracoUserTimeoutFilter]
[UmbracoAuthorize]
public abstract class UmbracoAuthorizedApiController : UmbracoApiController
{
diff --git a/src/umbraco.businesslogic/BasePages/BasePage.cs b/src/umbraco.businesslogic/BasePages/BasePage.cs
index d30d5aa506..a9727fb41c 100644
--- a/src/umbraco.businesslogic/BasePages/BasePage.cs
+++ b/src/umbraco.businesslogic/BasePages/BasePage.cs
@@ -252,12 +252,12 @@ namespace umbraco.BasePages
private void UpdateLogin()
{
- Context.RenewUmbracoAuthTicket(UmbracoTimeOutInMinutes);
+ Context.RenewUmbracoAuthTicket();
}
public static void RenewLoginTimeout()
{
- HttpContext.Current.RenewUmbracoAuthTicket(UmbracoTimeOutInMinutes);
+ HttpContext.Current.RenewUmbracoAuthTicket();
}
///