Merge remote-tracking branch 'origin/netcore/netcore' into netcore/feature/macros

Signed-off-by: Bjarke Berg <mail@bergmania.dk>

# Conflicts:
#	src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs
This commit is contained in:
Bjarke Berg
2020-06-10 12:02:53 +02:00
79 changed files with 881 additions and 1423 deletions

View File

@@ -1,4 +1,5 @@
using System;
using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
@@ -26,20 +27,42 @@ namespace Umbraco.Extensions
if (backOfficeIdentity != null) return backOfficeIdentity;
}
//Otherwise convert to a UmbracoBackOfficeIdentity if it's auth'd and has the back office session
if (user.Identity is ClaimsIdentity claimsIdentity && claimsIdentity.IsAuthenticated && claimsIdentity.HasClaim(x => x.Type == Constants.Security.SessionIdClaimType))
//Otherwise convert to a UmbracoBackOfficeIdentity if it's auth'd
if (user.Identity is ClaimsIdentity claimsIdentity
&& claimsIdentity.IsAuthenticated
&& UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity, out var umbracoIdentity))
{
try
{
return UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity);
}
catch (InvalidOperationException)
{
// TODO: Look into this? Why did we do this, see git history and add some notes
}
return umbracoIdentity;
}
return null;
}
/// <summary>
/// Returns the remaining seconds on an auth ticket for the user based on the claim applied to the user durnig authentication
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public static double GetRemainingAuthSeconds(this IPrincipal user) => user.GetRemainingAuthSeconds(DateTimeOffset.UtcNow);
/// <summary>
/// Returns the remaining seconds on an auth ticket for the user based on the claim applied to the user durnig authentication
/// </summary>
/// <param name="user"></param>
/// <param name="now"></param>
/// <returns></returns>
public static double GetRemainingAuthSeconds(this IPrincipal user, DateTimeOffset now)
{
var umbIdentity = user.GetUmbracoIdentity();
if (umbIdentity == null) return 0;
var ticketExpires = umbIdentity.FindFirstValue(Constants.Security.TicketExpiresClaimType);
if (ticketExpires.IsNullOrWhiteSpace()) return 0;
var utcExpired = DateTimeOffset.Parse(ticketExpires, null, DateTimeStyles.RoundtripKind);
var secondsRemaining = utcExpired.Subtract(now).TotalSeconds;
return secondsRemaining;
}
}
}

View File

@@ -9,16 +9,34 @@ namespace Umbraco.Core.BackOffice
/// <summary>
/// A custom user identity for the Umbraco backoffice
/// </summary>
/// <remarks>
/// This inherits from FormsIdentity for backwards compatibility reasons since we still support the forms auth cookie, in v8 we can
/// change over to 'pure' asp.net identity and just inherit from ClaimsIdentity.
/// </remarks>
[Serializable]
public class UmbracoBackOfficeIdentity : ClaimsIdentity
{
public static UmbracoBackOfficeIdentity FromClaimsIdentity(ClaimsIdentity identity)
public static bool FromClaimsIdentity(ClaimsIdentity identity, out UmbracoBackOfficeIdentity backOfficeIdentity)
{
return new UmbracoBackOfficeIdentity(identity);
//validate that all claims exist
foreach (var t in RequiredBackOfficeIdentityClaimTypes)
{
//if the identity doesn't have the claim, or the claim value is null
if (identity.HasClaim(x => x.Type == t) == false || identity.HasClaim(x => x.Type == t && x.Value.IsNullOrWhiteSpace()))
{
backOfficeIdentity = null;
return false;
}
}
backOfficeIdentity = new UmbracoBackOfficeIdentity(identity);
return true;
}
/// <summary>
/// Create a back office identity based on an existing claims identity
/// </summary>
/// <param name="identity"></param>
private UmbracoBackOfficeIdentity(ClaimsIdentity identity)
: base(identity.Claims, Constants.Security.BackOfficeAuthenticationType)
{
Actor = identity;
}
/// <summary>
@@ -30,22 +48,20 @@ namespace Umbraco.Core.BackOffice
/// <param name="startContentNodes"></param>
/// <param name="startMediaNodes"></param>
/// <param name="culture"></param>
/// <param name="sessionId"></param>
/// <param name="securityStamp"></param>
/// <param name="allowedApps"></param>
/// <param name="roles"></param>
public UmbracoBackOfficeIdentity(int userId, string username, string realName,
IEnumerable<int> startContentNodes, IEnumerable<int> startMediaNodes, string culture,
string sessionId, string securityStamp, IEnumerable<string> allowedApps, IEnumerable<string> roles)
string securityStamp, IEnumerable<string> allowedApps, IEnumerable<string> roles)
: base(Enumerable.Empty<Claim>(), Constants.Security.BackOfficeAuthenticationType) //this ctor is used to ensure the IsAuthenticated property is true
{
if (allowedApps == null) throw new ArgumentNullException(nameof(allowedApps));
if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username));
if (string.IsNullOrWhiteSpace(realName)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(realName));
if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture));
if (string.IsNullOrWhiteSpace(sessionId)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(sessionId));
if (string.IsNullOrWhiteSpace(securityStamp)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(securityStamp));
AddRequiredClaims(userId, username, realName, startContentNodes, startMediaNodes, culture, sessionId, securityStamp, allowedApps, roles);
AddRequiredClaims(userId, username, realName, startContentNodes, startMediaNodes, culture, securityStamp, allowedApps, roles);
}
/// <summary>
@@ -60,43 +76,21 @@ namespace Umbraco.Core.BackOffice
/// <param name="startContentNodes"></param>
/// <param name="startMediaNodes"></param>
/// <param name="culture"></param>
/// <param name="sessionId"></param>
/// <param name="securityStamp"></param>
/// <param name="allowedApps"></param>
/// <param name="roles"></param>
public UmbracoBackOfficeIdentity(ClaimsIdentity childIdentity,
int userId, string username, string realName,
IEnumerable<int> startContentNodes, IEnumerable<int> startMediaNodes, string culture,
string sessionId, string securityStamp, IEnumerable<string> allowedApps, IEnumerable<string> roles)
string securityStamp, IEnumerable<string> allowedApps, IEnumerable<string> roles)
: base(childIdentity.Claims, Constants.Security.BackOfficeAuthenticationType)
{
if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username));
if (string.IsNullOrWhiteSpace(realName)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(realName));
if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture));
if (string.IsNullOrWhiteSpace(sessionId)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(sessionId));
if (string.IsNullOrWhiteSpace(securityStamp)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(securityStamp));
Actor = childIdentity;
AddRequiredClaims(userId, username, realName, startContentNodes, startMediaNodes, culture, sessionId, securityStamp, allowedApps, roles);
}
/// <summary>
/// Create a back office identity based on an existing claims identity
/// </summary>
/// <param name="identity"></param>
private UmbracoBackOfficeIdentity(ClaimsIdentity identity)
: base(identity.Claims, Constants.Security.BackOfficeAuthenticationType)
{
Actor = identity;
//validate that all claims exist
foreach (var t in RequiredBackOfficeIdentityClaimTypes)
{
//if the identity doesn't have the claim, or the claim value is null
if (identity.HasClaim(x => x.Type == t) == false || identity.HasClaim(x => x.Type == t && x.Value.IsNullOrWhiteSpace()))
{
throw new InvalidOperationException("Cannot create a " + typeof(UmbracoBackOfficeIdentity) + " from " + typeof(ClaimsIdentity) + " since the required claim " + t + " is missing");
}
}
AddRequiredClaims(userId, username, realName, startContentNodes, startMediaNodes, culture, securityStamp, allowedApps, roles);
}
public const string Issuer = Constants.Security.BackOfficeAuthenticationType;
@@ -115,8 +109,7 @@ namespace Umbraco.Core.BackOffice
Constants.Security.StartContentNodeIdClaimType,
Constants.Security.StartMediaNodeIdClaimType,
ClaimTypes.Locality,
Constants.Security.SessionIdClaimType,
Constants.Web.SecurityStampClaimType
Constants.Security.SecurityStampClaimType
};
/// <summary>
@@ -124,7 +117,7 @@ namespace Umbraco.Core.BackOffice
/// </summary>
private void AddRequiredClaims(int userId, string username, string realName,
IEnumerable<int> startContentNodes, IEnumerable<int> startMediaNodes, string culture,
string sessionId, string securityStamp, IEnumerable<string> allowedApps, IEnumerable<string> roles)
string securityStamp, IEnumerable<string> allowedApps, IEnumerable<string> roles)
{
//This is the id that 'identity' uses to check for the user id
if (HasClaim(x => x.Type == ClaimTypes.NameIdentifier) == false)
@@ -155,13 +148,9 @@ namespace Umbraco.Core.BackOffice
if (HasClaim(x => x.Type == ClaimTypes.Locality) == false)
AddClaim(new Claim(ClaimTypes.Locality, culture, ClaimValueTypes.String, Issuer, Issuer, this));
if (HasClaim(x => x.Type == Constants.Security.SessionIdClaimType) == false && SessionId.IsNullOrWhiteSpace() == false)
AddClaim(new Claim(Constants.Security.SessionIdClaimType, sessionId, ClaimValueTypes.String, Issuer, Issuer, this));
//The security stamp claim is also required... this is because this claim type is hard coded
// by the SecurityStampValidator, see: https://katanaproject.codeplex.com/workitem/444
if (HasClaim(x => x.Type == Constants.Web.SecurityStampClaimType) == false)
AddClaim(new Claim(Constants.Web.SecurityStampClaimType, securityStamp, ClaimValueTypes.String, Issuer, Issuer, this));
//The security stamp claim is also required
if (HasClaim(x => x.Type == Constants.Security.SecurityStampClaimType) == false)
AddClaim(new Claim(Constants.Security.SecurityStampClaimType, securityStamp, ClaimValueTypes.String, Issuer, Issuer, this));
//Add each app as a separate claim
if (HasClaim(x => x.Type == Constants.Security.AllowedApplicationsClaimType) == false && allowedApps != null)
@@ -211,19 +200,7 @@ namespace Umbraco.Core.BackOffice
public string Culture => this.FindFirstValue(ClaimTypes.Locality);
public string SessionId
{
get => this.FindFirstValue(Constants.Security.SessionIdClaimType);
set
{
var existing = FindFirst(Constants.Security.SessionIdClaimType);
if (existing != null)
TryRemoveClaim(existing);
AddClaim(new Claim(Constants.Security.SessionIdClaimType, value, ClaimValueTypes.String, Issuer, Issuer, this));
}
}
public string SecurityStamp => this.FindFirstValue(Constants.Web.SecurityStampClaimType);
public string SecurityStamp => this.FindFirstValue(Constants.Security.SecurityStampClaimType);
public string[] Roles => this.FindAll(x => x.Type == DefaultRoleClaimType).Select(role => role.Value).ToArray();

View File

@@ -52,6 +52,12 @@
public const string StartMediaNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startmedianode";
public const string AllowedApplicationsClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/allowedapp";
public const string SessionIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/sessionid";
public const string TicketExpiresClaimType = "http://umbraco.org/2020/06/identity/claims/backoffice/ticketexpires";
/// <summary>
/// The claim type for the ASP.NET Identity security stamp
/// </summary>
public const string SecurityStampClaimType = "AspNet.Identity.SecurityStamp";
public const string AspNetCoreV3PasswordHashAlgorithmName = "PBKDF2.ASPNETCORE.V3";
public const string AspNetCoreV2PasswordHashAlgorithmName = "PBKDF2.ASPNETCORE.V2";

View File

@@ -40,11 +40,6 @@
/// </summary>
public const string NoContentRouteName = "umbraco-no-content";
/// <summary>
/// The claim type for the ASP.NET Identity security stamp
/// </summary>
public const string SecurityStampClaimType = "AspNet.Identity.SecurityStamp";
/// <summary>
/// The default authentication type used for remembering that 2FA is not needed on next login
/// </summary>

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

@@ -28,8 +28,4 @@
<_Parameter1>Umbraco.Tests.Benchmarks</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
<Compile Remove="Security\AuthenticationExtensions.cs" />
</ItemGroup>
</Project>

View File

@@ -4,16 +4,12 @@ using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Umbraco.Core.BackOffice;
namespace Umbraco.Core.BackOffice
{
public class BackOfficeClaimsPrincipalFactory<TUser> : UserClaimsPrincipalFactory<TUser>
where TUser : BackOfficeIdentityUser
{
private const string _identityProviderClaimType = "http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider";
private const string _identityProviderClaimValue = "ASP.NET Identity";
public BackOfficeClaimsPrincipalFactory(UserManager<TUser> userManager, IOptions<BackOfficeIdentityOptions> optionsAccessor)
: base(userManager, optionsAccessor)
{
@@ -25,9 +21,6 @@ namespace Umbraco.Core.BackOffice
var baseIdentity = await base.GenerateClaimsAsync(user);
// Required by ASP.NET 4.x anti-forgery implementation
baseIdentity.AddClaim(new Claim(_identityProviderClaimType, _identityProviderClaimValue));
var umbracoIdentity = new UmbracoBackOfficeIdentity(
baseIdentity,
user.Id,
@@ -36,13 +29,24 @@ namespace Umbraco.Core.BackOffice
user.CalculatedContentStartNodeIds,
user.CalculatedMediaStartNodeIds,
user.Culture,
//NOTE - there is no session id assigned here, this is just creating the identity, a session id will be generated when the cookie is written
Guid.NewGuid().ToString(),
user.SecurityStamp,
user.AllowedSections,
user.Roles.Select(x => x.RoleId).ToArray());
return new ClaimsPrincipal(umbracoIdentity);
}
protected override async Task<ClaimsIdentity> GenerateClaimsAsync(TUser user)
{
// TODO: Have a look at the base implementation https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/UserClaimsPrincipalFactory.cs#L79
// since it's setting an authentication type that is probably not what we want.
// also, this is the method that we should be returning our UmbracoBackOfficeIdentity from , not the method above,
// the method above just returns a principal that wraps the identity and we dont use a custom principal,
// see https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/UserClaimsPrincipalFactory.cs#L66
var identity = await base.GenerateClaimsAsync(user);
return identity;
}
}
}

View File

@@ -53,10 +53,11 @@ namespace Umbraco.Core.BackOffice
}
#region What we do not currently support
// TODO: We could support this - but a user claims will mostly just be what is in the auth cookie
// We don't support an IUserClaimStore and don't need to (at least currently)
public override bool SupportsUserClaim => false;
// TODO: Support this
// It would be nice to support this but we don't need to currently and that would require IQueryable support for our user service/repository
public override bool SupportsQueryableUsers => false;
/// <summary>
@@ -64,8 +65,9 @@ namespace Umbraco.Core.BackOffice
/// </summary>
public override bool SupportsUserTwoFactor => false;
// TODO: Support this
// We haven't needed to support this yet, though might be necessary for 2FA
public override bool SupportsUserPhoneNumber => false;
#endregion
/// <summary>

