Cleaning up websecurity and implementing it, migrates security stamp and session id validation for cookie auth

This commit is contained in:
Shannon
2020-06-02 13:28:30 +10:00
parent d85fc0d353
commit b75fba71f5
33 changed files with 489 additions and 756 deletions

View File

@@ -0,0 +1,33 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Security.Principal;
using System.Text;
using System.Threading;
using Umbraco.Core.BackOffice;
namespace Umbraco.Core.Security
{
public static class AuthenticationExtensions
{
/// <summary>
/// Ensures that the thread culture is set based on the back office user's culture
/// </summary>
/// <param name="identity"></param>
public static void EnsureCulture(this IIdentity identity)
{
if (identity is UmbracoBackOfficeIdentity umbIdentity && umbIdentity.IsAuthenticated)
{
Thread.CurrentThread.CurrentUICulture =
Thread.CurrentThread.CurrentCulture = UserCultures.GetOrAdd(umbIdentity.Culture, s => new CultureInfo(s));
}
}
/// <summary>
/// Used so that we aren't creating a new CultureInfo object for every single request
/// </summary>
private static readonly ConcurrentDictionary<string, CultureInfo> UserCultures = new ConcurrentDictionary<string, CultureInfo>();
}
}

View File

@@ -12,12 +12,6 @@ namespace Umbraco.Web.Security
/// <value>The current user.</value>
IUser CurrentUser { get; }
[Obsolete("This needs to be removed, ASP.NET Identity should always be used for this operation, this is currently only used in the installer which needs to be updated")]
double PerformLogin(int userId);
[Obsolete("This needs to be removed, ASP.NET Identity should always be used for this operation, this is currently only used in the installer which needs to be updated")]
void ClearCurrentLogin();
/// <summary>
/// Gets the current user's id.
/// </summary>

View File

