Cleans up the usages of auth cookies. OWIN is in charge of auth cookies but because we have Webforms, WebApi, MVC and OWIN, they all like to deal with cookies differently. OWIN should still be solely in charge of the auth cookies, so the auth extensions are cleaned up, the renewal now works by queuing the renewal and we have custom middleware detect if a force renewal has been queued and we renew the auth cookie there. Have obsoleted a few methods that should not be used that write auth tickets directly (this is purely for backwards compat with webforms). All of these changes now ensure that the auth cookie is renewed consistently between Webforms, WebApi, MVC and OWIN. Some changes also include ensuring that OWIN is used to sign out.
This commit is contained in:
@@ -155,7 +155,25 @@ namespace Umbraco.Web.Security.Identity
|
||||
UmbracoConfig.For.UmbracoSettings().Security,
|
||||
app.CreateLogger<GetUserSecondsMiddleWare>());
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if we should authenticate the request
|
||||
/// </summary>
|
||||
/// <param name="ctx"></param>
|
||||
/// <param name="originalRequestUrl"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// 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
|
||||
/// </remarks>
|
||||
internal static bool ShouldAuthenticateRequest(IOwinContext ctx, Uri originalRequestUrl)
|
||||
{
|
||||
var request = ctx.Request;
|
||||
var httpCtx = ctx.TryGetHttpContext();
|
||||
|
||||
if (//check the explicit flag
|
||||
ctx.Get<bool?>("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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// If a flag is set on the context to force renew the ticket, this will do it
|
||||
/// </summary>
|
||||
internal class ForceRenewalCookieAuthenticationHandler : AuthenticationHandler<CookieAuthenticationOptions>
|
||||
{
|
||||
/// <summary>
|
||||
/// This handler doesn't actually do any auth so we return null;
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected override Task<AuthenticationTicket> AuthenticateCoreAsync()
|
||||
{
|
||||
return Task.FromResult((AuthenticationTicket)null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ticket from the request
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This will check if the token exists in the request to force renewal
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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<bool?>("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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Microsoft.Owin;
|
||||
using Microsoft.Owin.Security.Cookies;
|
||||
using Microsoft.Owin.Security.Infrastructure;
|
||||
using Owin;
|
||||
|
||||
namespace Umbraco.Web.Security.Identity
|
||||
{
|
||||
/// <summary>
|
||||
/// This middleware is used simply to force renew the auth ticket if a flag to do so is found in the request
|
||||
/// </summary>
|
||||
internal class ForceRenewalCookieAuthenticationMiddleware : CookieAuthenticationMiddleware
|
||||
{
|
||||
public ForceRenewalCookieAuthenticationMiddleware(OwinMiddleware next, IAppBuilder app, CookieAuthenticationOptions options) : base(next, app, options)
|
||||
{
|
||||
}
|
||||
|
||||
protected override AuthenticationHandler<CookieAuthenticationOptions> CreateHandler()
|
||||
{
|
||||
return new ForceRenewalCookieAuthenticationHandler();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
/// <param name="owinContext"></param>
|
||||
/// <returns></returns>
|
||||
internal static HttpContextBase HttpContextFromOwinContext(this IOwinContext owinContext)
|
||||
internal static Attempt<HttpContextBase> TryGetHttpContext(this IOwinContext owinContext)
|
||||
{
|
||||
return owinContext.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
|
||||
var ctx = owinContext.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
|
||||
return ctx == null ? Attempt<HttpContextBase>.Fail() : Attempt.Succeed(ctx);
|
||||
}
|
||||
|
||||
}
|
||||
74
src/Umbraco.Web/Security/WebAuthExtensions.cs
Normal file
74
src/Umbraco.Web/Security/WebAuthExtensions.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// This will set a an authenticated IPrincipal to the current request given the IUser object
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
internal static IPrincipal SetPrincipalForRequest(this HttpRequestMessage request, IUser user)
|
||||
{
|
||||
var principal = new ClaimsPrincipal(
|
||||
new UmbracoBackOfficeIdentity(
|
||||
new ClaimsIdentity(),
|
||||
Mapper.Map<UserData>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This will set a an authenticated IPrincipal to the current request given the IUser object
|
||||
/// </summary>
|
||||
/// <param name="httpContext"></param>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
internal static IPrincipal SetPrincipalForRequest(this HttpContextBase httpContext, IUser user)
|
||||
{
|
||||
var principal = new ClaimsPrincipal(
|
||||
new UmbracoBackOfficeIdentity(
|
||||
new ClaimsIdentity(),
|
||||
Mapper.Map<UserData>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>returns the number of seconds until their session times out</returns>
|
||||
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<UserDetail>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs the user in
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <returns>returns the Forms Auth ticket created which is used to log them in</returns>
|
||||
[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<UserData>(user));
|
||||
|
||||
LogHelper.Info<WebSecurity>("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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user