Implements PostLogout and ensures all appropriate cookies are cleared on logout along with ensuring that the ticket is renewed each user seconds request if configured to stay logged in.

This commit is contained in:
Shannon
2020-06-09 14:36:36 +10:00
parent 9b991f3882
commit 246e28d147
9 changed files with 65 additions and 91 deletions

View File

@@ -14,7 +14,7 @@ namespace Umbraco.Configuration.Models
_configuration = configuration;
}
public bool KeepUserLoggedIn => _configuration.GetValue(Prefix + "KeepUserLoggedIn", true);
public bool KeepUserLoggedIn => _configuration.GetValue(Prefix + "KeepUserLoggedIn", false);
public bool HideDisabledUsersInBackoffice =>
_configuration.GetValue(Prefix + "HideDisabledUsersInBackoffice", false);

View File

@@ -67,7 +67,7 @@ namespace Umbraco.Tests.Security
}
[Test]
public void ShouldAuthenticateRequest_No_User_Seconds()
public void ShouldAuthenticateRequest_Is_Back_Office()
{
var testHelper = new TestHelper();
@@ -85,28 +85,9 @@ namespace Umbraco.Tests.Security
GetMockLinkGenerator(out var remainingTimeoutSecondsPath, out var isAuthPath));
var result = mgr.ShouldAuthenticateRequest(new Uri($"http://localhost{remainingTimeoutSecondsPath}"));
Assert.IsFalse(result);
}
Assert.IsTrue(result);
[Test]
public void ShouldAuthenticateRequest_Is_Auth()
{
var testHelper = new TestHelper();
var httpContextAccessor = testHelper.GetHttpContextAccessor();
var globalSettings = testHelper.SettingsForTests.GenerateMockGlobalSettings();
var runtime = Mock.Of<IRuntimeState>(x => x.Level == RuntimeLevel.Run);
var mgr = new BackOfficeCookieManager(
Mock.Of<IUmbracoContextAccessor>(),
runtime,
Mock.Of<IHostingEnvironment>(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco" && x.ToAbsolute(Constants.SystemDirectories.Install) == "/install"),
globalSettings,
Mock.Of<IRequestCache>(),
GetMockLinkGenerator(out var remainingTimeoutSecondsPath, out var isAuthPath));
var result = mgr.ShouldAuthenticateRequest(new Uri($"http://localhost{isAuthPath}"));
result = mgr.ShouldAuthenticateRequest(new Uri($"http://localhost{isAuthPath}"));
Assert.IsTrue(result);
}

View File

@@ -1,7 +1,9 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Net;
using System.Threading.Tasks;
using Umbraco.Core;
using Umbraco.Core.BackOffice;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
@@ -182,6 +184,22 @@ namespace Umbraco.Web.BackOffice.Controllers
throw new HttpResponseException(HttpStatusCode.BadRequest);
}
/// <summary>
/// Logs the current user out
/// </summary>
/// <returns></returns>
[TypeFilter(typeof(ValidateAngularAntiForgeryTokenAttribute))]
public IActionResult PostLogout()
{
HttpContext.SignOutAsync(Core.Constants.Security.BackOfficeAuthenticationType);
_logger.Info<AuthenticationController>("User {UserName} from IP address {RemoteIpAddress} has logged out", User.Identity == null ? "UNKNOWN" : User.Identity.Name, HttpContext.Connection.RemoteIpAddress);
_userManager.RaiseLogoutSuccessEvent(User, int.Parse(User.Identity.GetUserId()));
return Ok();
}
/// <summary>
/// Return the <see cref="UserDetail"/> for the given <see cref="IUser"/>
/// </summary>

View File