View File

@@ -24,10 +24,10 @@ namespace Umbraco.Web.Composing.CompositionExtensions
// composition.Register<StarterKitInstallStep>(Lifetime.Scope);
// composition.Register<StarterKitCleanupStep>(Lifetime.Scope);
composition.Register<SetUmbracoVersionStep>(Lifetime.Scope);
composition.Register<CompleteInstallStep>(Lifetime.Scope);
composition.Register<InstallStepCollection>();
composition.Register<InstallHelper>();
composition.RegisterUnique<InstallHelper>();
return composition;
}

View File

@@ -21,7 +21,6 @@ namespace Umbraco.Web.Install
private static HttpClient _httpClient;
private readonly DatabaseBuilder _databaseBuilder;
private readonly ILogger _logger;
private readonly IGlobalSettings _globalSettings;
private readonly IUmbracoVersion _umbracoVersion;
private readonly IConnectionStrings _connectionStrings;
private readonly IInstallationService _installationService;
@@ -33,7 +32,6 @@ namespace Umbraco.Web.Install
public InstallHelper(DatabaseBuilder databaseBuilder,
ILogger logger,
IGlobalSettings globalSettings,
IUmbracoVersion umbracoVersion,
IConnectionStrings connectionStrings,
IInstallationService installationService,
@@ -43,7 +41,6 @@ namespace Umbraco.Web.Install
IJsonSerializer jsonSerializer)
{
_logger = logger;
_globalSettings = globalSettings;
_umbracoVersion = umbracoVersion;
_databaseBuilder = databaseBuilder;
_connectionStrings = connectionStrings ?? throw new ArgumentNullException(nameof(connectionStrings));
@@ -52,6 +49,9 @@ namespace Umbraco.Web.Install
_userAgentProvider = userAgentProvider;
_umbracoDatabaseFactory = umbracoDatabaseFactory;
_jsonSerializer = jsonSerializer;
//We need to initialize the type already, as we can't detect later, if the connection string is added on the fly.
GetInstallationType();
}
public InstallationType GetInstallationType()
@@ -59,7 +59,7 @@ namespace Umbraco.Web.Install
return _installationType ?? (_installationType = IsBrandNewInstall ? InstallationType.NewInstall : InstallationType.Upgrade).Value;
}
public async Task InstallStatus(bool isCompleted, string errorMsg)
public async Task SetInstallStatusAsync(bool isCompleted, string errorMsg)
{
try
{

View File

@@ -30,7 +30,7 @@ namespace Umbraco.Web.Install
// a.OfType<StarterKitInstallStep>().First(),
// a.OfType<StarterKitCleanupStep>().First(),
a.OfType<SetUmbracoVersionStep>().First(),
a.OfType<CompleteInstallStep>().First(),
};
}

View File

@@ -0,0 +1,31 @@
using System.Threading.Tasks;
using Umbraco.Web.Install.Models;
namespace Umbraco.Web.Install.InstallSteps
{
[InstallSetupStep(InstallationType.NewInstall | InstallationType.Upgrade,
"UmbracoVersion", 50, "Installation is complete! Get ready to be redirected to your new CMS.",
PerformsAppRestart = true)]
public class CompleteInstallStep : InstallSetupStep<object>
{
private readonly InstallHelper _installHelper;
public CompleteInstallStep(InstallHelper installHelper)
{
_installHelper = installHelper;
}
public override async Task<InstallSetupResult> ExecuteAsync(object model)
{
//reports the ended install
await _installHelper.SetInstallStatusAsync(true, "");
return null;
}
public override bool RequiresExecution(object model)
{
return true;
}
}
}

View File

@@ -1,67 +0,0 @@
using System.Threading.Tasks;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Net;
using Umbraco.Web.Install.Models;
namespace Umbraco.Web.Install.InstallSteps
{
[InstallSetupStep(InstallationType.NewInstall | InstallationType.Upgrade,
"UmbracoVersion", 50, "Installation is complete! Get ready to be redirected to your new CMS.",
PerformsAppRestart = true)]
public class SetUmbracoVersionStep : InstallSetupStep<object>
{
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly InstallHelper _installHelper;
private readonly IGlobalSettings _globalSettings;
private readonly IUmbracoVersion _umbracoVersion;
public SetUmbracoVersionStep(IUmbracoContextAccessor umbracoContextAccessor, InstallHelper installHelper,
IGlobalSettings globalSettings, IUmbracoVersion umbracoVersion)
{
_umbracoContextAccessor = umbracoContextAccessor;
_installHelper = installHelper;
_globalSettings = globalSettings;
_umbracoVersion = umbracoVersion;
}
public override Task<InstallSetupResult> ExecuteAsync(object model)
{
//TODO: This needs to be reintroduced, when users are compatible with ASP.NET Core Identity.
// var security = _umbracoContextAccessor.GetRequiredUmbracoContext().Security;
// if (security.IsAuthenticated() == false && _globalSettings.ConfigurationStatus.IsNullOrWhiteSpace())
// {
// security.PerformLogin(-1);
// }
//
// if (security.IsAuthenticated())
// {
// // when a user is already logged in, we need to check whether it's user 'zero'
// // which is the legacy super user from v7 - and then we need to actually log the
// // true super user in - but before that we need to log off, else audit events
// // will try to reference user zero and fail
// var userIdAttempt = security.GetUserId();
// if (userIdAttempt && userIdAttempt.Result == 0)
// {
// security.ClearCurrentLogin();
// security.PerformLogin(Constants.Security.SuperUserId);
// }
// }
// else if (_globalSettings.ConfigurationStatus.IsNullOrWhiteSpace())
// {
// // for installs, we need to log the super user in
// security.PerformLogin(Constants.Security.SuperUserId);
// }
//reports the ended install
_installHelper.InstallStatus(true, "");
return Task.FromResult<InstallSetupResult>(null);
}
public override bool RequiresExecution(object model)
{
return true;
}
}
}

View File

@@ -51,7 +51,6 @@
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Remove="Install\InstallSteps\CompleteInstallStep.cs" />
</ItemGroup>
<ItemGroup>