@@ -1,278 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Owin;
using Microsoft.Owin.Logging;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Moq;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.BackOffice;
using Umbraco.Core.Configuration;
using Umbraco.Core.Models.Membership;
using Umbraco.Net;
using Umbraco.Web.Security;
namespace Umbraco.Tests.Security
{
public class UmbracoSecurityStampValidatorTests
{
private Mock<IOwinContext> _mockOwinContext;
private Mock<BackOfficeOwinUserManager> _mockUserManager;
private Mock<BackOfficeSignInManager> _mockSignInManager;
private AuthenticationTicket _testAuthTicket;
private CookieAuthenticationOptions _testOptions;
private BackOfficeIdentityUser _testUser;
private const string _testAuthType = "cookie";
[Test]
public void OnValidateIdentity_When_GetUserIdCallback_Is_Null_Expect_ArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => UmbracoSecurityStampValidator
.OnValidateIdentity<BackOfficeSignInManager, BackOfficeOwinUserManager, BackOfficeIdentityUser>(
TimeSpan.MaxValue, null, null));
}
[Test]
public async Task OnValidateIdentity_When_Validation_Interval_Not_Met_Expect_No_Op()
{
var func = UmbracoSecurityStampValidator
.OnValidateIdentity<BackOfficeSignInManager, BackOfficeOwinUserManager, BackOfficeIdentityUser>(
TimeSpan.MaxValue, null, identity => throw new Exception());
_testAuthTicket.Properties.IssuedUtc = DateTimeOffset.UtcNow;
var context = new CookieValidateIdentityContext(
_mockOwinContext.Object,
_testAuthTicket,
_testOptions);
await func(context);
Assert.AreEqual(_testAuthTicket.Identity, context.Identity);
}
[Test]
public void OnValidateIdentity_When_Time_To_Validate_But_No_UserManager_Expect_InvalidOperationException()
{
var func = UmbracoSecurityStampValidator
.OnValidateIdentity<BackOfficeSignInManager, BackOfficeOwinUserManager, BackOfficeIdentityUser>(
TimeSpan.MinValue, null, identity => throw new Exception());
_mockOwinContext.Setup(x => x.Get<BackOfficeOwinUserManager>(It.IsAny<string>()))
.Returns((BackOfficeOwinUserManager) null);
var context = new CookieValidateIdentityContext(
_mockOwinContext.Object,
_testAuthTicket,
_testOptions);
Assert.ThrowsAsync<InvalidOperationException>(async () => await func(context));
}
[Test]
public void OnValidateIdentity_When_Time_To_Validate_But_No_SignInManager_Expect_InvalidOperationException()
{
var func = UmbracoSecurityStampValidator
.OnValidateIdentity<BackOfficeSignInManager, BackOfficeOwinUserManager, BackOfficeIdentityUser>(
TimeSpan.MinValue, null, identity => throw new Exception());
_mockOwinContext.Setup(x => x.Get<BackOfficeSignInManager>(It.IsAny<string>()))
.Returns((BackOfficeSignInManager) null);
var context = new CookieValidateIdentityContext(
_mockOwinContext.Object,
_testAuthTicket,
_testOptions);
Assert.ThrowsAsync<InvalidOperationException>(async () => await func(context));
}
[Test]
public async Task OnValidateIdentity_When_Time_To_Validate_And_User_No_Longer_Found_Expect_Rejected()
{
var userId = Guid.NewGuid().ToString();
var func = UmbracoSecurityStampValidator
.OnValidateIdentity<BackOfficeSignInManager, BackOfficeOwinUserManager, BackOfficeIdentityUser>(
TimeSpan.MinValue, null, identity => userId);
_mockUserManager.Setup(x => x.FindByIdAsync(userId))
.ReturnsAsync((BackOfficeIdentityUser) null);
var context = new CookieValidateIdentityContext(
_mockOwinContext.Object,
_testAuthTicket,
_testOptions);
await func(context);
Assert.IsNull(context.Identity);
_mockOwinContext.Verify(x => x.Authentication.SignOut(_testAuthType), Times.Once);
}
[Test]
public async Task OnValidateIdentity_When_Time_To_Validate_And_User_Exists_And_Does_Not_Support_SecurityStamps_Expect_Rejected()
{
var userId = Guid.NewGuid().ToString();
var func = UmbracoSecurityStampValidator
.OnValidateIdentity<BackOfficeSignInManager, BackOfficeOwinUserManager, BackOfficeIdentityUser>(
TimeSpan.MinValue, null, identity => userId);
_mockUserManager.Setup(x => x.FindByIdAsync(userId)).ReturnsAsync(_testUser);
_mockUserManager.Setup(x => x.SupportsUserSecurityStamp).Returns(false);
var context = new CookieValidateIdentityContext(
_mockOwinContext.Object,
_testAuthTicket,
_testOptions);
await func(context);
Assert.IsNull(context.Identity);
_mockOwinContext.Verify(x => x.Authentication.SignOut(_testAuthType), Times.Once);
}
[Test]
public async Task OnValidateIdentity_When_Time_To_Validate_And_SecurityStamp_Has_Changed_Expect_Rejected()
{
var userId = Guid.NewGuid().ToString();
var func = UmbracoSecurityStampValidator
.OnValidateIdentity<BackOfficeSignInManager, BackOfficeOwinUserManager, BackOfficeIdentityUser>(
TimeSpan.MinValue, null, identity => userId);
_mockUserManager.Setup(x => x.FindByIdAsync(userId)).ReturnsAsync(_testUser);
_mockUserManager.Setup(x => x.SupportsUserSecurityStamp).Returns(true);
_mockUserManager.Setup(x => x.GetSecurityStampAsync(_testUser)).ReturnsAsync(Guid.NewGuid().ToString);
var context = new CookieValidateIdentityContext(
_mockOwinContext.Object,
_testAuthTicket,
_testOptions);
await func(context);
Assert.IsNull(context.Identity);
_mockOwinContext.Verify(x => x.Authentication.SignOut(_testAuthType), Times.Once);
}
[Test]
public async Task OnValidateIdentity_When_Time_To_Validate_And_SecurityStamp_Has_Not_Changed_Expect_No_Change()
{
var userId = Guid.NewGuid().ToString();
var func = UmbracoSecurityStampValidator
.OnValidateIdentity<BackOfficeSignInManager, BackOfficeOwinUserManager, BackOfficeIdentityUser>(
TimeSpan.MinValue, null, identity => userId);
_mockUserManager.Setup(x => x.FindByIdAsync(userId)).ReturnsAsync(_testUser);
_mockUserManager.Setup(x => x.SupportsUserSecurityStamp).Returns(true);
_mockUserManager.Setup(x => x.GetSecurityStampAsync(_testUser)).ReturnsAsync(_testUser.SecurityStamp);
var context = new CookieValidateIdentityContext(
_mockOwinContext.Object,
_testAuthTicket,
_testOptions);
await func(context);
Assert.AreEqual(_testAuthTicket.Identity, context.Identity);
}
[Test]
public async Task OnValidateIdentity_When_User_Validated_And_RegenerateIdentityCallback_Present_Expect_User_Refreshed()
{
var userId = Guid.NewGuid().ToString();
var expectedIdentity = new ClaimsIdentity(new List<Claim> {new Claim("sub", "bob")});
var regenFuncCalled = false;
Func<BackOfficeSignInManager, BackOfficeOwinUserManager, BackOfficeIdentityUser, Task<ClaimsIdentity>> regenFunc =
(signInManager, userManager, user) =>
{
regenFuncCalled = true;
return Task.FromResult(expectedIdentity);
};
var func = UmbracoSecurityStampValidator
.OnValidateIdentity<BackOfficeSignInManager, BackOfficeOwinUserManager, BackOfficeIdentityUser>(
TimeSpan.MinValue, regenFunc, identity => userId);
_mockUserManager.Setup(x => x.FindByIdAsync(userId)).ReturnsAsync(_testUser);
_mockUserManager.Setup(x => x.SupportsUserSecurityStamp).Returns(true);
_mockUserManager.Setup(x => x.GetSecurityStampAsync(_testUser)).ReturnsAsync(_testUser.SecurityStamp);
var context = new CookieValidateIdentityContext(
_mockOwinContext.Object,
_testAuthTicket,
_testOptions);
ClaimsIdentity callbackIdentity = null;
_mockOwinContext.Setup(x => x.Authentication.SignIn(context.Properties, It.IsAny<ClaimsIdentity>()))
.Callback((AuthenticationProperties props, ClaimsIdentity[] identities) => callbackIdentity = identities.FirstOrDefault())
.Verifiable();
await func(context);
Assert.True(regenFuncCalled);
Assert.AreEqual(expectedIdentity, callbackIdentity);
Assert.IsNull(context.Properties.IssuedUtc);
Assert.IsNull(context.Properties.ExpiresUtc);
_mockOwinContext.Verify();
}
[SetUp]
public void Setup()
{
var mockGlobalSettings = new Mock<IGlobalSettings>();
mockGlobalSettings.Setup(x => x.DefaultUILanguage).Returns("test");
_testUser = new BackOfficeIdentityUser(mockGlobalSettings.Object, 2, new List<IReadOnlyUserGroup>())
{
UserName = "alice",
Name = "Alice",
Email = "alice@umbraco.test",
SecurityStamp = Guid.NewGuid().ToString()
};
_testAuthTicket = new AuthenticationTicket(
new ClaimsIdentity(
new List<Claim> {new Claim("sub", "alice"), new Claim(Constants.Web.SecurityStampClaimType, _testUser.SecurityStamp)},
_testAuthType),
new AuthenticationProperties());
_testOptions = new CookieAuthenticationOptions { AuthenticationType = _testAuthType };
_mockUserManager = new Mock<BackOfficeOwinUserManager>(
new Mock<IPasswordConfiguration>().Object,
new Mock<IIpResolver>().Object,
new Mock<IUserStore<BackOfficeIdentityUser>>().Object,
null, null, null, null, null, null, null);
_mockUserManager.Setup(x => x.FindByIdAsync(It.IsAny<string>())).ReturnsAsync((BackOfficeIdentityUser) null);
_mockUserManager.Setup(x => x.SupportsUserSecurityStamp).Returns(false);
_mockSignInManager = new Mock<BackOfficeSignInManager>(
_mockUserManager.Object,
new Mock<IUserClaimsPrincipalFactory<BackOfficeIdentityUser>>().Object,
new Mock<IAuthenticationManager>().Object,
new Mock<ILogger>().Object,
new Mock<IGlobalSettings>().Object,
new Mock<IOwinRequest>().Object);
_mockOwinContext = new Mock<IOwinContext>();
_mockOwinContext.Setup(x => x.Get<BackOfficeOwinUserManager>(It.IsAny<string>()))
.Returns(_mockUserManager.Object);
_mockOwinContext.Setup(x => x.Get<BackOfficeSignInManager>(It.IsAny<string>()))
.Returns(_mockSignInManager.Object);
_mockOwinContext.Setup(x => x.Authentication.SignOut(It.IsAny<string>()));
}
}
}

View File

@@ -149,7 +149,6 @@
<Compile Include="Persistence\Repositories\KeyValueRepositoryTests.cs" />
<Compile Include="Security\BackOfficeOwinUserManagerTests.cs" />
<Compile Include="Security\OwinDataProtectorTokenProviderTests.cs" />
<Compile Include="Security\UmbracoSecurityStampValidatorTests.cs" />
<Compile Include="Services\KeyValueServiceTests.cs" />
<Compile Include="Persistence\Repositories\UserRepositoryTest.cs" />
<Compile Include="UmbracoExamine\ExamineExtensions.cs" />

View File

@@ -11,11 +11,11 @@ using Umbraco.Core.Mapping;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Services;
using Umbraco.Extensions;
using Umbraco.Web.BackOffice.Security;
using Umbraco.Web.Common.Attributes;
using Umbraco.Web.Common.Controllers;
using Umbraco.Web.Common.Exceptions;
using Umbraco.Web.Common.Filters;
using Umbraco.Web.Common.Security;
using Umbraco.Web.Models;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Security;

View File