@@ -24,6 +24,8 @@ namespace Umbraco.Web.BackOffice.Filters
/// </remarks>
public sealed class ValidateAngularAntiForgeryTokenAttribute : ActionFilterAttribute
{
// TODO: Either make this inherit from TypeFilter or make this just a normal IActionFilter
private readonly ILogger _logger;
private readonly IBackOfficeAntiforgery _antiforgery;
private readonly ICookieManager _cookieManager;

View File

@@ -89,9 +89,6 @@ namespace Umbraco.Web.BackOffice.Security
if (_explicitPaths != null)
return _explicitPaths.Any(x => x.InvariantEquals(requestUri.AbsolutePath));
//check user seconds path
if (requestUri.AbsolutePath.InvariantEquals(_getRemainingSecondsPath)) return false;
if (//check the explicit flag
checkForceAuthTokens && _requestCache.IsAvailable && _requestCache.Get(Constants.Security.ForceReAuthFlag) != null
//check back office

View File

@@ -36,6 +36,7 @@ namespace Umbraco.Web.BackOffice.Security
private readonly IRequestCache _requestCache;
private readonly IUserService _userService;
private readonly IIpResolver _ipResolver;
private readonly ISystemClock _systemClock;
private readonly BackOfficeSessionIdValidator _sessionIdValidator;
private readonly LinkGenerator _linkGenerator;
@@ -49,6 +50,7 @@ namespace Umbraco.Web.BackOffice.Security
IRequestCache requestCache,
IUserService userService,
IIpResolver ipResolver,
ISystemClock systemClock,
BackOfficeSessionIdValidator sessionIdValidator,
LinkGenerator linkGenerator)
{
@@ -61,6 +63,7 @@ namespace Umbraco.Web.BackOffice.Security
_requestCache = requestCache;
_userService = userService;
_ipResolver = ipResolver;
_systemClock = systemClock;
_sessionIdValidator = sessionIdValidator;
_linkGenerator = linkGenerator;
}
@@ -116,7 +119,7 @@ namespace Umbraco.Web.BackOffice.Security
// It would be possible to re-use the default behavior if any of these need to be set but that must be taken into account else
// our back office requests will not function correctly. For now we don't need to set/configure any of these callbacks because
// the defaults work fine with our setup.
OnValidatePrincipal = async ctx =>
{
// We need to resolve the BackOfficeSecurityStampValidator per request as a requirement (even in aspnetcore they do this)
@@ -136,6 +139,7 @@ namespace Umbraco.Web.BackOffice.Security
await EnsureValidSessionId(ctx);
await securityStampValidator.ValidateAsync(ctx);
EnsureTicketRenewalIfKeepUserLoggedIn(ctx);
// add a claim to track when the cookie expires, we use this to track time remaining
backOfficeIdentity.AddClaim(new Claim(
@@ -145,7 +149,11 @@ namespace Umbraco.Web.BackOffice.Security
UmbracoBackOfficeIdentity.Issuer,
UmbracoBackOfficeIdentity.Issuer,
backOfficeIdentity));
if (_securitySettings.KeepUserLoggedIn)
{
}
},
OnSigningIn = ctx =>
{
@@ -180,7 +188,6 @@ namespace Umbraco.Web.BackOffice.Security
OnSigningOut = ctx =>
{
//Clear the user's session on sign out
// TODO: We need to test this once we have signout functionality, not sure if the httpcontext.user.identity will still be set here
if (ctx.HttpContext?.User?.Identity != null)
{
var claimsIdentity = ctx.HttpContext.User.Identity as ClaimsIdentity;
@@ -197,7 +204,9 @@ namespace Umbraco.Web.BackOffice.Security
BackOfficeSessionIdValidator.CookieName,
_securitySettings.AuthCookieName,
Constants.Web.PreviewCookieName,
Constants.Security.BackOfficeExternalCookieName
Constants.Security.BackOfficeExternalCookieName,
Constants.Web.AngularCookieName,
Constants.Web.CsrfValidationCookieName,
};
foreach (var cookie in cookies)
{
@@ -223,5 +232,31 @@ namespace Umbraco.Web.BackOffice.Security
if (_runtimeState.Level == RuntimeLevel.Run)
await _sessionIdValidator.ValidateSessionAsync(TimeSpan.FromMinutes(1), context);
}
/// <summary>
/// Ensures the ticket is renewed if the <see cref="ISecuritySettings.KeepUserLoggedIn"/> is set to true
/// and the current request is for the get user seconds endpoint
/// </summary>
/// <param name="context"></param>
private void EnsureTicketRenewalIfKeepUserLoggedIn(CookieValidatePrincipalContext context)
{
if (!_securitySettings.KeepUserLoggedIn) return;
var currentUtc = _systemClock.UtcNow;
var issuedUtc = context.Properties.IssuedUtc;
var expiresUtc = context.Properties.ExpiresUtc;
if (expiresUtc.HasValue && issuedUtc.HasValue)
{
var timeElapsed = currentUtc.Subtract(issuedUtc.Value);
var timeRemaining = expiresUtc.Value.Subtract(currentUtc);
//if it's time to renew, then do it
if (timeRemaining < timeElapsed)
{
context.ShouldRenew = true;
}
}
}
}
}

View File

@@ -386,30 +386,7 @@ namespace Umbraco.Web.Editors
}
/// <summary>
/// Logs the current user out
/// </summary>
/// <returns></returns>
[ClearAngularAntiForgeryToken]
[ValidateAngularAntiForgeryToken]
public HttpResponseMessage PostLogout()
{
var owinContext = Request.TryGetOwinContext().Result;
owinContext.Authentication.SignOut(
Core.Constants.Security.BackOfficeAuthenticationType,
Core.Constants.Security.BackOfficeExternalAuthenticationType);
Logger.Info<AuthenticationController>("User {UserName} from IP address {RemoteIpAddress} has logged out", User.Identity == null ? "UNKNOWN" : User.Identity.Name, owinContext.Request.RemoteIpAddress);
if (UserManager != null)
{
int.TryParse(User.Identity.GetUserId(), out var userId);
UserManager.RaiseLogoutSuccessEvent(User, userId);
}
return Request.CreateResponse(HttpStatusCode.OK);
}
// NOTE: This has been migrated to netcore, but in netcore we don't explicitly set the principal in this method, that's done in ConfigureUmbracoBackOfficeCookieOptions so don't worry about that
private HttpResponseMessage SetPrincipalAndReturnUserDetail(IUser user, IPrincipal principal)

