Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Back office user updates to support OAuth and 8.9 features #9470

Merged
merged 23 commits into from
Dec 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
88c1259
Revert "Revert "Moves some files, adds notes, starts poc for back off…
bergmania Nov 27, 2020
d6357e8
Revert "Revert "Gets oauth working (with google) now need to test oth…
bergmania Nov 27, 2020
541ff0f
Revert "Revert "Moves auto linking logic to the BackOfficeSignInManag…
bergmania Nov 27, 2020
a098b95
Revert "Revert "Ensures that all back office controllers are authenti…
bergmania Nov 27, 2020
26dc921
Revert "Revert "Cleans up BackofficeSecurity, fixes up Authentication…
bergmania Nov 27, 2020
5efa93d
Revert "Revert "FIxes more of the auth procedure""
bergmania Nov 27, 2020
36830e4
remove c# 9
Shazwazza Nov 30, 2020
6176046
Deals with the Xsrf notes and handling in sign in manager
Shazwazza Nov 30, 2020
efff96f
Creates IBackOfficeSignInManager interface
Shazwazza Nov 30, 2020
47b4a4d
FIxes authz on some controllers, fixes js var paths for login provide…
Shazwazza Nov 30, 2020
9273351
try fixing watch task
Shazwazza Dec 1, 2020
20b4f55
Fixes up a bunch of TODOs moves user manager to the back office proje…
Shazwazza Dec 1, 2020
4671d9d
moves the back office user auditing logic
Shazwazza Dec 1, 2020
fe5dcd8
removes the 2FA store implementation since that will need to be manua…
Shazwazza Dec 1, 2020
999be04
cleaning up TODOs
Shazwazza Dec 2, 2020
37cf0d3
clean up TODOs
Shazwazza Dec 2, 2020
0f0c50b
Merge remote-tracking branch 'origin/netcore/netcore' into netcore/fe…
Shazwazza Dec 2, 2020
4f7c87d
Fix build issues after merge
Shazwazza Dec 2, 2020
372674a
re-adds test scheme for test authn/authz
Shazwazza Dec 2, 2020
0846fc5
Cleans up IBackofficeSecurity, ensures authn for the AuthenticationCo…
Shazwazza Dec 2, 2020
e297bc8
oops still have c# 9 stuff
Shazwazza Dec 2, 2020
d0e17d1
ext method for authn back office scheme with null check, fixing tests
Shazwazza Dec 2, 2020
9e6baf7
reverts js changes to continue to support multiple destinations but w…
Shazwazza Dec 3, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/Umbraco.Core/BackOffice/ClaimsPrincipalExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public static class ClaimsPrincipalExtensions
/// <returns></returns>
public static UmbracoBackOfficeIdentity GetUmbracoIdentity(this IPrincipal user)
{
// TODO: It would be nice to get rid of this and only rely on Claims, not a strongly typed identity instance

//If it's already a UmbracoBackOfficeIdentity
if (user.Identity is UmbracoBackOfficeIdentity backOfficeIdentity) return backOfficeIdentity;

Expand Down Expand Up @@ -53,10 +55,10 @@ public static UmbracoBackOfficeIdentity GetUmbracoIdentity(this IPrincipal user)
/// <returns></returns>
public static double GetRemainingAuthSeconds(this IPrincipal user, DateTimeOffset now)
{
var umbIdentity = user.GetUmbracoIdentity();
if (umbIdentity == null) return 0;
var claimsPrincipal = user as ClaimsPrincipal;
if (claimsPrincipal == null) return 0;

var ticketExpires = umbIdentity.FindFirstValue(Constants.Security.TicketExpiresClaimType);
var ticketExpires = claimsPrincipal.FindFirst(Constants.Security.TicketExpiresClaimType)?.Value;
if (ticketExpires.IsNullOrWhiteSpace()) return 0;

var utcExpired = DateTimeOffset.Parse(ticketExpires, null, DateTimeStyles.RoundtripKind);
Expand Down
4 changes: 4 additions & 0 deletions src/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ namespace Umbraco.Core.BackOffice
[Serializable]
public class UmbracoBackOfficeIdentity : ClaimsIdentity
{
// TODO: Ideally we remove this class and only deal with ClaimsIdentity as a best practice. All things relevant to our own
// identity are part of claims. This class would essentially become extension methods on a ClaimsIdentity for resolving
// values from it.

public static bool FromClaimsIdentity(ClaimsIdentity identity, out UmbracoBackOfficeIdentity backOfficeIdentity)
{
//validate that all claims exist
Expand Down
36 changes: 13 additions & 23 deletions src/Umbraco.Core/Security/IBackofficeSecurity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,48 +9,38 @@ public interface IBackOfficeSecurity
/// <summary>
/// Gets the current user.
/// </summary>
/// <value>The current user.</value>
/// <returns>The current user that has been authenticated for the request.</returns>
/// <remarks>If authentication hasn't taken place this will be null.</remarks>
// TODO: This is used a lot but most of it can be refactored to not use this at all since the IUser instance isn't
// needed in most cases. Where an IUser is required this could be an ext method on the ClaimsIdentity/ClaimsPrincipal that passes in
// an IUserService, like HttpContext.User.GetUmbracoUser(_userService);
// This one isn't as easy to remove as the others below.
IUser CurrentUser { get; }

/// <summary>
/// Gets the current user's id.
/// </summary>
/// <returns></returns>
/// <returns>The current user's Id that has been authenticated for the request.</returns>
/// <remarks>If authentication hasn't taken place this will be unsuccessful.</remarks>
// TODO: This should just be an extension method on ClaimsIdentity
Attempt<int> GetUserId();

/// <summary>
/// Validates the currently logged in user and ensures they are not timed out
/// </summary>
/// <returns></returns>
bool ValidateCurrentUser();

/// <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>
ValidateRequestAttempt ValidateCurrentUser(bool throwExceptions, bool requiresApproval = true);

/// <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>
ValidateRequestAttempt AuthorizeRequest(bool throwExceptions = false);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer needed, the more we can remove from this the better


/// <summary>
/// Checks if the specified user as access to the app
/// </summary>
/// <param name="section"></param>
/// <param name="user"></param>
/// <returns></returns>
/// <remarks>If authentication hasn't taken place this will be unsuccessful.</remarks>
// TODO: Should be part of IBackOfficeUserManager
bool UserHasSectionAccess(string section, IUser user);

/// <summary>
/// Ensures that a back office user is logged in
/// </summary>
/// <returns></returns>
/// <remarks>This does not force authentication, that must be done before calls to this are made.</remarks>
// TODO: Should be removed, this should not be necessary
bool IsAuthenticated();
}
}
14 changes: 0 additions & 14 deletions src/Umbraco.Core/Security/ValidateRequestAttempt.cs

This file was deleted.

90 changes: 34 additions & 56 deletions src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Umbraco.Core.Models;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services;

namespace Umbraco.Core.BackOffice
Expand All @@ -22,24 +23,27 @@ public class BackOfficeUserStore : DisposableObjectSlim,
IUserLoginStore<BackOfficeIdentityUser>,
IUserRoleStore<BackOfficeIdentityUser>,
IUserSecurityStampStore<BackOfficeIdentityUser>,
IUserLockoutStore<BackOfficeIdentityUser>,
IUserTwoFactorStore<BackOfficeIdentityUser>,
IUserLockoutStore<BackOfficeIdentityUser>,
IUserSessionStore<BackOfficeIdentityUser>

// TODO: This would require additional columns/tables for now people will need to implement this on their own
//IUserPhoneNumberStore<BackOfficeIdentityUser, int>,
// TODO: To do this we need to implement IQueryable - we'll have an IQuerable implementation soon with the UmbracoLinqPadDriver implementation
//IQueryableUserStore<BackOfficeIdentityUser, int>
// TODO: This would require additional columns/tables and then a lot of extra coding support to make this happen natively within umbraco
//IUserTwoFactorStore<BackOfficeIdentityUser>,
// TODO: This would require additional columns/tables for now people will need to implement this on their own
//IUserPhoneNumberStore<BackOfficeIdentityUser, int>,
// TODO: To do this we need to implement IQueryable - we'll have an IQuerable implementation soon with the UmbracoLinqPadDriver implementation
//IQueryableUserStore<BackOfficeIdentityUser, int>
{
private readonly IScopeProvider _scopeProvider;
private readonly IUserService _userService;
private readonly IEntityService _entityService;
private readonly IExternalLoginService _externalLoginService;
private readonly GlobalSettings _globalSettings;
private readonly UmbracoMapper _mapper;
private bool _disposed = false;

public BackOfficeUserStore(IUserService userService, IEntityService entityService, IExternalLoginService externalLoginService, IOptions<GlobalSettings> globalSettings, UmbracoMapper mapper)
public BackOfficeUserStore(IScopeProvider scopeProvider, IUserService userService, IEntityService entityService, IExternalLoginService externalLoginService, IOptions<GlobalSettings> globalSettings, UmbracoMapper mapper)
{
_scopeProvider = scopeProvider;
_userService = userService;
_entityService = entityService;
_externalLoginService = externalLoginService;
Expand Down Expand Up @@ -156,7 +160,7 @@ public Task SetNormalizedUserNameAsync(BackOfficeIdentityUser user, string norma
/// <param name="user"/>
/// <param name="cancellationToken"></param>
/// <returns/>
public async Task<IdentityResult> UpdateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
public Task<IdentityResult> UpdateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
Expand All @@ -168,31 +172,34 @@ public Task SetNormalizedUserNameAsync(BackOfficeIdentityUser user, string norma
throw new InvalidOperationException("The user id must be an integer to work with the Umbraco");
}

// TODO: Wrap this in a scope!

var found = _userService.GetUserById(asInt.Result);
if (found != null)
using (var scope = _scopeProvider.CreateScope())
{
// we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(BackOfficeIdentityUser.Logins));

if (UpdateMemberProperties(found, user))
var found = _userService.GetUserById(asInt.Result);
if (found != null)
{
_userService.Save(found);
}
// we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(BackOfficeIdentityUser.Logins));

if (isLoginsPropertyDirty)
{
_externalLoginService.Save(
found.Id,
user.Logins.Select(x => new ExternalLogin(
x.LoginProvider,
x.ProviderKey,
x.UserData)));
if (UpdateMemberProperties(found, user))
{
_userService.Save(found);
}

if (isLoginsPropertyDirty)
{
_externalLoginService.Save(
found.Id,
user.Logins.Select(x => new ExternalLogin(
x.LoginProvider,
x.ProviderKey,
x.UserData)));
}
}

scope.Complete();
}

return IdentityResult.Success;
return Task.FromResult(IdentityResult.Success);
}

/// <summary>
Expand Down Expand Up @@ -627,35 +634,6 @@ private BackOfficeIdentityUser AssignLoginsCallback(BackOfficeIdentityUser user)
return user;
}

/// <summary>
/// Sets whether two factor authentication is enabled for the user
/// </summary>
/// <param name="user"/>
/// <param name="enabled"/>
/// <param name="cancellationToken"></param>
/// <returns/>
public virtual Task SetTwoFactorEnabledAsync(BackOfficeIdentityUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();

user.TwoFactorEnabled = false;
return Task.CompletedTask;
}

/// <summary>
/// Returns whether two factor authentication is enabled for the user
/// </summary>
/// <param name="user"/>
/// <returns/>
public virtual Task<bool> GetTwoFactorEnabledAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();

return Task.FromResult(false);
}

#region IUserLockoutStore

/// <summary>
Expand Down
14 changes: 11 additions & 3 deletions src/Umbraco.Infrastructure/BackOffice/IBackOfficeUserManager.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
Expand All @@ -15,6 +16,12 @@ public interface IBackOfficeUserManager : IBackOfficeUserManager<BackOfficeIdent
public interface IBackOfficeUserManager<TUser>: IDisposable
where TUser : BackOfficeIdentityUser
{
Task<string> GetUserIdAsync(TUser user);

Task<TUser> GetUserAsync(ClaimsPrincipal principal);

string GetUserId(ClaimsPrincipal principal);

Task<IList<UserLoginInfo>> GetLoginsAsync(TUser user);

Task<IdentityResult> DeleteAsync(TUser user);
Expand Down Expand Up @@ -304,13 +311,14 @@ Task<IdentityResult> ChangePasswordAsync(TUser user, string currentPassword,
/// </remarks>
Task<string> GetPhoneNumberAsync(TUser user);

// TODO: These are raised from outside the signinmanager and usermanager in the auth and user controllers,
// let's see if there's a way to avoid that and only have these called within signinmanager and usermanager
// which means we can remove these from the interface (things like invite seems like they cannot be moved)
void RaiseForgotPasswordRequestedEvent(IPrincipal currentUser, int userId);
void RaiseForgotPasswordChangedSuccessEvent(IPrincipal currentUser, int userId);
SignOutAuditEventArgs RaiseLogoutSuccessEvent(IPrincipal currentUser, int userId);
UserInviteEventArgs RaiseSendingUserInvite(IPrincipal currentUser, UserInvite invite, IUser createdUser);

void RaiseLoginSuccessEvent(TUser currentUser, int userId);

bool HasSendingUserInviteEventHandler { get; }

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions
{
public const string TestAuthenticationScheme = "Test";

private readonly BackOfficeSignInManager _backOfficeSignInManager;
private readonly IBackOfficeSignInManager _backOfficeSignInManager;

private readonly BackOfficeIdentityUser _fakeUser;
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, BackOfficeSignInManager backOfficeSignInManager, IUserService userService, UmbracoMapper umbracoMapper)
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IBackOfficeSignInManager backOfficeSignInManager, IUserService userService, UmbracoMapper umbracoMapper)
: base(options, logger, encoder, clock)
{
_backOfficeSignInManager = backOfficeSignInManager;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
using Umbraco.Web.BackOffice.Controllers;
using Umbraco.Web.BackOffice.Routing;
using Umbraco.Web.Common.Install;
using Umbraco.Web.Common.Security;
using Umbraco.Web.WebApi;

namespace Umbraco.Tests.UnitTests.AutoFixture
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,6 @@ IHttpController IHttpControllerActivator.Create(HttpRequestMessage request, Http
.Returns(mockUser.Object);

//mock Validate
backofficeSecurity.Setup(x => x.ValidateCurrentUser())
.Returns(() => true);
backofficeSecurity.Setup(x => x.UserHasSectionAccess(It.IsAny<string>(), It.IsAny<IUser>()))
.Returns(() => true);

Expand Down
1 change: 0 additions & 1 deletion src/Umbraco.Tests/Testing/TestingTests/MockTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ public void Can_Mock_UmbracoApiController_Dependencies_With_Injected_UmbracoMapp
var memberService = Mock.Of<IMemberService>();
var memberTypeService = Mock.Of<IMemberTypeService>();
var membershipProvider = new MembersMembershipProvider(memberService, memberTypeService, Mock.Of<IUmbracoVersion>(), TestHelper.GetHostingEnvironment(), TestHelper.GetIpResolver());
var membershipHelper = new MembershipHelper(Mock.Of<IHttpContextAccessor>(), Mock.Of<IPublishedMemberCache>(), membershipProvider, Mock.Of<RoleProvider>(), memberService, memberTypeService, Mock.Of<IPublicAccessService>(), AppCaches.Disabled, NullLoggerFactory.Instance, ShortStringHelper, Mock.Of<IEntityService>());
var umbracoMapper = new UmbracoMapper(new MapDefinitionCollection(new[] { Mock.Of<IMapDefinition>() }));

var umbracoApiController = new FakeUmbracoApiController(new GlobalSettings(), Mock.Of<IUmbracoContextAccessor>(), Mock.Of<IBackOfficeSecurityAccessor>(), Mock.Of<ISqlContext>(), ServiceContext.CreatePartial(), AppCaches.NoCache, profilingLogger , Mock.Of<IRuntimeState>(), umbracoMapper, Mock.Of<IPublishedUrlProvider>());
Expand Down
21 changes: 10 additions & 11 deletions src/Umbraco.Web.BackOffice/Authorization/BackOfficeHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,17 @@ public BackOfficeHandler(IBackOfficeSecurityAccessor backOfficeSecurity, IRuntim

protected override Task<bool> IsAuthorized(AuthorizationHandlerContext context, BackOfficeRequirement requirement)
{
try
{
// if not configured (install or upgrade) then we can continue
// otherwise we need to ensure that a user is logged in
var isAuth = _runtimeState.Level == RuntimeLevel.Install
|| _runtimeState.Level == RuntimeLevel.Upgrade
|| _backOfficeSecurity.BackOfficeSecurity?.ValidateCurrentUser(false, requirement.RequireApproval) == ValidateRequestAttempt.Success;
return Task.FromResult(isAuth);
}
catch (Exception)
// if not configured (install or upgrade) then we can continue
// otherwise we need to ensure that a user is logged in

switch (_runtimeState.Level)
{
return Task.FromResult(false);
case RuntimeLevel.Install:
case RuntimeLevel.Upgrade:
return Task.FromResult(true);
default:
var userApprovalSucceeded = !requirement.RequireApproval || (_backOfficeSecurity.BackOfficeSecurity.CurrentUser?.IsApproved ?? false);
return Task.FromResult(userApprovalSucceeded);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Authorization;
using System.Threading.Tasks;
using Umbraco.Web.BackOffice.Security;
using Umbraco.Web.Common.Security;

namespace Umbraco.Web.BackOffice.Authorization
Expand Down
Loading