Creates a method available in V8 allowing validation of a user's credentials without actually logging them in.
This commit is contained in:
@@ -375,5 +375,13 @@ namespace Umbraco.Cms.Core.Security
|
||||
/// A user can only support a phone number if the BackOfficeUserStore is replaced with another that implements IUserPhoneNumberStore
|
||||
/// </remarks>
|
||||
Task<string> GetPhoneNumberAsync(TUser user);
|
||||
|
||||
/// <summary>
|
||||
/// Validates that a user's credentials are correct without actually logging them in.
|
||||
/// </summary>
|
||||
/// <param name="username">The user name.</param>
|
||||
/// <param name="password">The password.</param>
|
||||
/// <returns>True if the credentials are valid.</returns>
|
||||
Task<bool> ValidateCredentialsAsync(string username, string password);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
@@ -17,13 +15,12 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Security
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// A custom user store that uses Umbraco member data
|
||||
/// </summary>
|
||||
public class MemberUserStore : UmbracoUserStore<MemberIdentityUser, UmbracoIdentityRole>, IMemberUserStore
|
||||
{
|
||||
private const string genericIdentityErrorCode = "IdentityErrorUserStore";
|
||||
private const string GenericIdentityErrorCode = "IdentityErrorUserStore";
|
||||
private readonly IMemberService _memberService;
|
||||
private readonly IUmbracoMapper _mapper;
|
||||
private readonly IScopeProvider _scopeProvider;
|
||||
@@ -103,7 +100,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = genericIdentityErrorCode, Description = ex.Message }));
|
||||
return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = GenericIdentityErrorCode, Description = ex.Message }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +131,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
// we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
|
||||
var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(MemberIdentityUser.Logins));
|
||||
|
||||
var memberChangeType = UpdateMemberProperties(found, user);
|
||||
MemberDataChangeType memberChangeType = UpdateMemberProperties(found, user);
|
||||
if (memberChangeType == MemberDataChangeType.FullSave)
|
||||
{
|
||||
_memberService.Save(found);
|
||||
@@ -163,7 +160,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = genericIdentityErrorCode, Description = ex.Message }));
|
||||
return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = GenericIdentityErrorCode, Description = ex.Message }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +189,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = genericIdentityErrorCode, Description = ex.Message }));
|
||||
return Task.FromResult(IdentityResult.Failed(new IdentityError { Code = GenericIdentityErrorCode, Description = ex.Message }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,7 +502,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
|
||||
private MemberDataChangeType UpdateMemberProperties(IMember member, MemberIdentityUser identityUser)
|
||||
{
|
||||
var changeType = MemberDataChangeType.None;
|
||||
MemberDataChangeType changeType = MemberDataChangeType.None;
|
||||
|
||||
// don't assign anything if nothing has changed as this will trigger the track changes of the model
|
||||
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.LastLoginDateUtc))
|
||||
|
||||
@@ -75,11 +75,9 @@ namespace Umbraco.Cms.Core.Security
|
||||
/// <returns>True if the session is valid, else false</returns>
|
||||
public virtual async Task<bool> ValidateSessionIdAsync(string userId, string sessionId)
|
||||
{
|
||||
var userSessionStore = Store as IUserSessionStore<TUser>;
|
||||
|
||||
// if this is not set, for backwards compat (which would be super rare), we'll just approve it
|
||||
// TODO: This should be removed after members supports this
|
||||
if (userSessionStore == null)
|
||||
if (Store is not IUserSessionStore<TUser> userSessionStore)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -221,8 +219,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
var lockoutStore = Store as IUserLockoutStore<TUser>;
|
||||
if (lockoutStore == null)
|
||||
if (Store is not IUserLockoutStore<TUser> lockoutStore)
|
||||
{
|
||||
throw new NotSupportedException("The current user store does not implement " + typeof(IUserLockoutStore<>));
|
||||
}
|
||||
@@ -241,5 +238,23 @@ namespace Umbraco.Cms.Core.Security
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<bool> ValidateCredentialsAsync(string username, string password)
|
||||
{
|
||||
TUser user = await FindByNameAsync(username);
|
||||
if (user == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Store is not IUserPasswordStore<TUser> userPasswordStore)
|
||||
{
|
||||
throw new NotSupportedException("The current user store does not implement " + typeof(IUserPasswordStore<>));
|
||||
}
|
||||
|
||||
var hash = await userPasswordStore.GetPasswordHashAsync(user, new CancellationToken());
|
||||
|
||||
return await VerifyPasswordAsync(userPasswordStore, user, password) == PasswordVerificationResult.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Security
|
||||
{
|
||||
public abstract class UmbracoUserStore<TUser, TRole> : UserStoreBase<TUser, TRole, string, IdentityUserClaim<string>, IdentityUserRole<string>, IdentityUserLogin<string>, IdentityUserToken<string>, IdentityRoleClaim<string>>
|
||||
public abstract class UmbracoUserStore<TUser, TRole>
|
||||
: UserStoreBase<TUser, TRole, string, IdentityUserClaim<string>, IdentityUserRole<string>, IdentityUserLogin<string>, IdentityUserToken<string>, IdentityRoleClaim<string>>
|
||||
where TUser : UmbracoIdentityUser
|
||||
where TRole : IdentityRole<string>
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
@@ -9,6 +8,7 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
@@ -35,11 +35,21 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security
|
||||
|
||||
public MemberManager CreateSut()
|
||||
{
|
||||
var scopeProvider = new Mock<IScopeProvider>().Object;
|
||||
IScopeProvider scopeProvider = new Mock<IScopeProvider>().Object;
|
||||
_mockMemberService = new Mock<IMemberService>();
|
||||
|
||||
var mapDefinitions = new List<IMapDefinition>()
|
||||
{
|
||||
new IdentityMapDefinition(
|
||||
Mock.Of<ILocalizedTextService>(),
|
||||
Mock.Of<IEntityService>(),
|
||||
Options.Create(new GlobalSettings()),
|
||||
AppCaches.Disabled),
|
||||
};
|
||||
|
||||
_fakeMemberStore = new MemberUserStore(
|
||||
_mockMemberService.Object,
|
||||
new UmbracoMapper(new MapDefinitionCollection(new List<IMapDefinition>()), scopeProvider),
|
||||
new UmbracoMapper(new MapDefinitionCollection(mapDefinitions), scopeProvider),
|
||||
scopeProvider,
|
||||
new IdentityErrorDescriber(),
|
||||
Mock.Of<IPublishedSnapshotAccessor>());
|
||||
@@ -131,25 +141,11 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security
|
||||
{
|
||||
//arrange
|
||||
MemberManager sut = CreateSut();
|
||||
var fakeUser = new MemberIdentityUser(777)
|
||||
{
|
||||
UserName = "testUser",
|
||||
Email = "test@test.com",
|
||||
Name = "Test",
|
||||
MemberTypeAlias = "Anything",
|
||||
PasswordConfig = "testConfig"
|
||||
};
|
||||
MemberIdentityUser fakeUser = CreateValidUser();
|
||||
|
||||
var builder = new MemberTypeBuilder();
|
||||
MemberType memberType = builder.BuildSimpleMemberType();
|
||||
IMember fakeMember = CreateMember(fakeUser);
|
||||
|
||||
IMember fakeMember = new Member(memberType)
|
||||
{
|
||||
Id = 777
|
||||
};
|
||||
|
||||
_mockMemberService.Setup(x => x.CreateMember(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Returns(fakeMember);
|
||||
_mockMemberService.Setup(x => x.Save(fakeMember, false));
|
||||
MockMemberServiceForCreateMember(fakeMember);
|
||||
|
||||
//act
|
||||
IdentityResult identityResult = await sut.CreateAsync(fakeUser);
|
||||
@@ -158,5 +154,99 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security
|
||||
Assert.IsTrue(identityResult.Succeeded);
|
||||
Assert.IsTrue(!identityResult.Errors.Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GivenAUserExists_AndTheCorrectCredentialsAreProvided_ThenACheckOfCredentialsShouldSucceed()
|
||||
{
|
||||
//arrange
|
||||
var password = "password";
|
||||
MemberManager sut = CreateSut();
|
||||
|
||||
MemberIdentityUser fakeUser = CreateValidUser();
|
||||
|
||||
IMember fakeMember = CreateMember(fakeUser);
|
||||
|
||||
MockMemberServiceForCreateMember(fakeMember);
|
||||
|
||||
_mockMemberService.Setup(x => x.GetByUsername(It.Is<string>(y => y == fakeUser.UserName))).Returns(fakeMember);
|
||||
|
||||
_mockPasswordHasher.Setup(x => x.VerifyHashedPassword(It.IsAny<MemberIdentityUser>(), It.IsAny<string>(), It.IsAny<string>())).Returns(PasswordVerificationResult.Success);
|
||||
|
||||
//act
|
||||
await sut.CreateAsync(fakeUser);
|
||||
var result = await sut.ValidateCredentialsAsync(fakeUser.UserName, password);
|
||||
|
||||
//assert
|
||||
Assert.IsTrue(result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GivenAUserExists_AndIncorrectCredentialsAreProvided_ThenACheckOfCredentialsShouldFail()
|
||||
{
|
||||
//arrange
|
||||
var password = "password";
|
||||
MemberManager sut = CreateSut();
|
||||
|
||||
MemberIdentityUser fakeUser = CreateValidUser();
|
||||
|
||||
IMember fakeMember = CreateMember(fakeUser);
|
||||
|
||||
MockMemberServiceForCreateMember(fakeMember);
|
||||
|
||||
_mockMemberService.Setup(x => x.GetByUsername(It.Is<string>(y => y == fakeUser.UserName))).Returns(fakeMember);
|
||||
|
||||
_mockPasswordHasher.Setup(x => x.VerifyHashedPassword(It.IsAny<MemberIdentityUser>(), It.IsAny<string>(), It.IsAny<string>())).Returns(PasswordVerificationResult.Failed);
|
||||
|
||||
//act
|
||||
await sut.CreateAsync(fakeUser);
|
||||
var result = await sut.ValidateCredentialsAsync(fakeUser.UserName, password);
|
||||
|
||||
//assert
|
||||
Assert.IsFalse(result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GivenAUserDoesExists_AndCredentialsAreProvided_ThenACheckOfCredentialsShouldFail()
|
||||
{
|
||||
//arrange
|
||||
var password = "password";
|
||||
MemberManager sut = CreateSut();
|
||||
|
||||
_mockMemberService.Setup(x => x.GetByUsername(It.Is<string>(y => y == "testUser"))).Returns((IMember)null);
|
||||
|
||||
//act
|
||||
var result = await sut.ValidateCredentialsAsync("testUser", password);
|
||||
|
||||
//assert
|
||||
Assert.IsFalse(result);
|
||||
}
|
||||
|
||||
private static MemberIdentityUser CreateValidUser() =>
|
||||
new MemberIdentityUser(777)
|
||||
{
|
||||
UserName = "testUser",
|
||||
Email = "test@test.com",
|
||||
Name = "Test",
|
||||
MemberTypeAlias = "Anything",
|
||||
PasswordConfig = "testConfig",
|
||||
PasswordHash = "hashedPassword"
|
||||
};
|
||||
|
||||
private static IMember CreateMember(MemberIdentityUser fakeUser)
|
||||
{
|
||||
var builder = new MemberTypeBuilder();
|
||||
MemberType memberType = builder.BuildSimpleMemberType();
|
||||
return new Member(memberType)
|
||||
{
|
||||
Id = 777,
|
||||
Username = fakeUser.UserName,
|
||||
};
|
||||
}
|
||||
|
||||
private void MockMemberServiceForCreateMember(IMember fakeMember)
|
||||
{
|
||||
_mockMemberService.Setup(x => x.CreateMember(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Returns(fakeMember);
|
||||
_mockMemberService.Setup(x => x.Save(fakeMember, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
@@ -23,8 +24,6 @@ namespace Umbraco.Cms.Web.Common.Security
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public IUser CurrentUser
|
||||
{
|
||||
@@ -39,7 +38,7 @@ namespace Umbraco.Cms.Web.Common.Security
|
||||
//Check again
|
||||
if (_currentUser == null)
|
||||
{
|
||||
var id = GetUserId();
|
||||
Attempt<int> id = GetUserId();
|
||||
_currentUser = id ? _userService.GetUserById(id.Result) : null;
|
||||
}
|
||||
}
|
||||
@@ -52,22 +51,18 @@ namespace Umbraco.Cms.Web.Common.Security
|
||||
/// <inheritdoc />
|
||||
public Attempt<int> GetUserId()
|
||||
{
|
||||
var identity = _httpContextAccessor.HttpContext?.GetCurrentIdentity();
|
||||
ClaimsIdentity identity = _httpContextAccessor.HttpContext?.GetCurrentIdentity();
|
||||
return identity == null ? Attempt.Fail<int>() : Attempt.Succeed(identity.GetId());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsAuthenticated()
|
||||
{
|
||||
var httpContext = _httpContextAccessor.HttpContext;
|
||||
HttpContext httpContext = _httpContextAccessor.HttpContext;
|
||||
return httpContext?.User != null && httpContext.User.Identity.IsAuthenticated && httpContext.GetCurrentIdentity() != null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool UserHasSectionAccess(string section, IUser user)
|
||||
{
|
||||
return user.HasSectionAccess(section);
|
||||
}
|
||||
|
||||
public bool UserHasSectionAccess(string section, IUser user) => user.HasSectionAccess(section);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Extensions;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Net;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using System.Threading.Tasks;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.Security
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user