View File

@@ -350,7 +350,6 @@
<Compile Include="WebApi\AngularJsonOnlyConfigurationAttribute.cs" />
<Compile Include="Editors\Binders\MemberBinder.cs" />
<Compile Include="WebApi\Filters\AngularAntiForgeryHelper.cs" />
<Compile Include="WebApi\Filters\ClearAngularAntiForgeryTokenAttribute.cs" />
<Compile Include="WebApi\Filters\DisableBrowserCacheAttribute.cs" />
<Compile Include="WebApi\Filters\EnableOverrideAuthorizationAttribute.cs" />
<Compile Include="WebApi\Filters\FilterGrouping.cs" />

View File

@@ -1,35 +0,0 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Web.Http.Filters;
namespace Umbraco.Web.WebApi.Filters
{
/// <summary>
/// Clears the angular csrf cookie if the request was successful
/// </summary>
public sealed class ClearAngularAntiForgeryTokenAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(HttpActionExecutedContext context)
{
if (context.Response == null) return;
if (context.Response.IsSuccessStatusCode == false) return;
//remove the cookies
var angularCookie = new CookieHeaderValue(Core.Constants.Web.AngularCookieName, "null")
{
Expires = DateTime.Now.AddYears(-1),
//must be js readable
HttpOnly = false,
Path = "/"
};
var validationCookie = new CookieHeaderValue(Core.Constants.Web.CsrfValidationCookieName, "null")
{
Expires = DateTime.Now.AddYears(-1),
HttpOnly = true,
Path = "/"
};
context.Response.Headers.AddCookies(new[] { angularCookie, validationCookie });
}
}
}