@@ -16,9 +16,9 @@ using Umbraco.Core.Services;
using Umbraco.Core.WebAssets;
using Umbraco.Extensions;
using Umbraco.Web.BackOffice.Filters;
using Umbraco.Web.BackOffice.Security;
using Umbraco.Web.Common.ActionResults;
using Umbraco.Web.Common.Attributes;
using Umbraco.Web.Common.Security;
using Umbraco.Web.Models;
using Umbraco.Web.WebAssets;
using Constants = Umbraco.Core.Constants;
@@ -50,8 +50,7 @@ namespace Umbraco.Web.BackOffice.Controllers
IGridConfig gridConfig,
BackOfficeServerVariables backOfficeServerVariables,
AppCaches appCaches,
BackOfficeSignInManager signInManager // TODO: Review this, do we want it/need it or create our own?
)
BackOfficeSignInManager signInManager)
{
_userManager = userManager;
_runtimeMinifier = runtimeMinifier;

View File

@@ -13,7 +13,7 @@ using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Hosting;
using Umbraco.Core.WebAssets;
using Umbraco.Web.BackOffice.Controllers;
using Umbraco.Web.BackOffice.Security;
using Umbraco.Web.Common.Security;
using Umbraco.Web.Features;
using Umbraco.Web.Models;
using Umbraco.Web.WebApi;

View File

@@ -9,6 +9,7 @@ using Umbraco.Core.Serialization;
using Umbraco.Net;
using Umbraco.Web.BackOffice.Security;
using Umbraco.Web.Common.AspNetCore;
using Umbraco.Web.Common.Security;
namespace Umbraco.Extensions
{
@@ -25,8 +26,9 @@ namespace Umbraco.Extensions
services
.AddAuthentication(Constants.Security.BackOfficeAuthenticationType)
.AddCookie(Constants.Security.BackOfficeAuthenticationType);
// TODO: Need to add more cookie options, see https://github.com/dotnet/aspnetcore/blob/3.0/src/Identity/Core/src/IdentityServiceCollectionExtensions.cs#L45
services.ConfigureOptions<ConfigureUmbracoBackOfficeCookieOptions>();
services.ConfigureOptions<ConfigureBackOfficeCookieOptions>();
}
/// <summary>
@@ -47,8 +49,8 @@ namespace Umbraco.Extensions
.AddClaimsPrincipalFactory<BackOfficeClaimsPrincipalFactory<BackOfficeIdentityUser>>();
// Configure the options specifically for the UmbracoBackOfficeIdentityOptions instance
services.ConfigureOptions<ConfigureUmbracoBackOfficeIdentityOptions>();
//services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<BackOfficeIdentityUser>>();
services.ConfigureOptions<ConfigureBackOfficeIdentityOptions>();
services.ConfigureOptions<ConfigureBackOfficeSecurityStampValidatorOptions>();
}
private static IdentityBuilder BuildUmbracoBackOfficeIdentity(this IServiceCollection services)

View File