View File

@@ -17,6 +17,11 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice
[TestFixture]
public class BackOfficeClaimsPrincipalFactoryTests
{
private const int _testUserId = 2;
private const string _testUserName = "bob";
private const string _testUserGivenName = "Bob";
private const string _testUserCulture = "en-US";
private const string _testUserSecurityStamp = "B6937738-9C17-4C7D-A25A-628A875F5177";
private BackOfficeIdentityUser _testUser;
private Mock<UserManager<BackOfficeIdentityUser>> _mockUserManager;
@@ -65,52 +70,22 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice
Assert.IsNotNull(umbracoBackOfficeIdentity);
}
[Test]
public async Task CreateAsync_Should_Create_NameId()
[TestCase(ClaimTypes.NameIdentifier, _testUserId)]
[TestCase(ClaimTypes.Name, _testUserName)]
public async Task CreateAsync_Should_Include_Claim(string expectedClaimType, object expectedClaimValue)
{
const string expectedClaimType = ClaimTypes.NameIdentifier;
var expectedClaimValue = _testUser.Id.ToString();
var sut = CreateSut();
var claimsPrincipal = await sut.CreateAsync(_testUser);
Assert.True(claimsPrincipal.HasClaim(expectedClaimType, expectedClaimValue));
Assert.True(claimsPrincipal.GetUmbracoIdentity().Actor.HasClaim(expectedClaimType, expectedClaimValue));
}
[Test]
public async Task CreateAsync_Should_Create_Name()
{
const string expectedClaimType = ClaimTypes.Name;
var expectedClaimValue = _testUser.UserName;
var sut = CreateSut();
var claimsPrincipal = await sut.CreateAsync(_testUser);
Assert.True(claimsPrincipal.HasClaim(expectedClaimType, expectedClaimValue));
Assert.True(claimsPrincipal.GetUmbracoIdentity().Actor.HasClaim(expectedClaimType, expectedClaimValue));
}
[Test]
public async Task CreateAsync_Should_Create_IdentityProvider()
{
const string expectedClaimType = "http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider";
const string expectedClaimValue = "ASP.NET Identity";
var sut = CreateSut();
var claimsPrincipal = await sut.CreateAsync(_testUser);
Assert.True(claimsPrincipal.HasClaim(expectedClaimType, expectedClaimValue));
Assert.True(claimsPrincipal.GetUmbracoIdentity().Actor.HasClaim(expectedClaimType, expectedClaimValue));
Assert.True(claimsPrincipal.HasClaim(expectedClaimType, expectedClaimValue.ToString()));
Assert.True(claimsPrincipal.GetUmbracoIdentity().Actor.HasClaim(expectedClaimType, expectedClaimValue.ToString()));
}
[Test]
public async Task CreateAsync_When_SecurityStamp_Supported_Expect_SecurityStamp_Claim()
{
const string expectedClaimType = Constants.Web.SecurityStampClaimType;
const string expectedClaimType = Constants.Security.SecurityStampClaimType;
var expectedClaimValue = _testUser.SecurityStamp;
_mockUserManager.Setup(x => x.SupportsUserSecurityStamp).Returns(true);
@@ -165,12 +140,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice
var mockGlobalSettings = new Mock<IGlobalSettings>();
mockGlobalSettings.Setup(x => x.DefaultUILanguage).Returns("test");
_testUser = new BackOfficeIdentityUser(mockGlobalSettings.Object, 2, new List<IReadOnlyUserGroup>())
_testUser = new BackOfficeIdentityUser(mockGlobalSettings.Object, _testUserId, new List<IReadOnlyUserGroup>())
{
UserName = "bob",
Name = "Bob",
UserName = _testUserName,
Name = _testUserGivenName,
Email = "bob@umbraco.test",
SecurityStamp = "B6937738-9C17-4C7D-A25A-628A875F5177"
SecurityStamp = _testUserSecurityStamp,
Culture = _testUserCulture
};
_mockUserManager = new Mock<UserManager<BackOfficeIdentityUser>>(new Mock<IUserStore<BackOfficeIdentityUser>>().Object,

View File

@@ -16,7 +16,6 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice
[Test]
public void Create_From_Claims_Identity()
{
var sessionId = Guid.NewGuid().ToString();
var securityStamp = Guid.NewGuid().ToString();
var claimsIdentity = new ClaimsIdentity(new[]
{
@@ -31,15 +30,15 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice
new Claim(Constants.Security.AllowedApplicationsClaimType, "content", ClaimValueTypes.String, TestIssuer, TestIssuer),
new Claim(Constants.Security.AllowedApplicationsClaimType, "media", ClaimValueTypes.String, TestIssuer, TestIssuer),
new Claim(ClaimTypes.Locality, "en-us", ClaimValueTypes.String, TestIssuer, TestIssuer),
new Claim(Constants.Security.SessionIdClaimType, sessionId, Constants.Security.SessionIdClaimType, TestIssuer, TestIssuer),
new Claim(ClaimsIdentity.DefaultRoleClaimType, "admin", ClaimValueTypes.String, TestIssuer, TestIssuer),
new Claim(Constants.Web.SecurityStampClaimType, securityStamp, ClaimValueTypes.String, TestIssuer, TestIssuer),
new Claim(Constants.Security.SecurityStampClaimType, securityStamp, ClaimValueTypes.String, TestIssuer, TestIssuer),
});
var backofficeIdentity = UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity);
if (!UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity, out var backofficeIdentity))
Assert.Fail();
Assert.AreEqual(1234, backofficeIdentity.Id);
Assert.AreEqual(sessionId, backofficeIdentity.SessionId);
//Assert.AreEqual(sessionId, backofficeIdentity.SessionId);
Assert.AreEqual(securityStamp, backofficeIdentity.SecurityStamp);
Assert.AreEqual("testing", backofficeIdentity.Username);
Assert.AreEqual("hello world", backofficeIdentity.RealName);
@@ -49,7 +48,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice
Assert.AreEqual("en-us", backofficeIdentity.Culture);
Assert.IsTrue(new[] { "admin" }.SequenceEqual(backofficeIdentity.Roles));
Assert.AreEqual(12, backofficeIdentity.Claims.Count());
Assert.AreEqual(11, backofficeIdentity.Claims.Count());
}
[Test]
@@ -61,13 +60,15 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice
new Claim(ClaimTypes.Name, "testing", ClaimValueTypes.String, TestIssuer, TestIssuer),
});
Assert.Throws<InvalidOperationException>(() => UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity));
if (UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity, out var backofficeIdentity))
Assert.Fail();
Assert.Pass();
}
[Test]
public void Create_From_Claims_Identity_Required_Claim_Null()
{
var sessionId = Guid.NewGuid().ToString();
var claimsIdentity = new ClaimsIdentity(new[]
{
//null or empty
@@ -79,18 +80,20 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice
new Claim(Constants.Security.AllowedApplicationsClaimType, "content", ClaimValueTypes.String, TestIssuer, TestIssuer),
new Claim(Constants.Security.AllowedApplicationsClaimType, "media", ClaimValueTypes.String, TestIssuer, TestIssuer),
new Claim(ClaimTypes.Locality, "en-us", ClaimValueTypes.String, TestIssuer, TestIssuer),
new Claim(Constants.Security.SessionIdClaimType, sessionId, Constants.Security.SessionIdClaimType, TestIssuer, TestIssuer),
new Claim(ClaimsIdentity.DefaultRoleClaimType, "admin", ClaimValueTypes.String, TestIssuer, TestIssuer),
});
Assert.Throws<InvalidOperationException>(() => UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity));
if (UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity, out var backofficeIdentity))
Assert.Fail();
Assert.Pass();
}
[Test]
public void Create_With_Claims_And_User_Data()
{
var sessionId = Guid.NewGuid().ToString();
var securityStamp = Guid.NewGuid().ToString();
var claimsIdentity = new ClaimsIdentity(new[]
{
@@ -99,7 +102,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice
});
var identity = new UmbracoBackOfficeIdentity(claimsIdentity,
1234, "testing", "hello world", new[] { 654 }, new[] { 654 }, "en-us", sessionId, sessionId, new[] { "content", "media" }, new[] { "admin" });
1234, "testing", "hello world", new[] { 654 }, new[] { 654 }, "en-us", securityStamp, new[] { "content", "media" }, new[] { "admin" });
Assert.AreEqual(12, identity.Claims.Count());
}
@@ -108,10 +111,10 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice
[Test]
public void Clone()
{
var sessionId = Guid.NewGuid().ToString();
var securityStamp = Guid.NewGuid().ToString();
var identity = new UmbracoBackOfficeIdentity(
1234, "testing", "hello world", new[] { 654 }, new[] { 654 }, "en-us", sessionId, sessionId, new[] { "content", "media" }, new[] { "admin" });
1234, "testing", "hello world", new[] { 654 }, new[] { 654 }, "en-us", securityStamp, new[] { "content", "media" }, new[] { "admin" });
var cloned = identity.Clone();

View File

@@ -0,0 +1,43 @@
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using Umbraco.Extensions;
using Umbraco.Core;
using Umbraco.Core.BackOffice;
namespace Umbraco.Tests.UnitTests.Umbraco.Core.Extensions
{
[TestFixture]
public class ClaimsPrincipalExtensionsTests
{
[Test]
public void Get_Remaining_Ticket_Seconds()
{
var backOfficeIdentity = new UmbracoBackOfficeIdentity(-1, "test", "test",
Enumerable.Empty<int>(), Enumerable.Empty<int>(), "en-US", Guid.NewGuid().ToString(),
Enumerable.Empty<string>(), Enumerable.Empty<string>());
var principal = new ClaimsPrincipal(backOfficeIdentity);
var expireSeconds = 99;
var elapsedSeconds = 3;
var remainingSeconds = expireSeconds - elapsedSeconds;
var now = DateTimeOffset.Now;
var then = now.AddSeconds(elapsedSeconds);
var expires = now.AddSeconds(expireSeconds).ToString("o");
backOfficeIdentity.AddClaim(new Claim(
Constants.Security.TicketExpiresClaimType,
expires,
ClaimValueTypes.DateTime,
UmbracoBackOfficeIdentity.Issuer,
UmbracoBackOfficeIdentity.Issuer,
backOfficeIdentity));
var ticketRemainingSeconds = principal.GetRemainingAuthSeconds(then);
Assert.AreEqual(remainingSeconds, ticketRemainingSeconds);
}
}
}