@@ -1,7 +1,6 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Web.Common.Extensions;
using Umbraco.Extensions;
namespace Umbraco.Web.BackOffice.Filters
{

View File

@@ -16,6 +16,8 @@ namespace Umbraco.Web.BackOffice.Runtime
{
composition.RegisterUnique<BackOfficeAreaRoutes>();
composition.RegisterUnique<BackOfficeServerVariables>();
composition.RegisterUnique<BackOfficeSessionIdValidator>();
composition.RegisterUnique<BackOfficeSecurityStampValidator>();
composition.RegisterUnique<IBackOfficeAntiforgery, BackOfficeAntiforgery>();
}

View File

@@ -10,12 +10,12 @@ namespace Umbraco.Web.BackOffice.Security
/// Custom secure format that ensures the Identity in the ticket is <see cref="UmbracoBackOfficeIdentity"/> and not just a ClaimsIdentity
/// </summary>
// TODO: Unsure if we really need this, there's no real reason why we have a custom Identity instead of just a ClaimsIdentity
internal class UmbracoSecureDataFormat : ISecureDataFormat<AuthenticationTicket>
internal class BackOfficeSecureDataFormat : ISecureDataFormat<AuthenticationTicket>
{
private readonly int _loginTimeoutMinutes;
private readonly ISecureDataFormat<AuthenticationTicket> _ticketDataFormat;
public UmbracoSecureDataFormat(int loginTimeoutMinutes, ISecureDataFormat<AuthenticationTicket> ticketDataFormat)
public BackOfficeSecureDataFormat(int loginTimeoutMinutes, ISecureDataFormat<AuthenticationTicket> ticketDataFormat)
{
_loginTimeoutMinutes = loginTimeoutMinutes;
_ticketDataFormat = ticketDataFormat ?? throw new ArgumentNullException(nameof(ticketDataFormat));

View File

@@ -0,0 +1,27 @@
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Core.BackOffice;
using Umbraco.Web.Common.Security;
namespace Umbraco.Web.BackOffice.Security
{
/// <summary>
/// A security stamp validator for the back office
/// </summary>
public class BackOfficeSecurityStampValidator : SecurityStampValidator<BackOfficeIdentityUser>
{
public BackOfficeSecurityStampValidator(
IOptions<BackOfficeSecurityStampValidatorOptions> options,
BackOfficeSignInManager signInManager, ISystemClock clock, ILoggerFactory logger)
: base(options, signInManager, clock, logger)
{
}
}
}

View File

@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Identity;
namespace Umbraco.Web.BackOffice.Security
{
/// <summary>
/// Custom <see cref="SecurityStampValidatorOptions"/> for the back office
/// </summary>
public class BackOfficeSecurityStampValidatorOptions : SecurityStampValidatorOptions
{
}
}

View File

@@ -0,0 +1,134 @@

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using Umbraco.Core;
using Umbraco.Core.BackOffice;
using Umbraco.Core.Configuration;
using Umbraco.Core.Hosting;
using Umbraco.Extensions;
namespace Umbraco.Web.BackOffice.Security
{
using ICookieManager = Microsoft.AspNetCore.Authentication.Cookies.ICookieManager;
/// <summary>
/// Static helper class used to configure a CookieAuthenticationProvider to validate a cookie against a user's session id
/// </summary>
/// <remarks>
/// This uses another cookie to track the last checked time which is done for a few reasons:
/// * We can't use the user's auth ticket to do this because we'd be re-issuing the auth ticket all of the time and it would never expire
/// plus the auth ticket size is much larger than this small value
/// * This will execute quite often (every minute per user) and in some cases there might be several requests that end up re-issuing the cookie so the cookie value should be small
/// * We want to avoid the user lookup if it's not required so that will only happen when the time diff is great enough in the cookie
/// </remarks>
public class BackOfficeSessionIdValidator
{
public const string CookieName = "UMB_UCONTEXT_C";
private readonly ISystemClock _systemClock;
private readonly IGlobalSettings _globalSettings;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly BackOfficeUserManager _userManager;
public BackOfficeSessionIdValidator(ISystemClock systemClock, IGlobalSettings globalSettings, IHostingEnvironment hostingEnvironment, BackOfficeUserManager userManager)
{
_systemClock = systemClock;
_globalSettings = globalSettings;
_hostingEnvironment = hostingEnvironment;
_userManager = userManager;
}
public async Task ValidateSessionAsync(TimeSpan validateInterval, CookieValidatePrincipalContext context)
{
if (!context.Request.IsBackOfficeRequest(_globalSettings, _hostingEnvironment))
return;
var valid = await ValidateSessionAsync(validateInterval, context.HttpContext, context.Options.CookieManager, _systemClock, context.Properties.IssuedUtc, context.Principal.Identity as ClaimsIdentity);
if (valid == false)
{
context.RejectPrincipal();
await context.HttpContext.SignOutAsync(Constants.Security.BackOfficeAuthenticationType);
}
}
private async Task<bool> ValidateSessionAsync(
TimeSpan validateInterval,
HttpContext httpContext,
ICookieManager cookieManager,
ISystemClock systemClock,
DateTimeOffset? authTicketIssueDate,
ClaimsIdentity currentIdentity)
{
if (httpContext == null) throw new ArgumentNullException(nameof(httpContext));
if (cookieManager == null) throw new ArgumentNullException(nameof(cookieManager));
if (systemClock == null) throw new ArgumentNullException(nameof(systemClock));
if (currentIdentity == null)
{
return false;
}
DateTimeOffset? issuedUtc = null;
var currentUtc = systemClock.UtcNow;
//read the last checked time from a custom cookie
var lastCheckedCookie = cookieManager.GetRequestCookie(httpContext, CookieName);
if (lastCheckedCookie.IsNullOrWhiteSpace() == false)
{
if (DateTimeOffset.TryParse(lastCheckedCookie, out var parsed))
{
issuedUtc = parsed;
}
}
//no cookie, use the issue time of the auth ticket
if (issuedUtc.HasValue == false)
{
issuedUtc = authTicketIssueDate;
}
// Only validate if enough time has elapsed
var validate = issuedUtc.HasValue == false;
if (issuedUtc.HasValue)
{
var timeElapsed = currentUtc.Subtract(issuedUtc.Value);
validate = timeElapsed > validateInterval;
}
if (validate == false)
return true;
var userId = currentIdentity.GetUserId();
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
return false;
var sessionId = currentIdentity.FindFirstValue(Constants.Security.SessionIdClaimType);
if (await _userManager.ValidateSessionIdAsync(userId, sessionId) == false)
return false;
//we will re-issue the cookie last checked cookie
cookieManager.AppendResponseCookie(
httpContext,
CookieName,
DateTimeOffset.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffffffzzz"),
new CookieOptions
{
HttpOnly = true,
Secure = _globalSettings.UseHttps || httpContext.Request.IsHttps,
Path = "/"
});
return true;
}
}
}

View File

@@ -0,0 +1,185 @@
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Umbraco.Core;
using Umbraco.Core.BackOffice;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Hosting;
using Umbraco.Core.Services;
using Umbraco.Net;
using Umbraco.Core.Security;
using Umbraco.Web;
namespace Umbraco.Web.BackOffice.Security
{
/// <summary>
/// Used to configure <see cref="CookieAuthenticationOptions"/> for the back office authentication type
/// </summary>
public class ConfigureBackOfficeCookieOptions : IConfigureNamedOptions<CookieAuthenticationOptions>
{
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly ISecuritySettings _securitySettings;
private readonly IGlobalSettings _globalSettings;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IRuntimeState _runtimeState;
private readonly IDataProtectionProvider _dataProtection;
private readonly IRequestCache _requestCache;
private readonly IUserService _userService;
private readonly IIpResolver _ipResolver;
private readonly BackOfficeSessionIdValidator _sessionIdValidator;
private readonly BackOfficeSecurityStampValidator _securityStampValidator;
public ConfigureBackOfficeCookieOptions(
IUmbracoContextAccessor umbracoContextAccessor,
ISecuritySettings securitySettings,
IGlobalSettings globalSettings,
IHostingEnvironment hostingEnvironment,
IRuntimeState runtimeState,
IDataProtectionProvider dataProtection,
IRequestCache requestCache,
IUserService userService,
IIpResolver ipResolver,
BackOfficeSessionIdValidator sessionIdValidator,
BackOfficeSecurityStampValidator securityStampValidator)
{
_umbracoContextAccessor = umbracoContextAccessor;
_securitySettings = securitySettings;
_globalSettings = globalSettings;
_hostingEnvironment = hostingEnvironment;
_runtimeState = runtimeState;
_dataProtection = dataProtection;
_requestCache = requestCache;
_userService = userService;
_ipResolver = ipResolver;
_sessionIdValidator = sessionIdValidator;
_securityStampValidator = securityStampValidator;
}
public void Configure(string name, CookieAuthenticationOptions options)
{
if (name != Constants.Security.BackOfficeAuthenticationType) return;
Configure(options);
}
public void Configure(CookieAuthenticationOptions options)
{
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(_globalSettings.TimeOutInMinutes);
options.Cookie.Domain = _securitySettings.AuthCookieDomain;
options.Cookie.Name = _securitySettings.AuthCookieName;
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = _globalSettings.UseHttps ? CookieSecurePolicy.Always : CookieSecurePolicy.SameAsRequest;
options.Cookie.Path = "/";
options.DataProtectionProvider = _dataProtection;
// NOTE: This is borrowed directly from aspnetcore source
// Note: the purpose for the data protector must remain fixed for interop to work.
var dataProtector = options.DataProtectionProvider.CreateProtector("Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", Constants.Security.BackOfficeAuthenticationType, "v2");
var ticketDataFormat = new TicketDataFormat(dataProtector);
options.TicketDataFormat = new BackOfficeSecureDataFormat(_globalSettings.TimeOutInMinutes, ticketDataFormat);
//Custom cookie manager so we can filter requests
options.CookieManager = new BackOfficeCookieManager(
_umbracoContextAccessor,
_runtimeState,
_hostingEnvironment,
_globalSettings,
_requestCache);
// _explicitPaths); TODO: Implement this once we do OAuth somehow
options.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = async ctx =>
{
//ensure the thread culture is set
ctx.Principal?.Identity?.EnsureCulture();
await EnsureValidSessionId(ctx);
if (ctx.Principal?.Identity == null)
{
await ctx.HttpContext.SignOutAsync(Constants.Security.BackOfficeAuthenticationType);
return;
}
await _securityStampValidator.ValidateAsync(ctx);
},
OnSignedIn = ctx =>
{
// When we are signed in with the cookie, assign the principal to the current HttpContext
ctx.HttpContext.User = ctx.Principal;
if (ctx.Principal.Identity is UmbracoBackOfficeIdentity backOfficeIdentity)
{
//generate a session id and assign it
//create a session token - if we are configured and not in an upgrade state then use the db, otherwise just generate one
var session = _runtimeState.Level == RuntimeLevel.Run
? _userService.CreateLoginSession(backOfficeIdentity.Id, _ipResolver.GetCurrentRequestIpAddress())
: Guid.NewGuid();
backOfficeIdentity.SessionId = session.ToString();
//since it is a cookie-based authentication add that claim
backOfficeIdentity.AddClaim(new Claim(ClaimTypes.CookiePath, "/", ClaimValueTypes.String, UmbracoBackOfficeIdentity.Issuer, UmbracoBackOfficeIdentity.Issuer, backOfficeIdentity));
}
return Task.CompletedTask;
},
OnSigningOut = ctx =>
{
//Clear the user's session on sign out
if (ctx.HttpContext?.User?.Identity != null)
{
var claimsIdentity = ctx.HttpContext.User.Identity as ClaimsIdentity;
var sessionId = claimsIdentity.FindFirstValue(Constants.Security.SessionIdClaimType);
if (sessionId.IsNullOrWhiteSpace() == false && Guid.TryParse(sessionId, out var guidSession))
{
_userService.ClearLoginSession(guidSession);
}
}
// Remove all of our cookies
var cookies = new[]
{
BackOfficeSessionIdValidator.CookieName,
_securitySettings.AuthCookieName,
Constants.Web.PreviewCookieName,
Constants.Security.BackOfficeExternalCookieName
};
foreach (var cookie in cookies)
{
ctx.Options.CookieManager.DeleteCookie(ctx.HttpContext, cookie, new CookieOptions
{
Path = "/"
});
}
return Task.CompletedTask;
}
};
}
/// <summary>
/// Ensures that the user has a valid session id
/// </summary>
/// <remarks>
/// So that we are not overloading the database this throttles it's check to every minute
/// </remarks>
private async Task EnsureValidSessionId(CookieValidatePrincipalContext context)
{
if (_runtimeState.Level == RuntimeLevel.Run)
await _sessionIdValidator.ValidateSessionAsync(TimeSpan.FromMinutes(1), context);
}
}
}

View File

@@ -11,11 +11,11 @@ namespace Umbraco.Web.BackOffice.Security
/// <summary>
/// Used to configure <see cref="BackOfficeIdentityOptions"/> for the Umbraco Back office
/// </summary>
public class ConfigureUmbracoBackOfficeIdentityOptions : IConfigureOptions<BackOfficeIdentityOptions>
public class ConfigureBackOfficeIdentityOptions : IConfigureOptions<BackOfficeIdentityOptions>
{
private readonly IUserPasswordConfiguration _userPasswordConfiguration;
public ConfigureUmbracoBackOfficeIdentityOptions(IUserPasswordConfiguration userPasswordConfiguration)
public ConfigureBackOfficeIdentityOptions(IUserPasswordConfiguration userPasswordConfiguration)
{
_userPasswordConfiguration = userPasswordConfiguration;
}

View File

@@ -0,0 +1,18 @@
using Microsoft.Extensions.Options;
using System;
namespace Umbraco.Web.BackOffice.Security
{
/// <summary>
/// Configures the back office security stamp options
/// </summary>
public class ConfigureBackOfficeSecurityStampValidatorOptions : IConfigureOptions<BackOfficeSecurityStampValidatorOptions>
{
public void Configure(BackOfficeSecurityStampValidatorOptions options)
{
options.ValidationInterval = TimeSpan.FromMinutes(30);
}
}
}

View File

@@ -1,94 +0,0 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Hosting;
using Umbraco.Web;
namespace Umbraco.Web.BackOffice.Security
{
/// <summary>
/// Used to configure <see cref="CookieAuthenticationOptions"/> for the back office authentication type
/// </summary>
public class ConfigureUmbracoBackOfficeCookieOptions : IConfigureNamedOptions<CookieAuthenticationOptions>
{
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly ISecuritySettings _securitySettings;
private readonly IGlobalSettings _globalSettings;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IRuntimeState _runtimeState;
private readonly IDataProtectionProvider _dataProtection;
private readonly IRequestCache _requestCache;
public ConfigureUmbracoBackOfficeCookieOptions(
IUmbracoContextAccessor umbracoContextAccessor,
ISecuritySettings securitySettings,
IGlobalSettings globalSettings,
IHostingEnvironment hostingEnvironment,
IRuntimeState runtimeState,
IDataProtectionProvider dataProtection,
IRequestCache requestCache)
{
_umbracoContextAccessor = umbracoContextAccessor;
_securitySettings = securitySettings;
_globalSettings = globalSettings;
_hostingEnvironment = hostingEnvironment;
_runtimeState = runtimeState;
_dataProtection = dataProtection;
_requestCache = requestCache;
}
public void Configure(string name, CookieAuthenticationOptions options)
{
if (name != Constants.Security.BackOfficeAuthenticationType) return;
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(_globalSettings.TimeOutInMinutes);
options.Cookie.Domain = _securitySettings.AuthCookieDomain;
options.Cookie.Name = _securitySettings.AuthCookieName;
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = _globalSettings.UseHttps ? CookieSecurePolicy.Always : CookieSecurePolicy.SameAsRequest;
options.Cookie.Path = "/";
options.DataProtectionProvider = _dataProtection;
// NOTE: This is borrowed directly from aspnetcore source
// Note: the purpose for the data protector must remain fixed for interop to work.
var dataProtector = options.DataProtectionProvider.CreateProtector("Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", name, "v2");
var ticketDataFormat = new TicketDataFormat(dataProtector);
options.TicketDataFormat = new UmbracoSecureDataFormat(_globalSettings.TimeOutInMinutes, ticketDataFormat);
//Custom cookie manager so we can filter requests
options.CookieManager = new BackOfficeCookieManager(
_umbracoContextAccessor,
_runtimeState,
_hostingEnvironment,
_globalSettings,
_requestCache);
// _explicitPaths); TODO: Implement this once we do OAuth somehow
options.Events = new CookieAuthenticationEvents
{
OnSignedIn = ctx =>
{
// When we are signed in with the cookie, assign the principal to the current HttpContext
ctx.HttpContext.User = ctx.Principal;
return Task.CompletedTask;
}
};
}
public void Configure(CookieAuthenticationOptions options)
{
}
}
}

View File

@@ -1,10 +1,25 @@
using System.Net;
using System;
using System.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Hosting;
namespace Umbraco.Web.Common.Extensions
namespace Umbraco.Extensions
{
public static class HttpRequestExtensions
{
public static bool IsBackOfficeRequest(this HttpRequest request, IGlobalSettings globalSettings, IHostingEnvironment hostingEnvironment)
{
return new Uri(request.GetEncodedUrl(), UriKind.RelativeOrAbsolute).IsBackOfficeRequest(globalSettings, hostingEnvironment);
}
public static bool IsClientSideRequest(this HttpRequest request)
{
return new Uri(request.GetEncodedUrl(), UriKind.RelativeOrAbsolute).IsClientSideRequest();
}
internal static string ClientCulture(this HttpRequest request)
{
return request.Headers.TryGetValue("X-UMB-CULTURE", out var values) ? values[0] : null;

View File

@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Http.Extensions;
using Serilog.Context;
using Umbraco.Core;
using Umbraco.Core.Logging.Serilog.Enrichers;
using Umbraco.Extensions;
namespace Umbraco.Web.Common.Middleware
{
@@ -30,7 +31,7 @@ namespace Umbraco.Web.Common.Middleware
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
// do not process if client-side request
if (new Uri(context.Request.GetEncodedUrl(), UriKind.RelativeOrAbsolute).IsClientSideRequest())
if (context.Request.IsClientSideRequest())
{
await next(context);
return;

View File

@@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Primitives;
using Umbraco.Core;
using Umbraco.Web.Common.Extensions;
using Umbraco.Extensions;
namespace Umbraco.Web.Common.ModelBinders
{

View File

@@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Http.Extensions;
using StackExchange.Profiling;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Extensions;
namespace Umbraco.Web.Common.Profiler
{
@@ -70,7 +71,7 @@ namespace Umbraco.Web.Common.Profiler
private static bool ShouldProfile(HttpRequest request)
{
if (new Uri(request.GetEncodedUrl(), UriKind.RelativeOrAbsolute).IsClientSideRequest()) return false;
if (request.IsClientSideRequest()) return false;
if (bool.TryParse(request.Query["umbDebug"], out var umbDebug)) return umbDebug;
if (bool.TryParse(request.Headers["X-UMB-DEBUG"], out var xUmbDebug)) return xUmbDebug;
if (bool.TryParse(request.Cookies["UMB-DEBUG"], out var cUmbDebug)) return cUmbDebug;

View File

@@ -11,8 +11,12 @@ using System.Threading.Tasks;
using Umbraco.Core;
using Umbraco.Core.BackOffice;
namespace Umbraco.Web.BackOffice.Security
namespace Umbraco.Web.Common.Security
{
using Constants = Umbraco.Core.Constants;
// TODO: There's potential to extract an interface for this for only what we use and put that in Core without aspnetcore refs, but we need to wait till were done with it since there's a bit to implement
public class BackOfficeSignInManager : SignInManager<BackOfficeIdentityUser>
{
private readonly BackOfficeUserManager _userManager;
@@ -30,6 +34,17 @@ namespace Umbraco.Web.BackOffice.Security
_userManager = userManager;
}
// TODO: Implement these, this is what the security stamp thingy calls
public override Task<bool> ValidateSecurityStampAsync(BackOfficeIdentityUser user, string securityStamp)
{
return base.ValidateSecurityStampAsync(user, securityStamp);
}
// TODO: Implement these, this is what the security stamp thingy calls
public override Task<BackOfficeIdentityUser> ValidateSecurityStampAsync(ClaimsPrincipal principal)
{
return base.ValidateSecurityStampAsync(principal);
}
// TODO: Need to migrate more from Umbraco.Web.Security.BackOfficeSignInManager
// Things like dealing with auto-linking, cookie options, and a ton of other stuff. Some might not need to be ported but it
// will be a case by case basis.
@@ -132,21 +147,13 @@ namespace Umbraco.Web.BackOffice.Security
_userManager.RaiseLoginSuccessEvent(user, user.Id);
}
else if (result.IsLockedOut)
{
Logger.LogInformation("Login attempt failed for username {UserName} from IP address {IpAddress}, the user is locked", username, Context.Connection.RemoteIpAddress);
}
else if (result.RequiresTwoFactor)
{
Logger.LogInformation("Login attempt requires verification for username {UserName} from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress);
}
else if (!result.Succeeded || result.IsNotAllowed)
{
Logger.LogInformation("Login attempt failed for username {UserName} from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress);
}
else
{
throw new ArgumentOutOfRangeException();
}
return result;
}

View File

@@ -23,7 +23,11 @@ namespace Umbraco.Web.Common.Security
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IHttpContextAccessor _httpContextAccessor;
public WebSecurity(IUserService userService, IGlobalSettings globalSettings, IHostingEnvironment hostingEnvironment, IHttpContextAccessor httpContextAccessor)
public WebSecurity(
IUserService userService,
IGlobalSettings globalSettings,
IHostingEnvironment hostingEnvironment,
IHttpContextAccessor httpContextAccessor)
{
_userService = userService;
_globalSettings = globalSettings;
@@ -63,11 +67,6 @@ namespace Umbraco.Web.Common.Security
return ValidateCurrentUser(throwExceptions);
}
public void ClearCurrentLogin()
{
//throw new NotImplementedException();
}
public Attempt<int> GetUserId()
{
return Attempt.Succeed(-1);
@@ -79,11 +78,6 @@ namespace Umbraco.Web.Common.Security
return httpContext?.User != null && httpContext.User.Identity.IsAuthenticated && httpContext.GetCurrentIdentity() != null;
}
public double PerformLogin(int userId)
{
return 100;
}
public bool UserHasSectionAccess(string section, IUser user)
{
return true;

View File

@@ -52,11 +52,11 @@ namespace Umbraco.Web
_defaultCultureAccessor = defaultCultureAccessor ?? throw new ArgumentNullException(nameof(defaultCultureAccessor));
_globalSettings = globalSettings ?? throw new ArgumentNullException(nameof(globalSettings));
_userService = userService ?? throw new ArgumentNullException(nameof(userService));
_hostingEnvironment = hostingEnvironment;
_uriUtility = uriUtility;
_httpContextAccessor = httpContextAccessor;
_cookieManager = cookieManager;
_requestAccessor = requestAccessor;
_hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
_uriUtility = uriUtility ?? throw new ArgumentNullException(nameof(uriUtility));
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
_cookieManager = cookieManager ?? throw new ArgumentNullException(nameof(cookieManager));
_requestAccessor = requestAccessor ?? throw new ArgumentNullException(nameof(requestAccessor));
}
private IUmbracoContext CreateUmbracoContext()

View File

@@ -2,7 +2,7 @@
@using Umbraco.Web.Composing
@using Umbraco.Web
@using Umbraco.Web.WebAssets
@using Umbraco.Web.BackOffice.Security
@using Umbraco.Web.Common.Security
@using Umbraco.Core.WebAssets
@using Umbraco.Core.Configuration
@using Umbraco.Core.Hosting

View File

@@ -147,19 +147,6 @@ namespace Umbraco.Web.Security
//Create the default options and provider
var authOptions = app.CreateUmbracoCookieAuthOptions(umbracoContextAccessor, globalSettings, runtimeState, securitySettings, hostingEnvironment, requestCache);
authOptions.Provider = new BackOfficeCookieAuthenticationProvider(userService, runtimeState, globalSettings, hostingEnvironment, securitySettings)
{
// 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.
OnValidateIdentity = UmbracoSecurityStampValidator
.OnValidateIdentity<BackOfficeSignInManager, BackOfficeOwinUserManager, BackOfficeIdentityUser>(
TimeSpan.FromMinutes(30),
(signInManager, manager, user) => signInManager.CreateUserIdentityAsync(user),
identity => identity.GetUserId()),
};
return app.UseUmbracoBackOfficeCookieAuthentication(umbracoContextAccessor, runtimeState, globalSettings, securitySettings, hostingEnvironment, requestCache, authOptions, stage);
}

View File

@@ -68,7 +68,7 @@ namespace Umbraco.Web.Security
if (ex is FormatException || ex is JsonReaderException)
{
// this will occur if the cookie data is invalid
http.UmbracoLogout();
}
else
{
@@ -86,15 +86,10 @@ namespace Umbraco.Web.Security
/// This will return the current back office identity.
/// </summary>
/// <param name="http"></param>
/// <param name="authenticateRequestIfNotFound">
/// If set to true and a back office identity is not found and not authenticated, this will attempt to authenticate the
/// request just as is done in the Umbraco module and then set the current identity if it is valid.
/// Just like in the UmbracoModule, if this is true then the user's culture will be assigned to the request.
/// </param>
/// <returns>
/// Returns the current back office identity if an admin is authenticated otherwise null
/// </returns>
public static UmbracoBackOfficeIdentity GetCurrentIdentity(this HttpContextBase http, bool authenticateRequestIfNotFound)
public static UmbracoBackOfficeIdentity GetCurrentIdentity(this HttpContextBase http)
{
if (http == null) throw new ArgumentNullException(nameof(http));
if (http.User == null) return null; //there's no user at all so no identity
@@ -103,59 +98,20 @@ namespace Umbraco.Web.Security
var backOfficeIdentity = http.User.GetUmbracoIdentity();
if (backOfficeIdentity != null) return backOfficeIdentity;
if (authenticateRequestIfNotFound == false) return null;
// even if authenticateRequestIfNotFound is true we cannot continue if the request is actually authenticated
// which would mean something strange is going on that it is not an umbraco identity.
if (http.User.Identity.IsAuthenticated) return null;
// So the user is not authed but we've been asked to do the auth if authenticateRequestIfNotFound = true,
// which might occur in old webforms style things or for routes that aren't included as a back office request.
// in this case, we are just reverting to authing using the cookie.
// TODO: Even though this is in theory legacy, we have legacy bits laying around and we'd need to do the auth based on
// how the Module will eventually do it (by calling in to any registered authenticators).
var ticket = http.GetUmbracoAuthTicket();
if (http.AuthenticateCurrentRequest(ticket, true))
{
//now we 'should have an umbraco identity
return http.User.Identity as UmbracoBackOfficeIdentity;
}
return null;
}
/// <summary>
/// This will return the current back office identity.
/// </summary>
/// <param name="http"></param>
/// <param name="authenticateRequestIfNotFound">
/// If set to true and a back office identity is not found and not authenticated, this will attempt to authenticate the
/// request just as is done in the Umbraco module and then set the current identity if it is valid
/// </param>
/// <param name="http"></param>
/// <returns>
/// Returns the current back office identity if an admin is authenticated otherwise null
/// </returns>
internal static UmbracoBackOfficeIdentity GetCurrentIdentity(this HttpContext http, bool authenticateRequestIfNotFound)
internal static UmbracoBackOfficeIdentity GetCurrentIdentity(this HttpContext http)
{
if (http == null) throw new ArgumentNullException("http");
return new HttpContextWrapper(http).GetCurrentIdentity(authenticateRequestIfNotFound);
}
public static void UmbracoLogout(this HttpContextBase http)
{
if (http == null) throw new ArgumentNullException("http");
Logout(http, Current.Configs.Security().AuthCookieName);
}
/// <summary>
/// This clears the forms authentication cookie
/// </summary>
/// <param name="http"></param>
internal static void UmbracoLogout(this HttpContext http)
{
if (http == null) throw new ArgumentNullException("http");
new HttpContextWrapper(http).UmbracoLogout();
return new HttpContextWrapper(http).GetCurrentIdentity();
}
/// <summary>
@@ -218,52 +174,6 @@ namespace Umbraco.Web.Security
return GetAuthTicket(ctx, Current.Configs.Security().AuthCookieName);
}
/// <summary>
/// This clears the forms authentication cookie
/// </summary>
/// <param name="http"></param>
/// <param name="cookieName"></param>
private static void Logout(this HttpContextBase http, string cookieName)
{
// We need to clear the sessionId from the database. This is legacy code to do any logging out and shouldn't really be used at all but in any case
// we need to make sure the session is cleared. Due to the legacy nature of this it means we need to use singletons
if (http.User != null)
{
var claimsIdentity = http.User.Identity as ClaimsIdentity;
if (claimsIdentity != null)
{
var sessionId = claimsIdentity.FindFirstValue(Constants.Security.SessionIdClaimType);
Guid guidSession;
if (sessionId.IsNullOrWhiteSpace() == false && Guid.TryParse(sessionId, out guidSession))
{
Current.Services.UserService.ClearLoginSession(guidSession);
}
}
}
if (http == null) throw new ArgumentNullException("http");
// clear the preview cookie and external login
var cookies = new[] { cookieName, Constants.Web.PreviewCookieName, Constants.Security.BackOfficeExternalCookieName };
foreach (var c in cookies)
{
// remove from the request
http.Request.Cookies.Remove(c);
// expire from the response
var formsCookie = http.Response.Cookies[c];
if (formsCookie != null)
{
// this will expire immediately and be removed from the browser
formsCookie.Expires = DateTime.Now.AddYears(-1);
}
else
{
// ensure there's def an expired cookie
http.Response.Cookies.Add(new HttpCookie(c) { Expires = DateTime.Now.AddYears(-1) });
}
}
}
private static AuthenticationTicket GetAuthTicket(this IOwinContext owinCtx, string cookieName)
{
var asDictionary = new Dictionary<string, string>();
@@ -313,7 +223,7 @@ namespace Umbraco.Web.Security
catch (Exception)
{
// occurs when decryption fails
http.Logout(cookieName);
return null;
}
}
@@ -334,23 +244,6 @@ namespace Umbraco.Web.Security
return secureDataFormat.Unprotect(formsCookie);
}
/// <summary>
/// Ensures that the thread culture is set based on the back office user's culture
/// </summary>
/// <param name="identity"></param>
public static void EnsureCulture(this IIdentity identity)
{
if (identity is UmbracoBackOfficeIdentity umbIdentity && umbIdentity.IsAuthenticated)
{
Thread.CurrentThread.CurrentUICulture =
Thread.CurrentThread.CurrentCulture = UserCultures.GetOrAdd(umbIdentity.Culture, s => new CultureInfo(s));
}
}
/// <summary>
/// Used so that we aren't creating a new CultureInfo object for every single request
/// </summary>
private static readonly ConcurrentDictionary<string, CultureInfo> UserCultures = new ConcurrentDictionary<string, CultureInfo>();
}
}

View File

@@ -9,9 +9,12 @@ using Umbraco.Core.Configuration;
using Umbraco.Core.Services;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Hosting;
using Umbraco.Core.Security;
namespace Umbraco.Web.Security
{
// TODO: Migrate this logic to cookie events in ConfigureUmbracoBackOfficeCookieOptions
public class BackOfficeCookieAuthenticationProvider : CookieAuthenticationProvider
{
private readonly IUserService _userService;
@@ -29,97 +32,12 @@ namespace Umbraco.Web.Security
_securitySettings = securitySettings;
}
public override void ResponseSignIn(CookieResponseSignInContext context)
{
if (context.Identity is UmbracoBackOfficeIdentity backOfficeIdentity)
{
//generate a session id and assign it
//create a session token - if we are configured and not in an upgrade state then use the db, otherwise just generate one
var session = _runtimeState.Level == RuntimeLevel.Run
? _userService.CreateLoginSession(backOfficeIdentity.Id, context.OwinContext.GetCurrentRequestIpAddress())
: Guid.NewGuid();
backOfficeIdentity.SessionId = session.ToString();
//since it is a cookie-based authentication add that claim
backOfficeIdentity.AddClaim(new Claim(ClaimTypes.CookiePath, "/", ClaimValueTypes.String, UmbracoBackOfficeIdentity.Issuer, UmbracoBackOfficeIdentity.Issuer, backOfficeIdentity));
}
base.ResponseSignIn(context);
}
public override void ResponseSignOut(CookieResponseSignOutContext context)
{
//Clear the user's session on sign out
if (context?.OwinContext?.Authentication?.User?.Identity != null)
{
var claimsIdentity = context.OwinContext.Authentication.User.Identity as ClaimsIdentity;
var sessionId = claimsIdentity.FindFirstValue(Constants.Security.SessionIdClaimType);
if (sessionId.IsNullOrWhiteSpace() == false && Guid.TryParse(sessionId, out var guidSession))
{
_userService.ClearLoginSession(guidSession);
}
}
base.ResponseSignOut(context);
//Make sure the definitely all of these cookies are cleared when signing out with cookies
context.Response.Cookies.Append(SessionIdValidator.CookieName, "", new CookieOptions
{
Expires = DateTime.Now.AddYears(-1),
Path = "/"
});
context.Response.Cookies.Append(_securitySettings.AuthCookieName, "", new CookieOptions
{
Expires = DateTime.Now.AddYears(-1),
Path = "/"
});
context.Response.Cookies.Append(Constants.Web.PreviewCookieName, "", new CookieOptions
{
Expires = DateTime.Now.AddYears(-1),
Path = "/"
});
context.Response.Cookies.Append(Constants.Security.BackOfficeExternalCookieName, "", new CookieOptions
{
Expires = DateTime.Now.AddYears(-1),
Path = "/"
});
}
/// <summary>
/// Ensures that the culture is set correctly for the current back office user and that the user's session token is valid
/// </summary>
/// <param name="context"/>
/// <returns/>
public override async Task ValidateIdentity(CookieValidateIdentityContext context)
{
//ensure the thread culture is set
context?.Identity?.EnsureCulture();
await EnsureValidSessionId(context);
if (context?.Identity == null)
{
context?.OwinContext.Authentication.SignOut(context.Options.AuthenticationType);
return;
}
await base.ValidateIdentity(context);
}
/// <summary>
/// Ensures that the user has a valid session id
/// </summary>
/// <remarks>
/// So that we are not overloading the database this throttles it's check to every minute
/// </remarks>
protected virtual async Task EnsureValidSessionId(CookieValidateIdentityContext context)
{
if (_runtimeState.Level == RuntimeLevel.Run)
await SessionIdValidator.ValidateSessionAsync(TimeSpan.FromMinutes(1), context, _globalSettings, _hostingEnvironment);
}
}

View File

@@ -1,88 +0,0 @@
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Owin.Security.Cookies;
using Umbraco.Core;
using Umbraco.Core.BackOffice;
namespace Umbraco.Web.Security
{
/// <summary>
/// Adapted from Microsoft.AspNet.Identity.Owin.SecurityStampValidator
/// </summary>
public class UmbracoSecurityStampValidator
{
public static Func<CookieValidateIdentityContext, Task> OnValidateIdentity<TSignInManager, TManager, TUser>(
TimeSpan validateInterval,
Func<BackOfficeSignInManager, TManager, TUser, Task<ClaimsIdentity>> regenerateIdentityCallback,
Func<ClaimsIdentity, string> getUserIdCallback)
where TSignInManager : BackOfficeSignInManager
where TManager : BackOfficeUserManager<TUser>
where TUser : BackOfficeIdentityUser
{
if (getUserIdCallback == null) throw new ArgumentNullException(nameof(getUserIdCallback));
return async context =>
{
var currentUtc = context.Options?.SystemClock?.UtcNow ?? DateTimeOffset.UtcNow;
var issuedUtc = context.Properties.IssuedUtc;
// Only validate if enough time has elapsed
var validate = issuedUtc == null;
if (issuedUtc != null)
{
var timeElapsed = currentUtc.Subtract(issuedUtc.Value);
validate = timeElapsed > validateInterval;
}
if (validate)
{
var manager = context.OwinContext.Get<TManager>();
if (manager == null) throw new InvalidOperationException("Unable to load BackOfficeUserManager");
var signInManager = context.OwinContext.Get<TSignInManager>();
if (signInManager == null) throw new InvalidOperationException("Unable to load BackOfficeSignInManager");
var userId = getUserIdCallback(context.Identity);
if (userId != null)
{
var user = await manager.FindByIdAsync(userId);
var reject = true;
// Refresh the identity if the stamp matches, otherwise reject
if (user != null && manager.SupportsUserSecurityStamp)
{
var securityStamp = context.Identity.FindFirstValue(Constants.Web.SecurityStampClaimType);
var newSecurityStamp = await manager.GetSecurityStampAsync(user);
if (securityStamp == newSecurityStamp)
{
reject = false;
// Regenerate fresh claims if possible and resign in
if (regenerateIdentityCallback != null)
{
var identity = await regenerateIdentityCallback.Invoke(signInManager, manager, user);
if (identity != null)
{
// Fix for regression where this value is not updated
// Setting it to null so that it is refreshed by the cookie middleware
context.Properties.IssuedUtc = null;
context.Properties.ExpiresUtc = null;
context.OwinContext.Authentication.SignIn(context.Properties, identity);
}
}
}
}
if (reject)
{
context.RejectIdentity();
context.OwinContext.Authentication.SignOut(context.Options.AuthenticationType);
}
}
}
};
}
}
}

View File

@@ -74,41 +74,13 @@ namespace Umbraco.Web.Security
protected BackOfficeOwinUserManager UserManager
=> _userManager ?? (_userManager = _httpContextAccessor.GetRequiredHttpContext().GetOwinContext().GetBackOfficeUserManager());
[Obsolete("This needs to be removed, ASP.NET Identity should always be used for this operation, this is currently only used in the installer which needs to be updated")]
public double PerformLogin(int userId)
{
var httpContext = _httpContextAccessor.GetRequiredHttpContext();
var owinCtx = httpContext.GetOwinContext();
//ensure it's done for owin too
owinCtx.Authentication.SignOut(Constants.Security.BackOfficeExternalAuthenticationType);
var user = UserManager.FindByIdAsync(userId.ToString()).Result;
SignInManager.SignInAsync(user, isPersistent: true, rememberBrowser: false).Wait();
httpContext.SetPrincipalForRequest(owinCtx.Request.User);
return TimeSpan.FromMinutes(_globalSettings.TimeOutInMinutes).TotalSeconds;
}
[Obsolete("This needs to be removed, ASP.NET Identity should always be used for this operation, this is currently only used in the installer which needs to be updated")]
public void ClearCurrentLogin()
{
var httpContext = _httpContextAccessor.GetRequiredHttpContext();
httpContext.UmbracoLogout();
httpContext.GetOwinContext().Authentication.SignOut(
Core.Constants.Security.BackOfficeAuthenticationType,
Core.Constants.Security.BackOfficeExternalAuthenticationType);
}
/// <summary>
/// Gets the current user's id.
/// </summary>
/// <returns></returns>
public Attempt<int> GetUserId()
{
var identity = _httpContextAccessor.GetRequiredHttpContext().GetCurrentIdentity(false);
var identity = _httpContextAccessor.GetRequiredHttpContext().GetCurrentIdentity();
return identity == null ? Attempt.Fail<int>() : Attempt.Succeed(Convert.ToInt32(identity.Id));
}
@@ -191,7 +163,7 @@ namespace Umbraco.Web.Security
public bool IsAuthenticated()
{
var httpContext = _httpContextAccessor.HttpContext;
return httpContext?.User != null && httpContext.User.Identity.IsAuthenticated && httpContext.GetCurrentIdentity(false) != null;
return httpContext?.User != null && httpContext.User.Identity.IsAuthenticated && httpContext.GetCurrentIdentity() != null;
}
}

View File

@@ -201,7 +201,6 @@
<Compile Include="Security\OwinDataProtectorTokenProvider.cs" />
<Compile Include="Security\PublicAccessChecker.cs" />
<Compile Include="Security\UmbracoMembershipProviderBase.cs" />
<Compile Include="Security\UmbracoSecurityStampValidator.cs" />
<Compile Include="Security\UserAwarePasswordHasher.cs" />
<Compile Include="StringExtensions.cs" />
<Compile Include="Trees\ITreeNodeController.cs" />

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Web;
using System.Web.Routing;
using Umbraco.Core;
using Umbraco.Core.Security;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
using Umbraco.Core.Exceptions;