View File

@@ -26,7 +26,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Security
var httpContext = new DefaultHttpContext()
{
User = new ClaimsPrincipal(new UmbracoBackOfficeIdentity(-1, "test", "test", Enumerable.Empty<int>(), Enumerable.Empty<int>(), "en-US",
Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Enumerable.Empty<string>(), Enumerable.Empty<string>()))
Guid.NewGuid().ToString(), Enumerable.Empty<string>(), Enumerable.Empty<string>()))
};
httpContext.Request.IsHttps = true;
return httpContext;

View File

@@ -79,7 +79,7 @@ namespace Umbraco.Tests.Cache.PublishedCache
_umbracoContext = new UmbracoContext(
httpContextAccessor,
publishedSnapshotService.Object,
new WebSecurity(httpContextAccessor, Mock.Of<IUserService>(), globalSettings, HostingEnvironment),
Mock.Of<IWebSecurity>(),
globalSettings,
HostingEnvironment,
new TestVariationContextAccessor(),

View File

@@ -74,7 +74,7 @@ namespace Umbraco.Tests.PublishedContent
var umbracoContext = new UmbracoContext(
httpContextAccessor,
publishedSnapshotService.Object,
new WebSecurity(httpContextAccessor, ServiceContext.UserService, globalSettings, HostingEnvironment),
Mock.Of<IWebSecurity>(),
globalSettings,
HostingEnvironment,
new TestVariationContextAccessor(),

View File

@@ -123,7 +123,7 @@ namespace Umbraco.Tests.Scoping
var umbracoContext = new UmbracoContext(
httpContextAccessor,
service,
new WebSecurity(httpContextAccessor, ServiceContext.UserService, globalSettings, HostingEnvironment),
Mock.Of<IWebSecurity>(),
globalSettings,
HostingEnvironment,
new TestVariationContextAccessor(),

View File

@@ -34,7 +34,7 @@ namespace Umbraco.Tests.Security
var umbracoContext = new UmbracoContext(
httpContextAccessor,
Mock.Of<IPublishedSnapshotService>(),
new WebSecurity(httpContextAccessor, ServiceContext.UserService, globalSettings, HostingEnvironment),
Mock.Of<IWebSecurity>(),
globalSettings,
HostingEnvironment,
new TestVariationContextAccessor(),
@@ -58,7 +58,7 @@ namespace Umbraco.Tests.Security
var umbCtx = new UmbracoContext(
httpContextAccessor,
Mock.Of<IPublishedSnapshotService>(),
new WebSecurity(httpContextAccessor, ServiceContext.UserService, globalSettings, HostingEnvironment),
Mock.Of<IWebSecurity>(),
globalSettings,
HostingEnvironment,
new TestVariationContextAccessor(),

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

@@ -27,9 +27,9 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting
{
protected override Task<AuthenticationTicket> AuthenticateCoreAsync()
{
var sessionId = Guid.NewGuid().ToString();
var securityStamp = Guid.NewGuid().ToString();
var identity = new UmbracoBackOfficeIdentity(
-1, "admin", "Admin", new []{-1}, new[] { -1 }, "en-US", sessionId, sessionId, new[] { "content", "media", "members" }, new[] { "admin" });
-1, "admin", "Admin", new []{-1}, new[] { -1 }, "en-US", securityStamp, new[] { "content", "media", "members" }, new[] { "admin" });
return Task.FromResult(new AuthenticationTicket(identity,
new AuthenticationProperties()

View File

@@ -374,8 +374,7 @@ namespace Umbraco.Tests.TestHelpers
var umbracoContext = new UmbracoContext(
httpContextAccessor,
service,
new WebSecurity(httpContextAccessor, Factory.GetInstance<IUserService>(),
Factory.GetInstance<IGlobalSettings>(), HostingEnvironment),
Mock.Of<IWebSecurity>(),
globalSettings ?? Factory.GetInstance<IGlobalSettings>(),
HostingEnvironment,
new TestVariationContextAccessor(),

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

@@ -60,93 +60,94 @@ namespace Umbraco.Tests.Web.Controllers
}
[Test]
public async System.Threading.Tasks.Task GetCurrentUser_Fips()
{
ApiController CtrlFactory(HttpRequestMessage message, IUmbracoContextAccessor umbracoContextAccessor)
{
//setup some mocks
var userServiceMock = Mock.Get(ServiceContext.UserService);
userServiceMock.Setup(service => service.GetUserById(It.IsAny<int>()))
.Returns(() => null);
if (Thread.GetDomain().GetData(".appPath") != null)
{
HttpContext.Current = new HttpContext(new SimpleWorkerRequest("", "", new StringWriter()));
}
else
{
var baseDir = IOHelper.MapPath("").TrimEnd(Path.DirectorySeparatorChar);
HttpContext.Current = new HttpContext(new SimpleWorkerRequest("/", baseDir, "", "", new StringWriter()));
}
var usersController = new AuthenticationController(
new TestUserPasswordConfig(),
Factory.GetInstance<IGlobalSettings>(),
Factory.GetInstance<IHostingEnvironment>(),
umbracoContextAccessor,
Factory.GetInstance<ISqlContext>(),
Factory.GetInstance<ServiceContext>(),
Factory.GetInstance<AppCaches>(),
Factory.GetInstance<IProfilingLogger>(),
Factory.GetInstance<IRuntimeState>(),
Factory.GetInstance<UmbracoMapper>(),
Factory.GetInstance<ISecuritySettings>(),
Factory.GetInstance<IPublishedUrlProvider>(),
Factory.GetInstance<IRequestAccessor>(),
Factory.GetInstance<IEmailSender>()
);
return usersController;
}
Mock.Get(Current.SqlContext)
.Setup(x => x.Query<IUser>())
.Returns(new Query<IUser>(Current.SqlContext));
var syntax = new SqlCeSyntaxProvider();
Mock.Get(Current.SqlContext)
.Setup(x => x.SqlSyntax)
.Returns(syntax);
var mappers = new MapperCollection(new[]
{
new UserMapper(new Lazy<ISqlContext>(() => Current.SqlContext), new ConcurrentDictionary<Type, ConcurrentDictionary<string, string>>())
});
Mock.Get(Current.SqlContext)
.Setup(x => x.Mappers)
.Returns(mappers);
// Testing what happens if the system were configured to only use FIPS-compliant algorithms
var typ = typeof(CryptoConfig);
var flds = typ.GetFields(BindingFlags.Static | BindingFlags.NonPublic);
var haveFld = flds.FirstOrDefault(f => f.Name == "s_haveFipsAlgorithmPolicy");
var isFld = flds.FirstOrDefault(f => f.Name == "s_fipsAlgorithmPolicy");
var originalFipsValue = CryptoConfig.AllowOnlyFipsAlgorithms;
try
{
if (!originalFipsValue)
{
haveFld.SetValue(null, true);
isFld.SetValue(null, true);
}
var runner = new TestRunner(CtrlFactory);
var response = await runner.Execute("Authentication", "GetCurrentUser", HttpMethod.Get);
var obj = JsonConvert.DeserializeObject<UserDetail>(response.Item2);
Assert.AreEqual(-1, obj.UserId);
}
finally
{
if (!originalFipsValue)
{
haveFld.SetValue(null, false);
isFld.SetValue(null, false);
}
}
}
// TODO Reintroduce when moved to .NET Core
// [Test]
// public async System.Threading.Tasks.Task GetCurrentUser_Fips()
// {
// ApiController CtrlFactory(HttpRequestMessage message, IUmbracoContextAccessor umbracoContextAccessor)
// {
// //setup some mocks
// var userServiceMock = Mock.Get(ServiceContext.UserService);
// userServiceMock.Setup(service => service.GetUserById(It.IsAny<int>()))
// .Returns(() => null);
//
// if (Thread.GetDomain().GetData(".appPath") != null)
// {
// HttpContext.Current = new HttpContext(new SimpleWorkerRequest("", "", new StringWriter()));
// }
// else
// {
// var baseDir = IOHelper.MapPath("").TrimEnd(Path.DirectorySeparatorChar);
// HttpContext.Current = new HttpContext(new SimpleWorkerRequest("/", baseDir, "", "", new StringWriter()));
// }
//
// var usersController = new AuthenticationController(
// new TestUserPasswordConfig(),
// Factory.GetInstance<IGlobalSettings>(),
// Factory.GetInstance<IHostingEnvironment>(),
// umbracoContextAccessor,
// Factory.GetInstance<ISqlContext>(),
// Factory.GetInstance<ServiceContext>(),
// Factory.GetInstance<AppCaches>(),
// Factory.GetInstance<IProfilingLogger>(),
// Factory.GetInstance<IRuntimeState>(),
// Factory.GetInstance<UmbracoMapper>(),
// Factory.GetInstance<ISecuritySettings>(),
// Factory.GetInstance<IPublishedUrlProvider>(),
// Factory.GetInstance<IRequestAccessor>(),
// Factory.GetInstance<IEmailSender>()
// );
// return usersController;
// }
//
// Mock.Get(Current.SqlContext)
// .Setup(x => x.Query<IUser>())
// .Returns(new Query<IUser>(Current.SqlContext));
//
// var syntax = new SqlCeSyntaxProvider();
//
// Mock.Get(Current.SqlContext)
// .Setup(x => x.SqlSyntax)
// .Returns(syntax);
//
// var mappers = new MapperCollection(new[]
// {
// new UserMapper(new Lazy<ISqlContext>(() => Current.SqlContext), new ConcurrentDictionary<Type, ConcurrentDictionary<string, string>>())
// });
//
// Mock.Get(Current.SqlContext)
// .Setup(x => x.Mappers)
// .Returns(mappers);
//
// // Testing what happens if the system were configured to only use FIPS-compliant algorithms
// var typ = typeof(CryptoConfig);
// var flds = typ.GetFields(BindingFlags.Static | BindingFlags.NonPublic);
// var haveFld = flds.FirstOrDefault(f => f.Name == "s_haveFipsAlgorithmPolicy");
// var isFld = flds.FirstOrDefault(f => f.Name == "s_fipsAlgorithmPolicy");
// var originalFipsValue = CryptoConfig.AllowOnlyFipsAlgorithms;
//
// try
// {
// if (!originalFipsValue)
// {
// haveFld.SetValue(null, true);
// isFld.SetValue(null, true);
// }
//
// var runner = new TestRunner(CtrlFactory);
// var response = await runner.Execute("Authentication", "GetCurrentUser", HttpMethod.Get);
//
// var obj = JsonConvert.DeserializeObject<UserDetail>(response.Item2);
// Assert.AreEqual(-1, obj.UserId);
// }
// finally
// {
// if (!originalFipsValue)
// {
// haveFld.SetValue(null, false);
// isFld.SetValue(null, false);
// }
// }
// }
}
}

View File

@@ -438,7 +438,7 @@ namespace Umbraco.Tests.Web.Mvc
var ctx = new UmbracoContext(
httpContextAccessor,
_service,
new WebSecurity(httpContextAccessor, ServiceContext.UserService, globalSettings, HostingEnvironment),
Mock.Of<IWebSecurity>(),
globalSettings,
HostingEnvironment,
new TestVariationContextAccessor(),

View File

@@ -32,7 +32,7 @@ namespace Umbraco.Tests.Web
var umbCtx = new UmbracoContext(
httpContextAccessor,
Mock.Of<IPublishedSnapshotService>(),
new WebSecurity(httpContextAccessor, ServiceContext.UserService, TestObjects.GetGlobalSettings(), HostingEnvironment),
Mock.Of<IWebSecurity>(),
TestObjects.GetGlobalSettings(),
HostingEnvironment,
new TestVariationContextAccessor(),
@@ -53,7 +53,7 @@ namespace Umbraco.Tests.Web
var umbCtx = new UmbracoContext(
httpContextAccessor,
Mock.Of<IPublishedSnapshotService>(),
new WebSecurity(httpContextAccessor, ServiceContext.UserService, TestObjects.GetGlobalSettings(), HostingEnvironment),
Mock.Of<IWebSecurity>(),
TestObjects.GetGlobalSettings(),
HostingEnvironment,
new TestVariationContextAccessor(),
@@ -84,7 +84,7 @@ namespace Umbraco.Tests.Web
var umbCtx = new UmbracoContext(
httpContextAccessor,
Mock.Of<IPublishedSnapshotService>(),
new WebSecurity(httpContextAccessor, ServiceContext.UserService, TestObjects.GetGlobalSettings(), HostingEnvironment),
Mock.Of<IWebSecurity>(),
TestObjects.GetGlobalSettings(),
HostingEnvironment,
new TestVariationContextAccessor(),

View File

@@ -8,11 +8,12 @@ using Umbraco.Core.Mapping;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Services;
using Umbraco.Extensions;
using Umbraco.Web.BackOffice.Security;
using Umbraco.Web.BackOffice.Filters;
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;
@@ -67,6 +68,29 @@ namespace Umbraco.Web.BackOffice.Controllers
return false;
}
/// <summary>
/// Returns the currently logged in Umbraco user
/// </summary>
/// <returns></returns>
/// <remarks>
/// We have the attribute [SetAngularAntiForgeryTokens] applied because this method is called initially to determine if the user
/// is valid before the login screen is displayed. The Auth cookie can be persisted for up to a day but the csrf cookies are only session
/// cookies which means that the auth cookie could be valid but the csrf cookies are no longer there, in that case we need to re-set the csrf cookies.
/// </remarks>
[UmbracoAuthorize]
[TypeFilter(typeof(SetAngularAntiForgeryTokens))]
//[CheckIfUserTicketDataIsStale] // TODO: Migrate this, though it will need to be done differently at the cookie auth level
public UserDetail GetCurrentUser()
{
var user = _webSecurity.CurrentUser;
var result = _umbracoMapper.Map<UserDetail>(user);
//set their remaining seconds
result.SecondsUntilTimeout = HttpContext.User.GetRemainingAuthSeconds();
return result;
}
/// <summary>
/// Logs a user in
/// </summary>

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;
@@ -179,7 +178,7 @@ namespace Umbraco.Web.BackOffice.Controllers
/// otherwise process the external login info.
/// </summary>
/// <returns></returns>
private async Task<IActionResult> RenderDefaultOrProcessExternalLoginAsync(
private Task<IActionResult> RenderDefaultOrProcessExternalLoginAsync(
Func<IActionResult> defaultResponse,
Func<IActionResult> externalSignInResponse)
{
@@ -191,9 +190,9 @@ namespace Umbraco.Web.BackOffice.Controllers
//check if there is the TempData with the any token name specified, if so, assign to view bag and render the view
if (ViewData.FromTempData(TempData, ViewDataExtensions.TokenExternalSignInError) ||
ViewData.FromTempData(TempData, ViewDataExtensions.TokenPasswordResetCode))
return defaultResponse();
return Task.FromResult(defaultResponse());
return defaultResponse();
return Task.FromResult(defaultResponse());
//First check if there's external login info, if there's not proceed as normal
// TODO: Review this, not sure if this will work as expected until we integrate OAuth

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

@@ -20,8 +20,6 @@ namespace Umbraco.Extensions
backOfficeRoutes.CreateRoutes(endpoints);
});
app.UseAuthentication();
app.UseUmbracoRuntimeMinification();
// Important we handle image manipulations before the static files, otherwise the querystring is just ignored.

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
{
@@ -29,8 +30,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>
@@ -51,9 +53,9 @@ 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,6 +1,6 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Umbraco.Web.Common.Extensions;
using Umbraco.Extensions;
namespace Umbraco.Web.BackOffice.Filters
{

View File

@@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Web.BackOffice.Security;
namespace Umbraco.Web.BackOffice.Filters
{
@@ -24,10 +25,10 @@ namespace Umbraco.Web.BackOffice.Filters
public sealed class ValidateAngularAntiForgeryTokenAttribute : ActionFilterAttribute
{
private readonly ILogger _logger;
private readonly IAntiforgery _antiforgery;
private readonly IBackOfficeAntiforgery _antiforgery;
private readonly ICookieManager _cookieManager;
public ValidateAngularAntiForgeryTokenAttribute(ILogger logger, IAntiforgery antiforgery, ICookieManager cookieManager)
public ValidateAngularAntiForgeryTokenAttribute(ILogger logger, IBackOfficeAntiforgery antiforgery, ICookieManager cookieManager)
{
_logger = logger;
_antiforgery = antiforgery;

View File

@@ -20,6 +20,8 @@ namespace Umbraco.Web.BackOffice.Runtime
composition.RegisterUnique<BackOfficeAreaRoutes>();
composition.RegisterUnique<BackOfficeServerVariables>();
composition.Register<BackOfficeSessionIdValidator>(Lifetime.Request);
composition.Register<BackOfficeSecurityStampValidator>(Lifetime.Request);
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));
@@ -60,18 +60,8 @@ namespace Umbraco.Web.BackOffice.Security
return null;
}
UmbracoBackOfficeIdentity identity;
try
{
identity = UmbracoBackOfficeIdentity.FromClaimsIdentity((ClaimsIdentity)decrypt.Principal.Identity);
}
catch (Exception)
{
//if it cannot be created return null, will be due to serialization errors in user data most likely due to corrupt cookies or cookies
//for previous versions of Umbraco
if (!UmbracoBackOfficeIdentity.FromClaimsIdentity((ClaimsIdentity)decrypt.Principal.Identity, out var identity))
return null;
}
//return the ticket with a UmbracoBackOfficeIdentity
var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), decrypt.Properties, decrypt.AuthenticationScheme);

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

@@ -1,69 +1,94 @@

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 System.Web;
using Microsoft.Owin;
using Microsoft.Owin.Infrastructure;
using Microsoft.Owin.Security.Cookies;
using Umbraco.Core;
using Umbraco.Core.BackOffice;
using Umbraco.Core.Configuration;
using Umbraco.Core.Hosting;
using Umbraco.Core.IO;
using Constants = Umbraco.Core.Constants;
using Umbraco.Extensions;
namespace Umbraco.Web.Security
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
/// Used to validate a cookie against a user's session id
/// </summary>
/// <remarks>
/// <para>
/// 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
/// </para>
/// <para>
/// This is a scoped/request based object.
/// </para>
/// </remarks>
internal static class SessionIdValidator
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 static async Task ValidateSessionAsync(TimeSpan validateInterval, CookieValidateIdentityContext context, IGlobalSettings globalSettings, IHostingEnvironment hostingEnvironment)
public BackOfficeSessionIdValidator(ISystemClock systemClock, IGlobalSettings globalSettings, IHostingEnvironment hostingEnvironment, BackOfficeUserManager userManager)
{
if (context.Request.Uri.IsBackOfficeRequest(globalSettings, hostingEnvironment) == false)
_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.OwinContext, context.Options.CookieManager, context.Options.SystemClock, context.Properties.IssuedUtc, context.Identity, globalSettings);
var valid = await ValidateSessionAsync(validateInterval, context.HttpContext, context.Options.CookieManager, _systemClock, context.Properties.IssuedUtc, context.Principal.Identity as ClaimsIdentity);
if (valid == false)
{
context.RejectIdentity();
context.OwinContext.Authentication.SignOut(context.Options.AuthenticationType);
context.RejectPrincipal();
await context.HttpContext.SignOutAsync(Constants.Security.BackOfficeAuthenticationType);
}
}
public static async Task<bool> ValidateSessionAsync(
private async Task<bool> ValidateSessionAsync(
TimeSpan validateInterval,
IOwinContext owinCtx,
Microsoft.Owin.Infrastructure.ICookieManager cookieManager,
HttpContext httpContext,
ICookieManager cookieManager,
ISystemClock systemClock,
DateTimeOffset? authTicketIssueDate,
ClaimsIdentity currentIdentity,
IGlobalSettings globalSettings)
ClaimsIdentity currentIdentity)
{
if (owinCtx == null) throw new ArgumentNullException("owinCtx");
if (cookieManager == null) throw new ArgumentNullException("cookieManager");
if (systemClock == null) throw new ArgumentNullException("systemClock");
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(owinCtx, CookieName);
var lastCheckedCookie = cookieManager.GetRequestCookie(httpContext, CookieName);
if (lastCheckedCookie.IsNullOrWhiteSpace() == false)
{
DateTimeOffset parsed;
if (DateTimeOffset.TryParse(lastCheckedCookie, out parsed))
if (DateTimeOffset.TryParse(lastCheckedCookie, out var parsed))
{
issuedUtc = parsed;
}
@@ -86,28 +111,24 @@ namespace Umbraco.Web.Security
if (validate == false)
return true;
var manager = owinCtx.Get<BackOfficeOwinUserManager>();
if (manager == null)
return false;
var userId = currentIdentity.GetUserId();
var user = await manager.FindByIdAsync(userId);
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
return false;
var sessionId = currentIdentity.FindFirstValue(Constants.Security.SessionIdClaimType);
if (await manager.ValidateSessionIdAsync(userId, sessionId) == false)
if (await _userManager.ValidateSessionIdAsync(userId, sessionId) == false)
return false;
//we will re-issue the cookie last checked cookie
cookieManager.AppendResponseCookie(
owinCtx,
httpContext,
CookieName,
DateTimeOffset.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffffffzzz"),
new CookieOptions
{
HttpOnly = true,
Secure = globalSettings.UseHttps || owinCtx.Request.IsSecure,
Secure = _globalSettings.UseHttps || httpContext.Request.IsHttps,
Path = "/"
});

View File

@@ -0,0 +1,222 @@
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.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Web.Common.Security;
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;
public ConfigureBackOfficeCookieOptions(
IUmbracoContextAccessor umbracoContextAccessor,
ISecuritySettings securitySettings,
IGlobalSettings globalSettings,
IHostingEnvironment hostingEnvironment,
IRuntimeState runtimeState,
IDataProtectionProvider dataProtection,
IRequestCache requestCache,
IUserService userService,
IIpResolver ipResolver,
BackOfficeSessionIdValidator sessionIdValidator)
{
_umbracoContextAccessor = umbracoContextAccessor;
_securitySettings = securitySettings;
_globalSettings = globalSettings;
_hostingEnvironment = hostingEnvironment;
_runtimeState = runtimeState;
_dataProtection = dataProtection;
_requestCache = requestCache;
_userService = userService;
_ipResolver = ipResolver;
_sessionIdValidator = sessionIdValidator;
}
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 = "/";
// For any redirections that may occur for the back office, they all go to the same path
var backOfficePath = _globalSettings.GetBackOfficePath(_hostingEnvironment);
options.AccessDeniedPath = backOfficePath;
options.LoginPath = backOfficePath;
options.LogoutPath = backOfficePath;
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
{
// IMPORTANT! If you set any of OnRedirectToLogin, OnRedirectToAccessDenied, OnRedirectToLogout, OnRedirectToReturnUrl
// you need to be aware that this will bypass the default behavior of returning the correct status codes for ajax requests and
// not redirecting for non-ajax requests. This is because the default behavior is baked into this class here:
// https://github.com/dotnet/aspnetcore/blob/master/src/Security/Authentication/Cookies/src/CookieAuthenticationEvents.cs#L58
// 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)
var securityStampValidator = ctx.HttpContext.RequestServices.GetRequiredService<BackOfficeSecurityStampValidator>();
// Same goes for the signinmanager
var signInManager = ctx.HttpContext.RequestServices.GetRequiredService<BackOfficeSignInManager>();
var backOfficeIdentity = ctx.Principal.GetUmbracoIdentity();
if (backOfficeIdentity == null)
{
ctx.RejectPrincipal();
await signInManager.SignOutAsync();
}
//ensure the thread culture is set
backOfficeIdentity.EnsureCulture();
await EnsureValidSessionId(ctx);
await securityStampValidator.ValidateAsync(ctx);
// add a claim to track when the cookie expires, we use this to track time remaining
backOfficeIdentity.AddClaim(new Claim(
Constants.Security.TicketExpiresClaimType,
ctx.Properties.ExpiresUtc.Value.ToString("o"),
ClaimValueTypes.DateTime,
UmbracoBackOfficeIdentity.Issuer,
UmbracoBackOfficeIdentity.Issuer,
backOfficeIdentity));
},
OnSigningIn = ctx =>
{
// occurs when sign in is successful but before the ticket is written to the outbound cookie
var backOfficeIdentity = ctx.Principal.GetUmbracoIdentity();
if (backOfficeIdentity != null)
{
//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();
//add our session claim
backOfficeIdentity.AddClaim(new Claim(Constants.Security.SessionIdClaimType, session.ToString(), ClaimValueTypes.String, UmbracoBackOfficeIdentity.Issuer, UmbracoBackOfficeIdentity.Issuer, backOfficeIdentity));
//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;
},
OnSignedIn = ctx =>
{
// occurs when sign in is successful and after the ticket is written to the outbound cookie
// When we are signed in with the cookie, assign the principal to the current HttpContext
ctx.HttpContext.User = ctx.Principal;
return Task.CompletedTask;
},
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;
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;
}
@@ -26,7 +26,7 @@ namespace Umbraco.Web.BackOffice.Security
options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier;
options.ClaimsIdentity.UserNameClaimType = ClaimTypes.Name;
options.ClaimsIdentity.RoleClaimType = ClaimTypes.Role;
options.ClaimsIdentity.SecurityStampClaimType = Constants.Web.SecurityStampClaimType;
options.ClaimsIdentity.SecurityStampClaimType = Constants.Security.SecurityStampClaimType;
options.Lockout.AllowedForNewUsers = true;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromDays(30);

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

@@ -6,7 +6,6 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Core;
using Umbraco.Web.BackOffice.Trees;
using Umbraco.Web.Common.Extensions;
using Umbraco.Web.WebApi;
namespace Umbraco.Extensions

View File

@@ -28,12 +28,4 @@
<ProjectReference Include="..\Umbraco.Web.Common\Umbraco.Web.Common.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Security\ConfigureBackOfficeSecurityStampValidatorOptions.cs" />
<Compile Remove="Security\ConfigureBackOfficeIdentityOptions.cs" />
<Compile Remove="Security\ConfigureBackOfficeCookieOptions.cs" />
<Compile Remove="Security\BackOfficeSessionIdValidator.cs" />
<Compile Remove="Security\BackOfficeSecureDataFormat.cs" />
</ItemGroup>
</Project>

View File

@@ -72,6 +72,11 @@ namespace Umbraco.Extensions
{
app.UseMiddleware<UmbracoRequestMiddleware>();
app.UseMiddleware<MiniProfilerMiddleware>();
// TODO: Both of these need to be done before any endpoints but after UmbracoRequestMiddleware
// because they rely on an UmbracoContext. But should they be here?
app.UseAuthentication();
app.UseAuthorization();
}
return app;

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

@@ -14,11 +14,13 @@ using Umbraco.Web.Common.Attributes;
using Umbraco.Web.Common.Exceptions;
using Umbraco.Web.Common.Filters;
using Umbraco.Web.Common.ModelBinding;
using Umbraco.Web.Common.Security;
using Umbraco.Web.Install;
using Umbraco.Web.Install.Models;
namespace Umbraco.Web.Common.Install
{
using Constants = Umbraco.Core.Constants;
[UmbracoApiController]
[TypeFilter(typeof(HttpResponseExceptionFilter))]
@@ -30,19 +32,21 @@ namespace Umbraco.Web.Common.Install
private readonly DatabaseBuilder _databaseBuilder;
private readonly InstallStatusTracker _installStatusTracker;
private readonly IUmbracoApplicationLifetime _umbracoApplicationLifetime;
private readonly BackOfficeSignInManager _backOfficeSignInManager;
private readonly InstallStepCollection _installSteps;
private readonly ILogger _logger;
private readonly IProfilingLogger _proflog;
public InstallApiController(DatabaseBuilder databaseBuilder, IProfilingLogger proflog,
InstallHelper installHelper, InstallStepCollection installSteps, InstallStatusTracker installStatusTracker,
IUmbracoApplicationLifetime umbracoApplicationLifetime)
IUmbracoApplicationLifetime umbracoApplicationLifetime, BackOfficeSignInManager backOfficeSignInManager)
{
_databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder));
_proflog = proflog ?? throw new ArgumentNullException(nameof(proflog));
_installSteps = installSteps;
_installStatusTracker = installStatusTracker;
_umbracoApplicationLifetime = umbracoApplicationLifetime;
_backOfficeSignInManager = backOfficeSignInManager;
InstallHelper = installHelper;
_logger = _proflog;
}
@@ -85,10 +89,17 @@ namespace Umbraco.Web.Common.Install
return starterKits;
}
[HttpPost]
public ActionResult CompleteInstall()
public async Task<ActionResult> CompleteInstall()
{
// log the super user in if it's a new install
var installType = InstallHelper.GetInstallationType();
if (installType == InstallationType.NewInstall)
{
var user = await _backOfficeSignInManager.UserManager.FindByIdAsync(Constants.Security.SuperUserId.ToString());
await _backOfficeSignInManager.SignInAsync(user, false);
}
_umbracoApplicationLifetime.Restart();
return NoContent();
}

View File

@@ -87,7 +87,7 @@ namespace Umbraco.Web.Common.Install
ViewData.SetUmbracoVersion(_umbracoVersion.SemanticVersion);
await _installHelper.InstallStatus(false, "");
await _installHelper.SetInstallStatusAsync(false, "");
return View();
}

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

@@ -15,6 +15,9 @@ namespace Umbraco.Web.Common.Middleware
/// <summary>
/// Manages Umbraco request objects and their lifetime
/// </summary>
/// <remarks>
/// This is responsible for creating and assigning an <see cref="IUmbracoContext"/>
/// </remarks>
public class UmbracoRequestMiddleware : IMiddleware
{
private readonly ILogger _logger;

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

@@ -10,9 +10,14 @@ using System.Security.Claims;
using System.Threading.Tasks;
using Umbraco.Core;
using Umbraco.Core.BackOffice;
using Umbraco.Extensions;
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;
@@ -132,21 +137,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

@@ -1,9 +1,6 @@
using System;
using System.Collections.Generic;
using System.Security;
using System.Text;
using Microsoft.AspNetCore.Http;
using Umbraco.Composing;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Hosting;
@@ -11,10 +8,10 @@ using Umbraco.Core.Models.Membership;
using Umbraco.Core.Services;
using Umbraco.Extensions;
using Umbraco.Web.Security;
using Umbraco.Core.Models;
namespace Umbraco.Web.Common.Security
{
// TODO: need to implement this
public class WebSecurity : IWebSecurity
{
@@ -23,7 +20,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;
@@ -33,10 +34,7 @@ namespace Umbraco.Web.Common.Security
private IUser _currentUser;
/// <summary>
/// Gets the current user.
/// </summary>
/// <value>The current user.</value>
/// <inheritdoc />
public IUser CurrentUser
{
get
@@ -52,6 +50,7 @@ namespace Umbraco.Web.Common.Security
}
}
/// <inheritdoc />
public ValidateRequestAttempt AuthorizeRequest(bool throwExceptions = false)
{
// check for secure connection
@@ -63,37 +62,33 @@ namespace Umbraco.Web.Common.Security
return ValidateCurrentUser(throwExceptions);
}
public void ClearCurrentLogin()
{
//throw new NotImplementedException();
}
/// <inheritdoc />
public Attempt<int> GetUserId()
{
return Attempt.Succeed(-1);
var identity = _httpContextAccessor.GetRequiredHttpContext().GetCurrentIdentity();
return identity == null ? Attempt.Fail<int>() : Attempt.Succeed(identity.Id);
}
/// <inheritdoc />
public bool IsAuthenticated()
{
var httpContext = _httpContextAccessor.HttpContext;
return httpContext?.User != null && httpContext.User.Identity.IsAuthenticated && httpContext.GetCurrentIdentity() != null;
}
public double PerformLogin(int userId)
{
return 100;
}
/// <inheritdoc />
public bool UserHasSectionAccess(string section, IUser user)
{
return true;
return user.HasSectionAccess(section);
}
/// <inheritdoc />
public bool ValidateCurrentUser()
{
return true;
return ValidateCurrentUser(false, true) == ValidateRequestAttempt.Success;
}
/// <inheritdoc />
public ValidateRequestAttempt ValidateCurrentUser(bool throwExceptions, bool requiresApproval = true)
{
//This will first check if the current user is already authenticated - which should be the case in nearly all circumstances

View File

@@ -33,7 +33,6 @@
</ItemGroup>
<ItemGroup>
<Compile Remove="Security\BackOfficeSignInManager.cs" />
<Compile Remove="Macros\UmbracoMacroResult.cs" />
</ItemGroup>

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

@@ -1,11 +1,11 @@
angular.module('umbraco.interceptors', [])
// We have to add the interceptor to the queue as a string because the interceptor depends upon service instances that are not available in the config block.
.config(['$httpProvider', function($httpProvider) {
.config(['$httpProvider', function ($httpProvider) {
$httpProvider.defaults.xsrfHeaderName = 'X-UMB-XSRF-TOKEN';
$httpProvider.defaults.xsrfCookieName = 'UMB-XSRF-TOKEN';
$httpProvider.interceptors.push('securityInterceptor');
$httpProvider.interceptors.push('debugRequestInterceptor');
$httpProvider.interceptors.push('requiredHeadersInterceptor');
$httpProvider.interceptors.push('doNotPostDollarVariablesOnPostRequestInterceptor');
$httpProvider.interceptors.push('cultureRequestInterceptor');

View File

@@ -1,25 +0,0 @@
(function() {
'use strict';
/**
* Used to set debug headers on all requests where necessary
* @param {any} $q
* @param {any} urlHelper
*/
function debugRequestInterceptor($q, urlHelper) {
return {
//dealing with requests:
'request': function(config) {
var queryStrings = urlHelper.getQueryStringParams();
if (queryStrings.umbDebug === "true" || queryStrings.umbdebug === "true") {
config.headers["X-UMB-DEBUG"] = "true";
}
return config;
}
};
}
angular.module('umbraco.interceptors').factory('debugRequestInterceptor', debugRequestInterceptor);
})();

View File

@@ -0,0 +1,31 @@
(function() {
'use strict';
/**
* Used to set required headers on all requests where necessary
* @param {any} $q
* @param {any} urlHelper
*/
function requiredHeadersInterceptor($q, urlHelper) {
return {
//dealing with requests:
'request': function (config) {
// This is a standard header that should be sent for all ajax requests and is required for
// how the server handles auth rejections, etc... see https://github.com/dotnet/aspnetcore/blob/a2568cbe1e8dd92d8a7976469100e564362f778e/src/Security/Authentication/Cookies/src/CookieAuthenticationEvents.cs#L106-L107
config.headers["X-Requested-With"] = "XMLHttpRequest";
// Set the debug header if in debug mode
var queryStrings = urlHelper.getQueryStringParams();
if (queryStrings.umbDebug === "true" || queryStrings.umbdebug === "true") {
config.headers["X-UMB-DEBUG"] = "true";
}
return config;
}
};
}
angular.module('umbraco.interceptors').factory('requiredHeadersInterceptor', requiredHeadersInterceptor);
})();

View File

@@ -8,6 +8,9 @@ app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService',
$.ajaxSetup({
beforeSend: function (xhr) {
xhr.setRequestHeader("X-UMB-XSRF-TOKEN", $cookies["UMB-XSRF-TOKEN"]);
// This is a standard header that should be sent for all ajax requests and is required for
// how the server handles auth rejections, etc... see https://github.com/dotnet/aspnetcore/blob/a2568cbe1e8dd92d8a7976469100e564362f778e/src/Security/Authentication/Cookies/src/CookieAuthenticationEvents.cs#L106-L107
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
var queryStrings = urlHelper.getQueryStringParams();
if (queryStrings.umbDebug === "true" || queryStrings.umbdebug === "true") {
xhr.setRequestHeader("X-UMB-DEBUG", "true");

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

@@ -157,32 +157,6 @@ namespace Umbraco.Web.Editors
}
/// <summary>
/// Returns the currently logged in Umbraco user
/// </summary>
/// <returns></returns>
/// <remarks>
/// We have the attribute [SetAngularAntiForgeryTokens] applied because this method is called initially to determine if the user
/// is valid before the login screen is displayed. The Auth cookie can be persisted for up to a day but the csrf cookies are only session
/// cookies which means that the auth cookie could be valid but the csrf cookies are no longer there, in that case we need to re-set the csrf cookies.
/// </remarks>
[WebApi.UmbracoAuthorize]
[SetAngularAntiForgeryTokens]
[CheckIfUserTicketDataIsStale]
public UserDetail GetCurrentUser()
{
var user = Security.CurrentUser;
var result = Mapper.Map<UserDetail>(user);
var httpContextAttempt = TryGetHttpContext();
if (httpContextAttempt.Success)
{
//set their remaining seconds
result.SecondsUntilTimeout = httpContextAttempt.Result.GetRemainingAuthSeconds();
}
return result;
}
/// <summary>
/// When a user is invited they are not approved but we need to resolve the partially logged on (non approved)
/// user.
@@ -440,20 +414,7 @@ namespace Umbraco.Web.Editors
// 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)
{
if (user == null) throw new ArgumentNullException("user");
if (principal == null) throw new ArgumentNullException(nameof(principal));
var userDetail = Mapper.Map<UserDetail>(user);
// update the userDetail and set their remaining seconds
userDetail.SecondsUntilTimeout = TimeSpan.FromMinutes(GlobalSettings.TimeOutInMinutes).TotalSeconds;
// create a response with the userDetail object
var response = Request.CreateResponse(HttpStatusCode.OK, userDetail);
// ensure the user is set for the current request
Request.SetPrincipalForRequest(principal);
return response;
throw new NotImplementedException();
}
private string ConstructCallbackUrl(int userId, string code)

View File

@@ -90,20 +90,6 @@ namespace Umbraco.Web.Editors
protected IAuthenticationManager AuthenticationManager => OwinContext.Authentication;
/// <summary>
/// Render the default view
/// </summary>
/// <returns></returns>
public async Task<ActionResult> Default()
{
return await RenderDefaultOrProcessExternalLoginAsync(
() =>
View(GlobalSettings.GetBackOfficePath(_hostingEnvironment).EnsureEndsWith('/') + "Views/Default.cshtml", new BackOfficeModel(_features, GlobalSettings, _umbracoVersion, _contentSettings, _hostingEnvironment, _runtimeSettings, _securitySettings)),
() =>
View(GlobalSettings.GetBackOfficePath(_hostingEnvironment).EnsureEndsWith('/') + "Views/Default.cshtml", new BackOfficeModel(_features, GlobalSettings, _umbracoVersion, _contentSettings, _hostingEnvironment, _runtimeSettings, _securitySettings))
);
}
[HttpGet]
public async Task<ActionResult> VerifyInvite(string invite)
{
@@ -184,28 +170,6 @@ namespace Umbraco.Web.Editors
}
/// <summary>
/// Returns the JavaScript object representing the static server variables javascript object
/// </summary>
/// <returns></returns>
[UmbracoAuthorize(Order = 0)]
[MinifyJavaScriptResult(Order = 1)]
public JavaScriptResult ServerVariables()
{
var serverVars = new BackOfficeServerVariables(Url, _runtimeState, _features, GlobalSettings, _umbracoVersion, _contentSettings, _treeCollection, _hostingEnvironment, _runtimeSettings, _securitySettings, _runtimeMinifier);
//cache the result if debugging is disabled
var result = _hostingEnvironment.IsDebugMode
? ServerVariablesParser.Parse(serverVars.GetServerVariables())
: AppCaches.RuntimeCache.GetCacheItem<string>(
typeof(BackOfficeController) + "ServerVariables",
() => ServerVariablesParser.Parse(serverVars.GetServerVariables()),
new TimeSpan(0, 10, 0));
return JavaScript(result);
}
// TODO: for converting to netcore, some examples:
// * https://github.com/dotnet/aspnetcore/blob/master/src/Identity/samples/IdentitySample.Mvc/Controllers/AccountController.cs
// * https://github.com/dotnet/aspnetcore/blob/master/src/MusicStore/samples/MusicStore/Controllers/AccountController.cs

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>
@@ -170,11 +126,7 @@ namespace Umbraco.Web.Security
return true;
}
/// <summary>
/// returns the number of seconds the user has until their auth session times out
/// </summary>
/// <param name="http"></param>
/// <returns></returns>
// NOTE: Migrated to netcore (though in a different way)
public static double GetRemainingAuthSeconds(this HttpContextBase http)
{
if (http == null) throw new ArgumentNullException(nameof(http));
@@ -182,11 +134,7 @@ namespace Umbraco.Web.Security
return ticket.GetRemainingAuthSeconds();
}
/// <summary>
/// returns the number of seconds the user has until their auth session times out
/// </summary>
/// <param name="ticket"></param>
/// <returns></returns>
// NOTE: Migrated to netcore (though in a different way)
public static double GetRemainingAuthSeconds(this AuthenticationTicket ticket)
{
var utcExpired = ticket?.Properties.ExpiresUtc;
@@ -218,52 +166,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 +215,7 @@ namespace Umbraco.Web.Security
catch (Exception)
{
// occurs when decryption fails
http.Logout(cookieName);
return null;
}
}
@@ -334,23 +236,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

@@ -93,7 +93,7 @@ namespace Umbraco.Web.Security
options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier;
options.ClaimsIdentity.UserNameClaimType = ClaimTypes.Name;
options.ClaimsIdentity.RoleClaimType = ClaimTypes.Role;
options.ClaimsIdentity.SecurityStampClaimType = Constants.Web.SecurityStampClaimType;
options.ClaimsIdentity.SecurityStampClaimType = Constants.Security.SecurityStampClaimType;
options.Lockout.AllowedForNewUsers = true;
options.Lockout.MaxFailedAccessAttempts = passwordConfiguration.MaxFailedAccessAttemptsBeforeLockout;

View File

@@ -110,8 +110,9 @@ namespace Umbraco.Web.Security
}
}
//We also need to re-validate the user's session if we are relying on this ping to keep their session alive
await SessionIdValidator.ValidateSessionAsync(TimeSpan.FromMinutes(1), context, _authOptions.CookieManager, _authOptions.SystemClock, issuedUtc, ticket.Identity, _globalSettings);
// NOTE: SessionIdValidator has been moved to netcore
////We also need to re-validate the user's session if we are relying on this ping to keep their session alive
//await SessionIdValidator.ValidateSessionAsync(TimeSpan.FromMinutes(1), context, _authOptions.CookieManager, _authOptions.SystemClock, issuedUtc, ticket.Identity, _globalSettings);
}
else if (remainingSeconds <= 30)
{

View File

@@ -1,4 +1,5 @@
using System.Security.Claims;
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Owin;
using Umbraco.Core;
@@ -59,7 +60,10 @@ namespace Umbraco.Web.Security
//Ok, we've got a real ticket, now we can add this ticket's identity to the current
// Principal, this means we'll have 2 identities assigned to the principal which we can
// use to authorize the preview and allow for a back office User.
claimsPrincipal.AddIdentity(UmbracoBackOfficeIdentity.FromClaimsIdentity(unprotected.Identity));
if (!UmbracoBackOfficeIdentity.FromClaimsIdentity(unprotected.Identity, out var umbracoIdentity))
throw new InvalidOperationException("Cannot convert identity");
claimsPrincipal.AddIdentity(umbracoIdentity);
}
}
}

View File

@@ -55,18 +55,8 @@ namespace Umbraco.Web.Security
return null;
}
UmbracoBackOfficeIdentity identity;
try
{
identity = UmbracoBackOfficeIdentity.FromClaimsIdentity(decrypt.Identity);
}
catch (Exception)
{
//if it cannot be created return null, will be due to serialization errors in user data most likely due to corrupt cookies or cookies
//for previous versions of Umbraco
if (!UmbracoBackOfficeIdentity.FromClaimsIdentity(decrypt.Identity, out var identity))
return null;
}
//return the ticket with a UmbracoBackOfficeIdentity
var ticket = new AuthenticationTicket(identity, decrypt.Properties);

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

@@ -13,186 +13,39 @@ using Umbraco.Core.Models;
namespace Umbraco.Web.Security
{
/// <summary>
/// A utility class used for dealing with USER security in Umbraco
/// </summary>
// NOTE: Moved to netcore
public class WebSecurity : IWebSecurity
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IUserService _userService;
private readonly IGlobalSettings _globalSettings;
private readonly IHostingEnvironment _hostingEnvironment;
public IUser CurrentUser => throw new NotImplementedException();
public WebSecurity(IHttpContextAccessor httpContextAccessor, IUserService userService, IGlobalSettings globalSettings, IHostingEnvironment hostingEnvironment)
{
_httpContextAccessor = httpContextAccessor;
_userService = userService;
_globalSettings = globalSettings;
_hostingEnvironment = hostingEnvironment;
}
private IUser _currentUser;
/// <summary>
/// Gets the current user.
/// </summary>
/// <value>The current user.</value>
public IUser CurrentUser
{
get
{
//only load it once per instance! (but make sure groups are loaded)
if (_currentUser == null)
{
var id = GetUserId();
_currentUser = id ? _userService.GetUserById(id.Result) : null;
}
return _currentUser;
}
}
private BackOfficeSignInManager _signInManager;
private BackOfficeSignInManager SignInManager
{
get
{
if (_signInManager == null)
{
var mgr = _httpContextAccessor.GetRequiredHttpContext().GetOwinContext().Get<BackOfficeSignInManager>();
if (mgr == null)
{
throw new NullReferenceException("Could not resolve an instance of " + typeof(BackOfficeSignInManager) + " from the " + typeof(IOwinContext));
}
_signInManager = mgr;
}
return _signInManager;
}
}
private BackOfficeOwinUserManager _userManager;
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);
return identity == null ? Attempt.Fail<int>() : Attempt.Succeed(Convert.ToInt32(identity.Id));
}
/// <summary>
/// Validates the currently logged in user and ensures they are not timed out
/// </summary>
/// <returns></returns>
public bool ValidateCurrentUser()
{
return ValidateCurrentUser(false, true) == ValidateRequestAttempt.Success;
}
/// <summary>
/// Validates the current user assigned to the request and ensures the stored user data is valid
/// </summary>
/// <param name="throwExceptions">set to true if you want exceptions to be thrown if failed</param>
/// <param name="requiresApproval">If true requires that the user is approved to be validated</param>
/// <returns></returns>
public ValidateRequestAttempt ValidateCurrentUser(bool throwExceptions, bool requiresApproval = true)
{
//This will first check if the current user is already authenticated - which should be the case in nearly all circumstances
// since the authentication happens in the Module, that authentication also checks the ticket expiry. We don't
// need to check it a second time because that requires another decryption phase and nothing can tamper with it during the request.
if (IsAuthenticated() == false)
{
//There is no user
if (throwExceptions) throw new InvalidOperationException("The user has no umbraco contextid - try logging in");
return ValidateRequestAttempt.FailedNoContextId;
}
var user = CurrentUser;
// Check for console access
if (user == null || (requiresApproval && user.IsApproved == false) || (user.IsLockedOut && RequestIsInUmbracoApplication(_httpContextAccessor, _globalSettings, _hostingEnvironment)))
{
if (throwExceptions) throw new ArgumentException("You have no privileges to the umbraco console. Please contact your administrator");
return ValidateRequestAttempt.FailedNoPrivileges;
}
return ValidateRequestAttempt.Success;
}
private static bool RequestIsInUmbracoApplication(IHttpContextAccessor httpContextAccessor, IGlobalSettings globalSettings, IHostingEnvironment hostingEnvironment)
{
return httpContextAccessor.GetRequiredHttpContext().Request.Path.ToLower().IndexOf(hostingEnvironment.ToAbsolute(globalSettings.UmbracoPath).ToLower(), StringComparison.Ordinal) > -1;
}
/// <summary>
/// Authorizes the full request, checks for SSL and validates the current user
/// </summary>
/// <param name="throwExceptions">set to true if you want exceptions to be thrown if failed</param>
/// <returns></returns>
public ValidateRequestAttempt AuthorizeRequest(bool throwExceptions = false)
{
// check for secure connection
if (_globalSettings.UseHttps && _httpContextAccessor.GetRequiredHttpContext().Request.IsSecureConnection == false)
{
if (throwExceptions) throw new SecurityException("This installation requires a secure connection (via SSL). Please update the URL to include https://");
return ValidateRequestAttempt.FailedNoSsl;
}
return ValidateCurrentUser(throwExceptions);
throw new NotImplementedException();
}
/// <summary>
/// Checks if the specified user as access to the app
/// </summary>
/// <param name="section"></param>
/// <param name="user"></param>
/// <returns></returns>
public bool UserHasSectionAccess(string section, IUser user)
public Attempt<int> GetUserId()
{
return user.HasSectionAccess(section);
throw new NotImplementedException();
}
/// <summary>
/// Ensures that a back office user is logged in
/// </summary>
/// <returns></returns>
public bool IsAuthenticated()
{
var httpContext = _httpContextAccessor.HttpContext;
return httpContext?.User != null && httpContext.User.Identity.IsAuthenticated && httpContext.GetCurrentIdentity(false) != null;
throw new NotImplementedException();
}
public bool UserHasSectionAccess(string section, IUser user)
{
throw new NotImplementedException();
}
public bool ValidateCurrentUser()
{
throw new NotImplementedException();
}
public ValidateRequestAttempt ValidateCurrentUser(bool throwExceptions, bool requiresApproval = true)
{
throw new NotImplementedException();
}
}
}

View File

@@ -152,6 +152,7 @@
<Compile Include="Macros\PartialViewMacroEngine.cs" />
<Compile Include="Mvc\UmbracoViewPageOfTModel.cs" />
<Compile Include="Security\IdentityFactoryMiddleware.cs" />
<Compile Include="Security\WebSecurity.cs" />
<Compile Include="WebApi\Filters\UmbracoTreeAuthorizeAttribute.cs" />
<Compile Include="WebAssets\CDF\ClientDependencyRuntimeMinifier.cs" />
<Compile Include="Models\NoNodesViewModel.cs" />
@@ -197,7 +198,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" />
@@ -221,7 +221,6 @@
<Compile Include="Mvc\ContainerControllerFactory.cs" />
<Compile Include="OwinExtensions.cs" />
<Compile Include="Security\BackOfficeCookieAuthenticationProvider.cs" />
<Compile Include="Security\SessionIdValidator.cs" />
<Compile Include="SignalR\PreviewHubComposer.cs" />
<Compile Include="Trees\FilesTreeController.cs" />
<Compile Include="WebAssets\CDF\ClientDependencyConfiguration.cs" />
@@ -394,7 +393,6 @@
<Compile Include="Mvc\ControllerFactoryExtensions.cs" />
<Compile Include="Mvc\IRenderMvcController.cs" />
<Compile Include="Mvc\SurfaceRouteHandler.cs" />
<Compile Include="Security\WebSecurity.cs" />
<Compile Include="CdfLogger.cs" />
<Compile Include="Controllers\UmbLoginController.cs" />
<Compile Include="UrlHelperExtensions.cs" />

View File

@@ -67,9 +67,7 @@ namespace Umbraco.Web
_variationContextAccessor.VariationContext = new VariationContext(_defaultCultureAccessor.DefaultCulture);
}
var webSecurity = new WebSecurity(_httpContextAccessor, _userService, _globalSettings, _hostingEnvironment);
return new UmbracoContext(_httpContextAccessor, _publishedSnapshotService, webSecurity, _globalSettings, _hostingEnvironment, _variationContextAccessor, _uriUtility, _cookieManager);
return new UmbracoContext(_httpContextAccessor, _publishedSnapshotService, new WebSecurity(), _globalSettings, _hostingEnvironment, _variationContextAccessor, _uriUtility, _cookieManager);
}
/// <inheritdoc />

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;